HP/Google 的 LSTM OCR 引擎,用 OHOS NDK 把引擎与 leptonica 等依赖交叉编译为静态库再经 NAPI 封成端侧 Demo;攻克 libpng 生成器丢 target、try_run 交叉编译非法、CMAKE_SYSTEM_NAME 设定等难题,最终实现端侧异步、不阻塞 UI 的中英文 OCR。
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 18 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 12 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 | ||
| 20 天前 |
Tesseract OCR 的 OpenHarmony 适配记录
这份文档记录我们把 Tesseract OCR 引擎搬到 OpenHarmony / HarmonyOS 上的过程:交叉编译怎么搭、依赖链怎么裁、引擎怎么用 NAPI 接进 ArkTS、demo 应用怎么编出 HAP,以及最后哪些能力在端侧能用、哪些按这个平台的定位本来就不该做。
要先说清楚一件事:Tesseract 不是个 GUI 程序。它原本是一套 C++ 写的 OCR 引擎库(libtesseract)外加一个命令行工具 tesseract,给开发者拿来做文字识别用的。所以这次「移植」跟把一个桌面应用塞进 HAP 不是一回事——我们做的是把这套库交叉编译成 OpenHarmony 能用的静态库,再写一个最小的 demo 应用把它跑起来、证明端侧 OCR 这条路通。后面的 T0/T1/T2 也都按「库 / 命令行」的口径来评,不是按「桌面应用」。
仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Tesseract
整体思路一句话:上游的 Tesseract C++ 源码一行没改,全部适配工作都压在交叉编译的工具链和构建胶水上。引擎本身是可移植的 C++17,OHOS 用的是一套 clang + musl + CMake 的工具链,跟 Tesseract 早就支持的 Android NDK 很接近,所以真正费劲的不是改代码,而是把依赖链一个个交叉编译过来、绕开几个交叉编译时才会冒出来的坑。
一、工程结构 / 鸿蒙工程在哪
先说清楚目录,免得找错地方。鸿蒙这部分全部在仓库根目录下的 ohos/ 里(不是 harmony_pc,也不在别处),里面又分成「引擎交叉编译脚本」和「demo 应用」两块。下面这棵树是 ls/find 实际确认过的:
ohos_Tesseract/
├── README.md # 原项目 README(Tesseract 引擎本身)
├── CMakeLists.txt # 上游 Tesseract 的 CMake 工程(源码未改)
├── src/ include/ tessdata/ # 上游引擎源码与数据,原样保留
└── ohos/ # ★ OpenHarmony 适配的全部内容都在这一级
├── README.md # 移植说明(英文,第一手资料)
├── scripts/ # 交叉编译脚本
│ ├── env.sh # 定位 OHOS NDK,设好 arch/triple/clang/sysroot
│ ├── fetch-sources.sh # 拉 zlib / libjpeg-turbo / libpng / leptonica 源码
│ ├── build-deps.sh # 交叉编译依赖链(静态、PIC)
│ ├── build-tesseract.sh # 交叉编译 libtesseract.a + 装头文件
│ └── build-all.sh # 一把梭:fetch + deps + tesseract
├── src/ # 下载下来的依赖源码(zlib / libpng / libjpeg / leptonica)
├── build/<arch>/ # 各依赖的中间构建目录
├── prebuilt/<arch>/ # 产物:include/ + lib/*.a,也就是给端侧用的 OCR SDK
│ └── arm64-v8a/lib/ # libtesseract.a / libleptonica.a / libpng16.a / libjpeg.a / libz.a
└── demo/TessOcrDemo/ # ★ DevEco Studio demo 应用(用 DevEco 打开这个目录)
├── AppScope/app.json5 # bundleName / 显示名 / 版本
├── build-profile.json5 # SDK 版本、签名、产品
├── oh-package.json5
└── entry/
├── build-profile.json5 # CMake / abiFilters
└── src/main/
├── cpp/
│ ├── CMakeLists.txt # 把静态库链成 libtessocr.so
│ ├── tesseract_napi.cpp # NAPI 桥:version() + recognize()
│ └── types/libtessocr/index.d.ts # 原生模块的 ArkTS 类型声明
├── ets/
│ ├── entryability/EntryAbility.ets
│ └── pages/Index.ets # UI:选图 → recognize() → 显示文字
├── module.json5
└── resources/rawfile/
├── sample.png # demo 示例图
└── tessdata/eng.traineddata # 英文识别模型
两个目录得分清楚:
ohos/scripts/+ohos/prebuilt/是「引擎 SDK」这一层——交叉编译产出一批.a静态库,这才是移植的主体成果。ohos/demo/TessOcrDemo/是验证用的 HAP 工程,用 DevEco Studio 打开的就是这个目录。它把上面那批.a静态链成一个libtessocr.so,再用 NAPI 暴露给 ArkTS。
二、环境要求
| 项目 | 要求 | 备注 |
|---|---|---|
| OpenHarmony Native NDK | 装好即可 | 本次移植针对 API 21 / native 6.0.1.112、clang-15 构建;脚本会从 OHOS_NATIVE_HOME / HOS_SDK_HOME 或 DevEco 安装目录里自动找 NDK |
| CMake | ≥ 3.16 | 交叉编译依赖链要用 |
| Ninja | 需在 PATH 里 |
同上 |
| DevEco Studio | 装好 HarmonyOS SDK | 跑 demo 应用、命令行 hvigorw 都要用 |
| compatibleSdkVersion | 6.0.1(21) |
见 ohos/demo/TessOcrDemo/build-profile.json5 |
| targetSdkVersion | 6.0.1(21) |
同上 |
| runtimeOS | HarmonyOS |
同上 |
| 目标架构 | arm64-v8a |
真机默认这个;模拟器走 x86_64,引擎和 demo 都要按对应 arch 各编一份 |
| 签名 | DevEco 自动生成的调试签名 | AppScope/app.json5 里的 bundleName 是 com.tesseract.ocrdemo,显示名取 app_name |
NDK 的探测顺序写在 ohos/scripts/env.sh 里:优先看环境变量 OHOS_NATIVE_HOME,再看 HOS_SDK_HOME/default/openharmony/native,最后兜底到 macOS 上 DevEco 的默认安装路径。找不到就直接报错让你设 OHOS_NATIVE_HOME,不会瞎猜。
三、编译与运行
整个流程分两段:先交叉编译引擎,再编 demo HAP。demo 的 CMake 一开头就检查 prebuilt/<arch>/lib/libtesseract.a 在不在,不在就直接 FATAL_ERROR,所以第一段是绕不过去的前置。
1. 拉代码
git clone https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Tesseract.git
cd ohos_Tesseract
2. 交叉编译引擎
cd ohos
# 默认目标是 arm64-v8a(真机)
bash scripts/build-all.sh
# 想给模拟器编就换 arch:
OHOS_ARCH=x86_64 bash scripts/build-all.sh
build-all.sh 依次干三件事:fetch-sources.sh 把 zlib / libjpeg-turbo / libpng / leptonica 的源码拉下来 → build-deps.sh 按依赖顺序交叉编译这条链(全部静态、开 PIC)→ build-tesseract.sh 编出 libtesseract.a 并把头文件装好。跑完产物落在 ohos/prebuilt/arm64-v8a/:
| 库 | 作用 |
|---|---|
libtesseract.a |
OCR 引擎本体 |
libleptonica.a |
Tesseract 用的图像 I/O |
libpng16.a / libjpeg.a / libz.a |
图像编解码 |
这些都是 aarch64-linux-ohos 的 ELF,位置无关,可以直接链进 OHOS 的原生 .so 模块。
3. 编 / 跑 demo 应用
方式 A:DevEco Studio(上手推荐)
- 用 DevEco Studio 打开
ohos/demo/TessOcrDemo/这个目录(注意不是仓库根,也不是ohos/)。 - 让它自动生成调试签名:
File → Project Structure → Signing Configs → Automatically generate。 - 接一台 arm64 真机或起对应模拟器,点 Run。
方式 B:命令行(要接 CI 或习惯命令行走这条)
cd ohos/demo/TessOcrDemo
ohpm install
hvigorw assembleHap -p product=default --no-daemon
# 产物:entry/build/default/outputs/default/entry-default-unsigned.hap
CMake/Ninja 的 native 编译、ArkTS 编译、HAP 打包全程无界面跑就行,命令行出的是 unsigned HAP;只有真要装到设备上才需要第 2 步那份签名。
demo 的默认 ABI 是
arm64-v8a(见entry/build-profile.json5的abiFilters)。要上 x86_64 模拟器,得先用OHOS_ARCH=x86_64 bash scripts/build-all.sh把引擎也编一份,再往abiFilters里加"x86_64"。
四、这个项目是如何适配的
按 ohos/README.md 的说法,引擎源码是完全没动的,适配的摩擦全在构建侧。逐条说清楚:
CMAKE_SYSTEM_NAME=OHOS,走 Tesseract 的通用 Unix 路径。 OHOS 工具链会把 UNIX 置为 TRUE,于是 Tesseract 直接用它那条 generic Unix 分支,顺便把 Android 专属的 CpuFeaturesNdkCompat 依赖跳掉——那东西在 OHOS 上既没有也用不上。
SIMD 走编译期常开,不做运行时探测。 在 __aarch64__ 上 NEON 是编译期就always-on 的,所以不需要像 Android 那样在运行时去 getauxval / android_getCpuFamily 探 CPU 特性。这块本来是 Android NDK 那套的麻烦,到 arm64 OHOS 上正好绕开了。
libpng 交叉编译要补 include 目录。 libpng 的 pnglibconf 生成器会直接调 clang,但调用时把工具链的 target 丢了,导致找不到头文件。build-deps.sh 里把每个 arch 的 musl include 目录重新塞回去才编得过。这条当时卡了一下。
Leptonica 的 TIFF 探测得手动短路。 Tesseract 配置阶段会跑一个 try_run() 去检测 Leptonica 里有没有 TIFF——交叉编译时根本不能 try_run(产物跑不在宿主机上)。所以在 build-tesseract.sh 里预先把 LEPT_TIFF_RESULT=1 喂进去,把这步跳过。
依赖链按需裁剪。 端侧识别只需要这么一条链:
zlib ──► libpng ─┐
libjpeg-turbo ───┼─► leptonica ──► libtesseract
┘
TIFF / WEBP / GIF / OpenJPEG、训练工具那一套(ICU / Pango / Cairo)、libcurl、libarchive 全部 关掉——端侧只做识别,用不到这些,砍掉之后交叉编译是自洽的,PNG + JPEG 已经覆盖常见输入。build-tesseract.sh 里还显式带了 -DDISABLE_TIFF=ON -DGRAPHICS_DISABLED=ON。要恢复 TIFF 得先单独把 libtiff 编出来,再翻 ENABLE_TIFF/DISABLE_TIFF 这组开关。
NAPI 桥(tesseract_napi.cpp)。 demo 把引擎接进 ArkTS 是靠这一个文件,对外只暴露两个东西:
version(): string—— 同步返回tesseract::TessBaseAPI::Version(),平时用来确认链进来的引擎版本(当前是 5.5.2)。recognize(imagePath, dataPath, lang): Promise<string>—— 关键的那个。OCR 可能很慢,所以它把识别丢到 libuv 的 worker 线程上跑(napi_create_async_work),跑完再 resolve/reject Promise,ArkTS 的 UI 线程一点不卡。识别失败会带着[error] ...的信息 reject 出来,方便上层定位是模型路径不对还是图读不进来。
原生模块怎么链(entry/src/main/cpp/CMakeLists.txt)。 把那批 .a 按 tesseract → leptonica → png/jpeg → z 的顺序静态链进一个 libtessocr.so——静态库的链接顺序是有讲究的,顺序错了符号会解析不了。TESSERACT_PREBUILT_DIR 默认按相对路径回溯到 ohos/prebuilt/<arch>,工程挪了位置可以用 -DTESSERACT_PREBUILT_DIR= 覆盖。
ArkTS 侧的文件落地(Index.ets)。 这里有个端侧特有的坑:Leptonica 只认真实文件系统路径,不能直接喂 rawfile 资源或 URI。所以 Index.ets 启动时先把 eng.traineddata 和 sample.png 从 rawfile 拷进应用沙箱(ctx.filesDir),再把沙箱里的绝对路径交给 recognize。
选图走系统图库选择器,且不声明相册权限。 「选择图片」用的是 photoAccessHelper.PhotoViewPicker,选完把图拷进沙箱当识别目标,「恢复示例图」切回内置 sample。这里特意没有声明相册权限——系统选择器只对用户选中的那一个文件授临时读权限就够了,比起申请整个相册权限更克制。沙箱里那个拷贝甚至不用带扩展名,因为 Leptonica 是按文件内容判格式的。
五、功能支持情况(T0 / T1 / T2)
先把口径说清楚,免得误读。Tesseract 是个引擎库,不是桌面应用,所以这三档是按「库/命令行能不能用」来分的:
- T0:引擎能不能交叉编译出来、能不能链进端侧模块、最低限度能不能识别一张图。
- T1:日常做 OCR 真正会用到的核心能力——多语言、置信度、不同输出格式这些。
- T2:上游引擎的加分项或桌面/服务端专属能力,端侧未必合适或暂时没接。
状态只有两个,含义如下:
- 能用:在鸿蒙 PC 真机上实际用过,正常。
- 未适配:桌面/服务端特有、或平台没有对应概念的能力,按平台特点主动换了做法或没做,是取舍不是没做完。
状态来源交代一句:引擎按 API 21 / native 6.0.1.112 这套 NDK 交叉编译,产物是 aarch64-linux-ohos 的 ELF;demo 的命令行能跑出 unsigned HAP,签名后已装到 arm64 真机上跑通。下面的状态都是真机实测确认过的。
T0 基础能力(编得出、链得上、能识别)
| 能力 | 状态 | 说明 |
|---|---|---|
引擎交叉编译成 libtesseract.a |
能用 | bash scripts/build-all.sh 走完,产物落在 ohos/prebuilt/arm64-v8a/lib,是 arm64 OHOS 的静态库 |
| 依赖链交叉编译(leptonica / png / jpeg / z) | 能用 | 同一条脚本一并编出,PIC、可链进 .so |
链进端侧原生模块(libtessocr.so) |
能用 | demo 的 CMake 把那批 .a 静态链成一个 NAPI 模块 |
| 编出 demo HAP | 能用 | hvigorw assembleHap 出 unsigned HAP,签名后已装到真机 |
单张图片识别(recognize) |
能用 | NAPI 桥 + worker 线程,真机实测能正常识别一张图 |
取引擎版本(version) |
能用 | 同步返回,当前链进来的是 libtesseract 5.5.2 |
小结:移植的主体——把引擎和依赖链交叉编译成 OHOS 能用的静态库、再链进一个 NAPI 模块——这条线是通的,HAP 能编出来,装到真机上端侧识别也跑通了。
T1 主要能力(做 OCR 真正会用的)
| 能力 | 状态 | 说明 |
|---|---|---|
| 英文识别(eng) | 能用 | demo 内置 eng.traineddata,recognize(..., 'eng') 真机实测可用 |
| 多语言识别 | 未适配 | 引擎本身支持 100+ 语言,lang 参数也透传了,但 demo 只随包带了 eng 模型,换语言得自己把对应 .traineddata 放进 tessdata 目录;多语言模型当前 demo 没纳入 |
| PNG / JPEG 输入 | 能用 | 依赖链就编了 png + jpeg,Leptonica 按内容判格式,这两类覆盖常见输入 |
| 从相册选图识别 | 能用 | PhotoViewPicker 选图 → 拷进沙箱 → 识别这条链在 Index.ets 里实现完整,未声明相册权限走临时授权,真机实测可用 |
| 后台线程识别、不卡 UI | 能用 | recognize 走 libuv worker 线程 + Promise,UI 线程不阻塞 |
| 识别置信度 / 版面分析 | 未适配 | 引擎 API 里有(TessBaseAPI),但 demo 的 NAPI 桥只暴露了纯文本 GetUTF8Text,置信度 / 版面这层桥接没暴露,要用得自己扩桥 |
小结:引擎层面这些能力本来就在,关键看 NAPI 桥暴露了多少。demo 现在接的「单语言、纯文本」这条最短路在真机上跑通了;多语言、置信度、版面这些要往 ArkTS 透,得自己在 tesseract_napi.cpp 里加接口。
T2 增强能力(引擎加分项 / 端侧未必合适)
| 能力 | 状态 | 说明 |
|---|---|---|
| TIFF / WEBP / GIF / OpenJPEG 输入 | 未适配 | 依赖链里主动关掉了,端侧识别用不上;要 TIFF 得先编 libtiff 再翻 ENABLE_TIFF 开关 |
| hOCR / PDF / TSV / ALTO 等输出格式 | 未适配 | 引擎支持,但桥只输出纯文本,这些 renderer 桥接没暴露 |
| 训练工具(ICU / Pango / Cairo 那套) | 未适配 | 端侧只做识别,整条训练工具链直接没编,这是按定位主动取舍 |
| libcurl / libarchive 相关能力 | 未适配 | 同上,端侧用不到,交叉编译时关掉以保持自洽 |
命令行工具 tesseract |
未适配 | 移植只产出引擎静态库 + NAPI 模块,没有去搭命令行可执行那一套 |
| Legacy OCR 引擎(--oem 0) | 未适配 | 上游代码里有,但端侧默认走 LSTM,legacy 模式桥接没暴露 |
小结:T2 里 TIFF/WEBP 那些冷门格式、训练工具、命令行,都是顺着「端侧识别引擎」这个定位主动砍掉的,不是适配没做出来。多种输出格式、Legacy 引擎是引擎有能力、demo 的桥没往上接,需要时扩桥即可,归在未适配。
六、整体到什么程度
把三档合起来看:
- T0 基础能力:引擎和依赖链交叉编译、链进端侧 NAPI 模块、编出 HAP、装到真机识别一张图,这条主线全部跑通,到位。
- T1 主要能力:「单语言、纯文本、后台线程、相册选图」这条端侧 OCR 的最短闭环在 demo 里实现完整,真机实测可用;多语言只差放模型,置信度/版面这些要扩 NAPI 桥。核心闭环跑通了,外延看接口暴露多少。
- T2 增强能力:冷门格式、训练工具、命令行这些按平台定位主动取舍掉了,剩下的输出格式 / Legacy 引擎是「引擎有、桥没接」。这一档的低覆盖是取舍,不是缺陷。
一句话总结:这次移植做的是把 Tesseract 引擎及其依赖链交叉编译成 OpenHarmony 可用的静态库,再用一个 NAPI demo 把端侧 OCR 这条路走通——选图、后台识别、出文字的链路已在 arm64 真机上实测可用。后续按需把多语言、更多输出格式往 ArkTS 侧再透一层即可。