开源鸿蒙平台 KMP_CMP 三方库「端侧OCR」适配全流程
本文记录
kmp-ohos-ocr接入 OpenHarmony 端侧文字识别(OCR)能力的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、Core Vision Kit 图片识别、签名 HAP 和真机验收。本次适配复用 Kotlin 侧的识别结果模型、确定性引擎、JSON 边界和自检逻辑,再由 ArkTS 调用 HarmonyOS
@kit.CoreVisionKit的textRecognition.recognizeText对真实图片执行端侧识别。这样验证的是同一份 KMP 代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新写一套只在页面里生效的数据结构。
项目地址: AtomGit/lqjmac/kmp-ohos-ocr
开发工具: 华为云码道
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
端侧 OCR 是 HarmonyOS 基础视觉服务(Core Vision Kit)的一部分。应用把图片转成 PixelMap 后,系统会在设备本地完成文字检测与识别,返回段落(TextBlock)、行(TextLine)和单词(TextWord)三级结构。业务层不需要上传图片,也不需要自行实现检测算法。
如果只把页面重新写成 ArkTS,页面可能会显示几行识别文本,却无法证明共享 Kotlin 模型、Native 动态库和系统识别结果已经连通。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认只有 JVM,必须增加 ohosArm64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | JDK 25 会让 Kotlin 2.2.21 的 JavaVersion.parse 直接抛异常,必须固定 JDK 21。 |
| 媒体库 URI 边界 | 图库返回的 file://media/Photo/... URI 不能直接创建 ImageSource,必须先取文件描述符。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须经过 C ABI、C++ N-API 和 JSON。 |
| 原生对象生命周期 | PixelMap/ImageSource 不能声明为 @State,状态代理会破坏原生方法。 |
| 权限要求 | 宿主需要声明 READ_IMAGEVIDEO 使用场景,首次选图还要用户授权确认。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和 ArkUI 识别页面。结果模型仍由 KMP 维护,ArkTS 只负责图片选择、PixelMap 生命周期和系统识别调用。
1.2 库提供的能力
ocr 公共模块提供以下能力:
OcrLanguage:中、英、日、韩四种语言枚举;OcrBlock:携带文本与 0.0…1.0 置信度的识别块,构造时完整校验;OcrResult:一次识别的不可变结果,派生fullText与averageConfidence;OcrEngine.filter:按置信度阈值过滤识别块;OcrEngine.reduce:接收识别动作并推进刷新版本;OcrEngine.runChecks:在 JVM、Kotlin/Native 和 ArkTS 示例中复用同一组检查;OcrResult.toJson:生成稳定的跨语言 JSON;String.jsonQuote:覆盖引号、反斜杠与全部控制字符的 JSON 转义。
演示场景目录如下:
| KMP 场景 | 内容 | 语言 |
|---|---|---|
demo-receipt | 门店收银小票,4 个识别块 | 简体中文 |
demo-invoice | 增值税发票,4 个识别块 | 简体中文 |
demo-card | 名片,3 个识别块 | 英文 |
每个场景的置信度分布在 0.88…0.99 之间,filter(0.9) 能稳定演示低置信度块被剔除,JVM 测试与真机页面使用同一套规则。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 结果模型、置信度过滤、JSON 和自检由 Kotlin 共享。 |
| 平台目标 | 为公共模块和示例加入 ohosArm64,生成 libohos_ocr.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持选图、预览、端侧识别、逐行结果和完整文本复制。 |
| 可测试 | JVM 测试、Native 链接、HAP 构建、设备安装和系统识别分别验收。 |
| 签名安全 | 源码只保留签名配置入口,证书和密钥材料由开发者本机配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以复用
OcrResult和OcrEngine,再自行决定如何消费 Core Vision Kit 的识别结果。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 模块、识别模型和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:结果与序列化 ── 建立 OcrResult、OcrBlock、Engine 和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:系统能力封装 ── ArkTS 选图、PixelMap 转换、Core Vision Kit 识别和权限
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、真机识别和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享模型,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 对真实图片执行端侧识别。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 CMP 工程一致的分层:
ocr/ KMP 识别模型、引擎、自检和 JSON 边界
example/shared/ 示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
docs/openharmony/ 验收记录和真机效果图
example 是独立 Gradle 工程,通过 include("ocr") 与 projectDir = file("../ocr") 引用公共模块,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后在 DevEco Studio 中配置签名。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | 工程兼容和目标版本 |
| ABI | arm64-v8a | HAP 原生库架构 |
| 识别能力 | textRecognition | Core Vision Kit 端侧 OCR |
执行 Gradle 脚本前先选择 JDK 21。本机默认 JAVA_HOME 指向 DevEco 自带的 JBR 25,直接构建会失败:
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
java -version
JDK 25 会让 Kotlin 2.2.21 在 JavaVersion.parse 处抛出 IllegalArgumentException: 25.0.2,这是本次适配遇到的第一个环境坑。
1.3 创建 OpenHarmony 示例目录
示例页面没有把系统能力伪装成普通列表,而是围绕“选图、预览、识别和结果展示”组织:
标题区 端侧OCR / 选择图片 · Core Vision Kit 端侧文字识别
操作区 选择图片 / 开始识别
预览区 所选图片缩略图
结果区 逐行识别文本卡片 + 完整文本(可复制)
状态区 图片已就绪 / 识别完成,共 N 行 · 耗时 Xms
「选择图片」只拉起系统图库,不访问识别能力;只有点击「开始识别」时,ArkTS 才调用 textRecognition.recognizeText。这样不支持该能力的设备也能完成选图链路验证。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
公共模块同时保留 JVM 测试和 OpenHarmony Native 目标:
plugins {
kotlin("multiplatform")
`maven-publish`
}
kotlin {
explicitApi()
jvm()
jvmToolchain(21)
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
JVM 目标让识别模型和过滤规则可以快速测试;ohosArm64 则把同一份 commonMain 代码编译成 OpenHarmony KLIB。
2.2 Native focused build 的作用
example/nativeApp 构建一个受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "ohos_ocr"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接器只导出四个 C ABI 符号:
OcrResultGet
OcrResultFilter
OcrRunChecks
OcrFree
这样 ArkTS 只能通过明确的边界获取识别结果和自检结果,Kotlin/Native 内部实现不会变成不受控的 ABI。注意 version script 必须包含 { global: ... local: *; }; 完整结构,缺少花括号会让 ld.lld 直接报错。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
共享示例通过项目依赖消费本地 ocr:
sourceSets {
commonMain.dependencies {
api(project(":ocr"))
}
}
如果使用已经发布的 Maven 产物,业务 KMP 模块可以写成:
commonMain.dependencies {
implementation("com.ohos.ocr:ocr:1.0.0")
}
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libohos_ocr.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libohos_ocr_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("ohosApp/entry"))
}
动态库、生成头文件和 HAP 属于构建产物,仓库通过 .gitignore 排除它们;每台开发机都可以从源码重新生成与自身 SDK 匹配的文件。
第 3 阶段:结果与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
Core Vision Kit recognizeText
↓ TextBlock[] / TextLine[]
ArkTS Index.ets
↓ index + revision
N-API getOcrResult()
↓ C ABI OcrResultGet()
Kotlin OcrEngine
↓ JSON result
ArkUI page
页面只消费 JSON,不复制结果结构。这样 JVM 测试和设备页面使用同一套 OcrResult 规则。
3.2 OcrBlock 和 OcrResult
公共结果模型位于 ocr/src/commonMain:
public data class OcrBlock(
val text: String,
val confidence: Double,
) {
init {
require(text.isNotBlank()) { "Block text must not be blank" }
require(confidence in 0.0..1.0) { "Confidence must be between 0.0 and 1.0" }
}
}
public data class OcrResult(
val id: String,
val source: String,
val language: OcrLanguage,
val blocks: List<OcrBlock>,
val durationMs: Long,
) {
public val fullText: String get() = blocks.joinToString(separator = "\n") { it.text }
public val averageConfidence: Double
get() = if (blocks.isEmpty()) 0.0 else blocks.sumOf { it.confidence } / blocks.size
}
fullText 和 averageConfidence 是派生属性,保证多份展示口径一致。构造时的 require 让非法数据在边界处立即失败,而不是在页面上显示成奇怪的结果。
3.3 Engine filter、reduce 和 JSON
public object OcrEngine {
public fun filter(model: OcrResult, minConfidence: Double): OcrResult {
require(minConfidence in 0.0..1.0) { "Confidence threshold must be between 0.0 and 1.0" }
return model.copy(blocks = model.blocks.filter { it.confidence >= minConfidence })
}
public fun reduce(model: OcrResult, actionId: String, revision: Int): OcrResult {
require(revision >= 0) { "Revision must be non-negative" }
if (actionId.isEmpty()) return model
return when (actionId) {
"recognize" -> result(catalog().indexOfFirst { it.id == model.id }, revision + 1)
"filter" -> filter(model, 0.9)
else -> requireNotNull(null) { "Action not supported by result" }
}
}
}
JSON 边界保持字段稳定:
{
"id": "demo-receipt",
"source": "收银小票 · 演示图",
"language": "CHINESE",
"fullText": "门店收银小票\n美式咖啡 x1 ¥28.00",
"averageConfidence": 0.9375,
"durationMs": 120,
"blocks": [
{ "text": "门店收银小票", "confidence": 0.98 }
]
}
所有字符串经过 jsonQuote 处理,覆盖引号、反斜杠、换行和全部控制字符,避免识别文本破坏 JSON。
3.4 自检和错误边界
OcrEngine.runChecks() 覆盖六项检查:
- 三个识别场景都存在;
- 每个场景都有识别块;
- 置信度值在 0.0…1.0 之间;
- 结果标识唯一;
- 过滤会剔除低置信度块;
- 识别动作会产生新状态。
Native 异常会转换为 {"error":"..."},C++ 在创建 ArkTS 字符串后立即调用 OcrFree。这样页面能显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 OcrResult 和 OcrBlock 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ OcrEngine
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出整数 | 实现简单 | 页面会重新实现结果结构 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整模型 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
@CName("OcrResultGet")
public fun resultNative(index: Int, revision: Int): CPointer<ByteVar> = response {
OcrExamples.result(index, revision).toJson()
}
@CName("OcrResultFilter")
public fun filterNative(index: Int, minConfidence: Double): CPointer<ByteVar> = response {
OcrExamples.filter(index, minConfidence).toJson()
}
@CName("OcrRunChecks")
public fun checksNative(): CPointer<ByteVar> = response { ... }
@CName("OcrFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 OcrFree 释放。
4.4 C++ N-API 方法分发
C++ 注册三个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getOcrResult", nullptr, Result, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getFilteredResult", nullptr, Filter, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getOcrResult 会检查参数数量、数值类型和非负范围,再调用 OcrResultGet。快照、过滤和自检都遵循“调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区”的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(ohos_ocr SHARED IMPORTED)
set_target_properties(ohos_ocr PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libohos_ocr.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE ohos_ocr libace_napi.z.so)
4.5 N-API 生命周期
ArkTS getOcrResult(index, revision)
│
▼
ReadNumber + argument check
│
▼
OcrResultGet(index, revision)
│
▼
napi_create_string_utf8(...)
│
▼
OcrFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:系统能力封装
5.1 ArkTS 调用 Core Vision Kit
图片选择与识别封装在 Index.ets:
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { textRecognition } from '@kit.CoreVisionKit';
private async chooseImage(): Promise<void> {
const picker = new photoAccessHelper.PhotoViewPicker();
const result = await picker.select({
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 1,
});
this.imageUri = result.photoUris[0];
const source = image.createImageSource(this.imageUri);
if (source) {
this.imageSource = source;
} else {
// Media-library URIs may not be accepted directly; open a fd instead.
const file = await fileIo.open(this.imageUri, fileIo.OpenMode.READ_ONLY);
this.imageSource = image.createImageSource(file.fd);
}
this.pixelMap = await this.imageSource!.createPixelMap();
}
private async recognize(): Promise<void> {
const visionInfo: textRecognition.VisionInfo = { pixelMap: this.pixelMap };
const output = await textRecognition.recognizeText(visionInfo);
for (const block of output.blocks ?? []) {
for (const line of (block.lines ?? [])) {
lines.push({ text: line.value, confidence: 0 });
}
}
}
图库返回的 file://media/Photo/... URI 直接创建 ImageSource 会得到 undefined,必须通过 fileIo.open 取文件描述符后再创建,这是本次适配最重要的一个坑。
5.2 权限声明
宿主 entry/src/main/module.json5 声明:
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:photo_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
权限只声明真实使用场景。首次选图时系统还会弹出媒体库授权确认,需要用户同意。
5.3 原生对象生命周期
PixelMap 和 ImageSource 是原生对象,不能声明为 ArkTS @State——状态代理会让 createPixelMap 等原生方法变成 undefined,页面直接报 TypeError。本项目把它们放在普通成员变量里,只让 imageUri、识别结果和状态文本参与状态渲染:
@State private imageUri: string = '';
private pixelMap: image.PixelMap | null = null;
private imageSource: image.ImageSource | null = null;
aboutToDisappear 里统一调用 release(),重复选图时也先释放上一张,避免原生内存泄漏。
5.4 结果解析边界
SDK 中 TextLine 只有 value、cornerPoints 和 words 三个字段:文本字段叫 value 而不是部分文档示例写的 text,行级也没有置信度。解析代码按 SDK 实际类型定义(textRecognition.TextBlock/TextLine)编写,不引入 any/unknown(ArkTS 禁止),置信度列在有值时才渲染。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../OcrExamples.kt
│ └── src/commonTest/.../OcrExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── ocr/OcrClient.ets
shared 验证公共模型,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面和系统 API。三者的边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import { getOcrResult, getFilteredResult, getChecks } from 'libentry.so';
注意工程根目录的 hvigorfile.ts 必须导出 appTasks(application 级),只有 entry 模块使用 hapTasks。两者写反会直接报 no system plugins were found in hvigorfile。
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
(cd example && ./gradlew :ocr:jvmTest)
(cd example && ./gradlew :nativeApp:prepareOhos)
成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libohos_ocr.so
example/ohosApp/entry/src/main/cpp/include/libohos_ocr_api.h
生成的头文件包含 OcrResultGet、OcrResultFilter、OcrRunChecks、OcrFree 四个符号,与 napi_init.cpp 的引用一一对应。
6.4 构建、签名和安装
未配置签名时,Hvigor 会生成未签名 HAP;真机安装需要签名 HAP。在 DevEco Studio 中配置自动签名后执行:
cd example/ohosApp
ohpm install --all
hvigorw --mode module -p module=entry@default -p product=default \
-p buildMode=debug assembleHap --no-daemon
产物位于:
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
hdc list targets -v
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.ocr.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
HarmonyOS 图库
│ PhotoViewPicker
▼
Index.ets
│ fileIo.open(fd) -> createPixelMap
▼
Core Vision Kit
│ textRecognition.recognizeText(VisionInfo)
▼
TextBlock[] / TextLine[](value 字段为行文本)
│ ArkTS 解析为逐行文本
▼
ArkUI 列表渲染 + 完整文本(可复制)
(演示引擎与自检链路)
Index.ets -> libentry.so
│ N-API
▼
OcrResultGet / OcrResultFilter / OcrRunChecks
│ C ABI
▼
libohos_ocr.so
│ Kotlin/Native
▼
OcrEngine -> OcrResult -> JSON
4.2 文件清单
| 文件 | 职责 |
|---|---|
ocr/OcrResult.kt | 语言枚举、识别块和结果模型 |
ocr/OcrEngine.kt | 场景目录、置信度过滤、动作归约和自检 |
ocr/OcrJson.kt | JSON 编码和字符串转义 |
example/nativeApp/NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/ohosApp/.../Index.ets | 选图、端侧识别和真机预览页面 |
example/ohosApp/.../OcrClient.ets | N-API 客户端封装 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 导出和参数检查 |
example/ohosApp/entry/src/main/module.json5 | READ_IMAGEVIDEO 权限声明 |
docs/openharmony/ | 验收记录和真机效果图 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | OcrEngine.filter | 按置信度过滤识别块 |
| Kotlin | OcrEngine.reduce | 接收动作并推进刷新版本 |
| Kotlin | OcrResult.toJson | 跨语言 JSON 契约 |
| ArkTS | photoAccessHelper.PhotoViewPicker | 拉起系统图库 |
| ArkTS | textRecognition.recognizeText | 系统端侧文字识别 |
| Native | OcrResultGet / OcrResultFilter | 返回 JSON 结果 |
| N-API | getOcrResult / getFilteredResult | 向 ArkTS 暴露方法 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 负责系统能力和界面生命周期:
const visionInfo: textRecognition.VisionInfo = { pixelMap: this.pixelMap };
const output = await textRecognition.recognizeText(visionInfo);
Kotlin 负责结果结构和业务规则:
public fun filter(model: OcrResult, minConfidence: Double): OcrResult
两者之间只传输整数、浮点数和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
端侧识别发生在 ARM64 设备上,只有真正链接 ohosArm64 动态库,才能证明共享 Kotlin 代码可以进入 OpenHarmony 运行时。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 ocr,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而不暴露内部对象布局。
决策 4:桥接层只开放四个 C ABI 入口
结果、过滤、自检和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期和兼容风险。
决策 5:选图链路和识别链路分开
选图只验证选择器、URI 和 PixelMap 转换;识别只验证 Core Vision Kit 能力。两者分开后,不支持该能力的设备仍然可以完成选图链路验收。
决策 6:把库验证和设备验证分开
JVM 测试验证结果规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证 recognizeText 和端侧回调。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
JAVA_HOME=/path/to/jdk21 ./gradlew :ocr:jvmTest
(cd example && JAVA_HOME=/path/to/jdk21 ../gradlew :shared:jvmTest)
测试覆盖模型校验(空文本、越界置信度被拒绝)、过滤语义、动作归约、JSON 转义和六项公共自检。
6.3 原生桥接和 HAP 验证
(cd example && JAVA_HOME=/path/to/jdk21 ../gradlew :nativeApp:prepareOhos)
cd example/ohosApp && hvigorw --mode module -p module=entry@default \
-p product=default -p buildMode=debug assembleHap --no-daemon
验证结果:
libohos_ocr.so 链接成功,导出符号与 napi_init.cpp 一致
Hvigor BUILD SUCCESSFUL(33 tasks)
entry-default-signed.hap generated
6.4 功能验证用例
用例 1:选图链路
点击「选择图片」,系统图库拉起,选定一张照片并确认后,页面显示预览且状态变为「图片已就绪」。日志确认走的是 fileIo.open → createImageSource(fd) 的降级路径。
用例 2:端侧识别
点击「开始识别」,状态更新为「识别完成,共 N 行 · 耗时 Xms」。本次真机识别一张含「智感握姿」示例页面的截图,返回 16 行文本,耗时 242ms。
用例 3:逐行结果展示
每行文本以独立卡片渲染,底部完整文本支持长按复制(CopyOptions.InApp)。
用例 4:演示引擎桥接
示例页通过 OcrResultGet/OcrResultFilter 验证 KMP 引擎:三个场景切换、重新识别推进版本、filter(0.9) 剔除低置信度块,JVM 测试与真机行为一致。
用例 5:异常与空结果
未选图直接点识别时提示「请先选择图片」;识别失败时显示错误码与信息;模糊或无文字图片返回空结果时提示「未识别到文字」。
6.5 验证结论
自动测试、Kotlin/Native ARM64 链接、Hvigor HAP 构建、签名安装和真机端侧识别均已完成。真机已经对真实图片返回识别文本,说明从系统识别到结果展示的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为「端侧OCR」;
- 副标题说明 Core Vision Kit 端侧文字识别;
- 「选择图片」「开始识别」两个操作入口;
- 状态区显示「识别完成,共 16 行 · 耗时 242ms」;
- 逐行识别结果以卡片展示(时间、标题、副标题、状态码等文本均被正确识别);
- 全程离线,未使用任何网络识别服务。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./gradlew :ocr:jvmTest
(cd example && ../gradlew :nativeApp:prepareOhos)
# 在已配置签名的工程中构建 HAP
cd example/ohosApp
ohpm install --all
hvigorw --mode module -p module=entry@default -p product=default \
-p buildMode=debug assembleHap --no-daemon
# 安装和启动
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.ocr.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- JDK 25 不能用于 Kotlin 2.2.21:
JavaVersion.parse抛IllegalArgumentException: 25.0.2,必须固定 JDK 21。 - 媒体库 URI 不能直接创建 ImageSource:
file://media/Photo/...直接传入返回undefined,必须fileIo.open取 fd。 - 原生对象不能放 @State:状态代理会让
createPixelMap变成undefined,页面报TypeError。 - SDK 字段名以 .d.ts 为准:
TextLine的文本字段是value不是text,行级没有置信度。 - hvigorfile 必须区分层级:根工程用
appTasks,entry 模块用hapTasks,写反直接构建失败。 - version script 要完整:缺少
{ }花括号结构会让ld.lld链接失败。
8.2 已知问题
- Core Vision Kit 行级结果不返回置信度,本库的置信度过滤主要面向演示引擎或其他带置信度的识别源;
- 识别精度受图片质量影响,模糊、艺术字、强反光图片可能返回空结果;
- 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境;
- 当前示例是单页面识别模型,批量图片识别需要在业务层管理 PixelMap 生命周期。
8.3 未来优化方向
- 把 Core Vision Kit 的
TextBlock[]直接映射为 KMPOcrResult,让置信度过滤、平均值统计等库能力作用于真实识别结果; - 增加相机实时识别入口;
- 将识别流程封装为
Flow<OcrResult>,减少业务层回调管理; - 增加语言枚举与系统
getSupportedLanguages的映射; - 在持续集成中加入 Native 链接和 HAP 未签名构建任务。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 recognizeText 调用,而是一条完整跨端链路:
OpenHarmony 图库
→ ArkTS 选图与 PixelMap 生命周期
→ Core Vision Kit 端侧识别
→ ArkUI 结果渲染
→ N-API / C ABI
→ KMP 结果模型与置信度过滤
→ JSON
→ JVM / Native / 真机共用同一套规则
9.2 封装层次
- KMP 层:定义稳定的结果结构、过滤、归约和 JSON;
- Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理选图、PixelMap 生命周期、系统识别和视觉展示;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 系统能力的字段名和返回值以 SDK
.d.ts为准,文档示例可能有出入。
9.4 适配成果
当前 kmp-ohos-ocr 已完成:
OcrResult/OcrBlock/OcrLanguage公共结果模型;OcrEngine确定性引擎:场景目录、置信度过滤、动作归约和六项自检;- JVM 和 OpenHarmony ARM64 共用的自检逻辑;
- Kotlin/Native + C ABI + N-API 桥接(四个导出符号);
- Core Vision Kit 真机端侧识别:选图、PixelMap 转换、逐行结果展示;
READ_IMAGEVIDEO权限声明与媒体库 URI 降级路径;- 签名 HAP 构建、设备安装和真机识别效果图;
- 与参考 CMP 工程一致的模块组织;
- AtomGit 项目文档和 OpenHarmony 验收记录。
参考文档
更多推荐


所有评论(0)