Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugins/native-ocr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
dist/
releases/
.DS_Store
*.log
.env
.env.*
50 changes: 50 additions & 0 deletions plugins/native-ocr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 更新日志

Native OCR 从官方 `wechat-ocr` 插件 fork 而来,作者署名 Samson。以下版本历史已按里程碑整合。

## v0.6.11(当前)

- Windows 引擎排序调整:ONNX OCR(识别率最优)排第一
- 切换引擎时自动用新引擎重新识别当前图片

## v0.6.7 ~ v0.6.10

- **微信 OCR 收缩为 macOS 专属**,Windows 引擎定稿为:ONNX OCR + Windows OCR(系统 WinRT)
- Windows OCR(WinRT)稳定性修复:补齐 `System.Runtime.WindowsRuntime` 程序集加载;输出统一 UTF-8 消除中文乱码;脚本带版本标记
- Windows OCR 识别率优化:小图(最长边 < 1200px)解码阶段自动放大,提升小字识别率
- 防御性回退:放大路径失败时自动退回原始路径,保证识别始终可用

## v0.6.0 ~ v0.6.6

- **三大引擎双端架构落地,全程零 Python**:
- ONNX OCR:PP-OCR v4 模型(约 60MB 一次性下载),Electron 自带 Node 子进程运行,双端通用,支持表格识别
- 系统 OCR:macOS Vision(Swift)/ Windows OCR(WinRT PowerShell)
- 微信 OCR:macOS 原生运行时(npmmirror 按需下载)
- 运行时下载框架:npmmirror 源 + sha1 校验 + 原子替换
- 修复 ONNX 引擎 "process is not defined"(改为 `ELECTRON_RUN_AS_NODE` 子进程常驻服务)

## v0.5.0 ~ v0.5.3

- Windows 平台初步支持(过渡方案:RapidOCR / wechat-ocr Python 链路)
- Windows 引擎不可用时的自动降级与引导

## v0.4.0

- 双平台能力补齐:Windows 系统级 OCR 与历史管理适配

## v0.3.0 ~ v0.3.1

- 表格视图翻译修复
- Windows 引擎错误处理与引导优化

## v0.2.0 ~ v0.2.1

- **表格识别**:行列聚类模型、分隔线拖拽/增删、单元格编辑、低置信度标记、TSV/CSV 导出
- **OCR 后翻译**:文本/表格双视图翻译
- **识别历史**:本地持久化、回放、单条删除、一键清空

## v0.1.0 ~ v0.1.1

- 从官方 `wechat-ocr` 插件 fork,改名 `native-ocr`(Native OCR)
- 新增 macOS Vision 引擎(系统内置,零依赖)
- 平台支持 darwin + win32
21 changes: 21 additions & 0 deletions plugins/native-ocr/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 ztocwst

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
53 changes: 53 additions & 0 deletions plugins/native-ocr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Native OCR

ZTools 本地多引擎 OCR 插件。支持 macOS Vision / Windows OCR(系统 WinRT)/ ONNX OCR(PP-OCR v4)/ 微信 OCR(macOS)四类引擎,内置表格识别、OCR 后翻译、识别历史。

作者:Samson(fork 自官方 `wechat-ocr` 插件,原作者 zing,MIT 协议)

## 引擎

| 引擎 | macOS | Windows | 说明 |
| --- | --- | --- | --- |
| ONNX OCR | ✅ | ✅(默认首选) | PP-OCR v4 模型,约 60MB 一次性下载,离线可用,支持表格识别,无需任何环境 |
| 系统 OCR | macOS Vision | Windows OCR | 系统内置能力,零依赖;小图自动放大提升识别率 |
| 微信 OCR | ✅ | — | 复用微信自带离线 OCR 运行时(按需从 npmmirror 下载) |

Windows 平台默认展示 ONNX OCR,切换引擎后自动用新引擎重新识别当前图片。

## 功能

- **图片 OCR**:拖入/粘贴/截图,多引擎结果对比(切换引擎自动重识别)
- **表格识别**:行列聚类还原表格,分隔线可拖动/增删,单元格可编辑,导出 TSV/CSV
- **翻译**:识别结果一键翻译(文本/表格双视图)
- **识别历史**:本地持久化,可回放、删除、清空
- **批量识别**:多图队列,合并复制 / 导出 TXT

## 运行时下载

ONNX OCR 与微信 OCR 运行时不内置在插件包中,首次使用时从 npmmirror 下载并校验后缓存到本地:

- ONNX OCR:`~/Library/Application Support/ZTools/native-ocr/onnx-runtime`
- 微信 OCR(macOS):`~/Library/Application Support/ZTools/native-ocr/ocr-runtime`

## 开发

```bash
npm install
npm run dev
```

## 构建

```bash
npm run build
```

构建产物在 `dist/`,README/LICENSE/bin 会自动复制进产物目录。

## 更新日志

见 [CHANGELOG.md](./CHANGELOG.md)。

## 致谢

本项目基于 [ZToolsCenter 官方 wechat-ocr 插件](https://github.com/ZToolsCenter/ZTools-plugins/tree/main/plugins/wechat-ocr) fork 修改(原作者 zing,MIT 协议)。
4 changes: 4 additions & 0 deletions plugins/native-ocr/bin/build-swift.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
swiftc -O bin/ocr-vision.swift -o bin/ocr-vision
chmod +x bin/ocr-vision
60 changes: 60 additions & 0 deletions plugins/native-ocr/bin/ocr-vision.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import Vision
import AppKit

guard CommandLine.arguments.count > 1 else {
FileHandle.standardError.write("Usage: ocr-vision <image-path>\n".data(using: .utf8)!)
exit(2)
}

let imagePath = CommandLine.arguments[1]
let url = URL(fileURLWithPath: imagePath)

guard let image = NSImage(contentsOf: url),
let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData),
let cgImage = bitmap.cgImage else {
FileHandle.standardError.write("无法加载图片: \(imagePath)\n".data(using: .utf8)!)
exit(3)
}

let request = VNRecognizeTextRequest { req, err in
if let err = err {
FileHandle.standardError.write("识别失败: \(err.localizedDescription)\n".data(using: .utf8)!)
exit(4)
}
guard let observations = req.results as? [VNRecognizedTextObservation] else { return }

var items: [[String: Any]] = []
for obs in observations {
guard let top = obs.topCandidates(1).first else { continue }
let box = obs.boundingBox
items.append([
"text": top.string,
"box": [
"x": Double(box.origin.x),
"y": Double(box.origin.y),
"w": Double(box.size.width),
"h": Double(box.size.height)
]
])
}

guard let data = try? JSONSerialization.data(withJSONObject: items, options: []),
let json = String(data: data, encoding: .utf8) else {
FileHandle.standardError.write("识别结果序列化失败\n".data(using: .utf8)!)
exit(4)
}
FileHandle.standardOutput.write((json + "\n").data(using: .utf8)!)
}
request.recognitionLevel = .accurate
request.recognitionLanguages = ["zh-Hans", "zh-Hant", "en-US"]
request.usesLanguageCorrection = true

let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
do {
try handler.perform([request])
} catch {
FileHandle.standardError.write("执行失败: \(error.localizedDescription)\n".data(using: .utf8)!)
exit(5)
}
156 changes: 156 additions & 0 deletions plugins/native-ocr/bin/ocr-winrt.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Windows system OCR (WinRT Windows.Media.Ocr) for the native-ocr ZTools plugin.
# macOS equivalent of ocr-vision.swift. Runs on built-in PowerShell 5.1+ with
# zero dependencies. Output contract (stdout, single JSON line):
# { "width": int, "height": int, "lines": [ { "text": str,
# "box": { "x","y","w","h" } } ] }
# Box convention matches the macOS Vision engine: normalized to [0,1] with
# the origin at the image's lower-left corner (y measured from the bottom).
# Exit codes: 0 ok / 2 bad args / 4 engine unavailable / 5 ocr failed

param(
[Parameter(Mandatory = $true)][string]$ImagePath
)

$ErrorActionPreference = 'Stop'

$scriptVersion = "0.6.10"

# Emit stdout/stderr as UTF-8 so the Node side decodes Chinese messages correctly.
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}

# Required for [System.WindowsRuntimeSystemExtensions] (AsTask reflection below).
# Some systems do not auto-load it when resolving WinRT types.
try { Add-Type -AssemblyName System.Runtime.WindowsRuntime } catch {}

$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Storage.StorageFile, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Globalization.Language, Windows.Foundation, ContentType = WindowsRuntime]
$null = [Windows.Security.Cryptography.CryptographicBuffer, Windows.Foundation, ContentType = WindowsRuntime]

function Await($WinRtTask, $ResultType) {
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() |
Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
if (-not $asTaskGeneric) {
throw 'WinRT AsTask helper not found'
}
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
$netTask = $asTask.Invoke($null, @($WinRtTask))
$netTask.Wait(-1) | Out-Null
return $netTask.Result
}

function Fail($code, $message) {
[Console]::Error.WriteLine("[winrt-$scriptVersion] $message")
exit $code
}

if (-not (Test-Path -LiteralPath $ImagePath)) {
Fail 3 "image not found: $ImagePath"
}

# Prefer Chinese, then any user-profile language.
$engine = $null
try {
$available = @([Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages)
$preferred = $available | Where-Object { $_.LanguageTag -like 'zh*' } | Select-Object -First 1
if ($preferred) {
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($preferred)
}
if (-not $engine) {
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
}
} catch {
Fail 4 "ocr engine init failed: $($_.Exception.Message)"
}
if (-not $engine) {
Fail 4 "ocr engine unavailable: install a language pack (Settings > Time & Language > Language)"
}

try {
$file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync((Resolve-Path -LiteralPath $ImagePath).Path)) ([Windows.Storage.StorageFile])
$stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])

# Accuracy boost: small images / small text hurt WinRT OCR badly, so upscale
# at decode time (max edge < 1200px gets 2x, capped at 4000px). Boxes are
# normalized afterwards, so coordinates stay consistent with the source.
$srcW = [uint32]$decoder.PixelWidth
$srcH = [uint32]$decoder.PixelHeight
$outW = $srcW
$outH = $srcH
$transform = New-Object Windows.Graphics.Imaging.BitmapTransform
$maxEdge = [Math]::Max($srcW, $srcH)
if ($maxEdge -gt 0 -and $maxEdge -lt 1200) {
$scale = [Math]::Min(4.0, 1200.0 / [double]$maxEdge)
$outW = [uint32][Math]::Min(4000, [Math]::Round([double]$srcW * $scale))
$outH = [uint32][Math]::Min(4000, [Math]::Round([double]$srcH * $scale))
$transform.ScaledWidth = $outW
$transform.ScaledHeight = $outH
}
$pixelData = Await ($decoder.GetPixelDataAsync(
[Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8,
[Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied,
$transform,
[Windows.Graphics.Imaging.ExifOrientationMode]::IgnoreExifOrientation,
[Windows.Graphics.Imaging.ColorManagementMode]::DoNotColorManage)) ([Windows.Graphics.Imaging.PixelDataProvider])
# PowerShell 的 byte[] 不能直接匹配 IBuffer 重载,需转成 IBuffer。
# 任何失败都回退到原始不放大路径,保证 OCR 始终可用。
$bitmap = $null
try {
$pixels = $pixelData.DetachPixelData()
$buffer = [Windows.Security.Cryptography.CryptographicBuffer]::CreateFromByteArray($pixels)
$bitmap = [Windows.Graphics.Imaging.SoftwareBitmap]::CreateCopyFromBuffer(
$buffer,
[Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8,
[int]$outW,
[int]$outH)
} catch {
$bitmap = $null
}
if (-not $bitmap) {
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
}
$result = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])

$width = [double]$srcW
$height = [double]$srcH
$lines = @()
foreach ($line in $result.Lines) {
$text = $line.Text
if ([string]::IsNullOrWhiteSpace($text)) { continue }
$rect = $null
foreach ($word in $line.Words) {
$r = $word.BoundingRect
if ($null -eq $rect) {
$rect = @{
left = $r.X; top = $r.Y
right = ($r.X + $r.Width); bottom = ($r.Y + $r.Height)
}
} else {
$rect.left = [Math]::Min($rect.left, $r.X)
$rect.top = [Math]::Min($rect.top, $r.Y)
$rect.right = [Math]::Max($rect.right, $r.X + $r.Width)
$rect.bottom = [Math]::Max($rect.bottom, $r.Y + $r.Height)
}
}
if ($null -eq $rect) {
$rect = @{ left = 0; top = 0; right = $width; bottom = $height }
}
$lines += @{
text = $text
box = @{
x = [Math]::Max(0.0, [Math]::Min(1.0, $rect.left / $width))
y = [Math]::Max(0.0, [Math]::Min(1.0, 1.0 - $rect.bottom / $height))
w = [Math]::Max(0.0, [Math]::Min(1.0, ($rect.right - $rect.left) / $width))
h = [Math]::Max(0.0, [Math]::Min(1.0, ($rect.bottom - $rect.top) / $height))
}
}
}

$payload = @{ width = $width; height = $height; lines = $lines }
[Console]::Out.Write(($payload | ConvertTo-Json -Compress -Depth 5))
exit 0
} catch {
Fail 5 "ocr failed: $($_.Exception.Message)"
}
Loading
Loading