本文记录 kmp-ohos-ocr 接入 OpenHarmony 端侧文字识别(OCR)能力的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、Core Vision Kit 图片识别、签名 HAP 和真机验收。

本次适配复用 Kotlin 侧的识别结果模型、确定性引擎、JSON 边界和自检逻辑,再由 ArkTS 调用 HarmonyOS @kit.CoreVisionKittextRecognition.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:一次识别的不可变结果,派生 fullTextaverageConfidence
  • 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 应用可以复用 OcrResultOcrEngine,再自行决定如何消费 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 Multiplatform2.2.21-1.0.0JVM、Kotlin/Native 和 KLIB
JDK21Gradle、Kotlin 编译和 Native 任务
OpenHarmony 目标ohosArm64ARM64 真机动态库
DevEco product6.0.0(20)工程兼容和目标版本
ABIarm64-v8aHAP 原生库架构
识别能力textRecognitionCore 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
}

fullTextaverageConfidence 是派生属性,保证多份展示口径一致。构造时的 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() 覆盖六项检查:

  1. 三个识别场景都存在;
  2. 每个场景都有识别块;
  3. 置信度值在 0.0…1.0 之间;
  4. 结果标识唯一;
  5. 过滤会剔除低置信度块;
  6. 识别动作会产生新状态。

Native 异常会转换为 {"error":"..."},C++ 在创建 ArkTS 字符串后立即调用 OcrFree。这样页面能显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。

第 4 阶段:原生桥接(技术难点)

4.1 Kotlin/Native 对象不能直接交给 ArkTS

Kotlin/Native 的 OcrResultOcrBlock 属于 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 原生对象生命周期

PixelMapImageSource 是原生对象,不能声明为 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 只有 valuecornerPointswords 三个字段:文本字段叫 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

生成的头文件包含 OcrResultGetOcrResultFilterOcrRunChecksOcrFree 四个符号,与 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.ktJSON 编码和字符串转义
example/nativeApp/NativeBridge.ktC ABI、JSON 返回和内存释放
example/ohosApp/.../Index.ets选图、端侧识别和真机预览页面
example/ohosApp/.../OcrClient.etsN-API 客户端封装
example/ohosApp/entry/src/main/cpp/napi_init.cppN-API 导出和参数检查
example/ohosApp/entry/src/main/module.json5READ_IMAGEVIDEO 权限声明
docs/openharmony/验收记录和真机效果图

4.3 关键 API 对照

层次API作用
KotlinOcrEngine.filter按置信度过滤识别块
KotlinOcrEngine.reduce接收动作并推进刷新版本
KotlinOcrResult.toJson跨语言 JSON 契约
ArkTSphotoAccessHelper.PhotoViewPicker拉起系统图库
ArkTStextRecognition.recognizeText系统端侧文字识别
NativeOcrResultGet / OcrResultFilter返回 JSON 结果
N-APIgetOcrResult / 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 单独解析 ocrohosApp 单独接收 .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.opencreateImageSource(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 踩坑复盘

  1. JDK 25 不能用于 Kotlin 2.2.21JavaVersion.parseIllegalArgumentException: 25.0.2,必须固定 JDK 21。
  2. 媒体库 URI 不能直接创建 ImageSourcefile://media/Photo/... 直接传入返回 undefined,必须 fileIo.open 取 fd。
  3. 原生对象不能放 @State:状态代理会让 createPixelMap 变成 undefined,页面报 TypeError
  4. SDK 字段名以 .d.ts 为准TextLine 的文本字段是 value 不是 text,行级没有置信度。
  5. hvigorfile 必须区分层级:根工程用 appTasks,entry 模块用 hapTasks,写反直接构建失败。
  6. version script 要完整:缺少 { } 花括号结构会让 ld.lld 链接失败。

8.2 已知问题

  • Core Vision Kit 行级结果不返回置信度,本库的置信度过滤主要面向演示引擎或其他带置信度的识别源;
  • 识别精度受图片质量影响,模糊、艺术字、强反光图片可能返回空结果;
  • 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境;
  • 当前示例是单页面识别模型,批量图片识别需要在业务层管理 PixelMap 生命周期。

8.3 未来优化方向

  • 把 Core Vision Kit 的 TextBlock[] 直接映射为 KMP OcrResult,让置信度过滤、平均值统计等库能力作用于真实识别结果;
  • 增加相机实时识别入口;
  • 将识别流程封装为 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 三条经验

  1. 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递;
  3. 系统能力的字段名和返回值以 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 验收记录。

参考文档

Logo

一站式 AI 云服务平台

更多推荐