前言
大家好!我是全栈开发者Jack,最近在开发一款名为 BabyOne 的母婴育儿应用。在开发过程中,遇到了一个有趣的需求:用户需要快速识别疫苗本、体检报告等纸质文档上的文字信息。为了提升用户体验,我决定集成 OCR(光学字符识别)功能。
经过调研,我发现鸿蒙系统提供了强大的 Core Vision Kit,其中包含了高性能的文字识别能力。于是,我封装了 jack-ocr 插件,让 UniApp 开发者可以轻松调用鸿蒙原生 OCR 能力。
本文将详细介绍如何在 UniApp 项目中使用鸿蒙原生 OCR 功能,实现高效的文字识别。
什么是 OCR?
OCR(Optical Character Recognition,光学字符识别)是一种将图像中的文字转换为可编辑文本的技术,广泛应用于:
- 📄 证件识别:身份证、驾驶证、护照等
- 🏥 医疗场景:病历、处方、体检报告识别
- 📚 教育场景:试卷扫描、作业批改
- 💼 办公场景:名片识别、文档数字化
- 🍼 母婴场景:疫苗本记录、成长档案录入
jack-ocr 插件介绍
为了解决 UniApp 项目中使用鸿蒙原生 OCR 的痛点,我封装了 jack-ocr 插件。这是一个基于 UTS 开发的 OCR 插件,完全开源。
平台支持
- ✅ HarmonyOS:使用
@kit.CoreVisionKit原生 API
核心特性
- 统一的 API 接口:简洁易用的调用方式
- 多语言支持:支持简体中文、英文、日文、韩文、繁体中文
- 朝向检测:自动检测文本方向
- 完善的错误处理:提供详细的错误码和错误信息
- 生命周期管理:支持初始化、识别、资源释放
- 开箱即用:本文提供完整源码,无需下载,直接复制即可使用
插件市场地址
插件已发布到 UniApp 插件市场,可以直接搜索安装:
🔗 插件市场地址:ext.dcloud.net.cn/plugin?name…
为什么要封装这个插件?
在开发 BabyOne 应用时,我需要实现以下功能:
- 疫苗本识别:快速录入疫苗接种记录
- 体检报告识别:自动提取身高、体重等数据
- 成长档案:识别纸质照片上的文字信息
直接使用鸿蒙原生 API 存在以下问题:
- 代码复杂:需要处理图片加载、PixelMap 创建等底层细节
- 平台差异:鸿蒙 API 无法在 UniApp 中直接使用
- 错误处理繁琐:需要手动处理各种异常情况
- 资源管理困难:需要手动释放 PixelMap 等资源
因此,我将功能封装成了 jack-ocr 插件,大大简化了使用流程。
技术架构
系统架构图
graph TB
subgraph "UniApp 应用层"
A["Vue 页面"] --> B["jack-ocr 插件"]
end
subgraph "UTS 插件层"
B --> C["interface.uts
接口定义"]
B --> D["unierror.uts
错误处理"]
B --> E["app-harmony/index.uts
鸿蒙实现"]
end
subgraph "鸿蒙系统层"
E --> F["@kit.CoreVisionKit
视觉识别"]
E --> G["@kit.ImageKit
图像处理"]
E --> H["@kit.CoreFileKit
文件操作"]
end
F --> I["textRecognition API"]
G --> J["PixelMap 处理"]
H --> K["文件读取"]
style A fill:#e1f5ff
style B fill:#fff4e1
style E fill:#ffe1f5
style F fill:#e1ffe1
style G fill:#e1ffe1
style H fill:#e1ffe1
插件目录结构
uni_modules/jack-ocr/
├── utssdk/
│ ├── interface.uts # 接口定义
│ ├── unierror.uts # 错误处理
│ └── app-harmony/ # 鸿蒙平台实现
│ ├── index.uts # 核心实现
│ └── types.uts # 类型定义
└── package.json
核心 API 设计
插件提供了 3 个核心 API:
| API | 说明 | 返回值 |
|---|---|---|
ocrInit(options) | 初始化 OCR 服务 | void |
ocrRecognize(options) | 识别图片中的文字 | void |
ocrRelease(options) | 释放 OCR 服务资源 | void |
OCR 识别流程图
sequenceDiagram
participant U as UniApp 页面
participant P as jack-ocr 插件
participant V as CoreVisionKit
participant I as ImageKit
participant F as FileKit
U->>P: 1. ocrInit() 初始化
P->>V: textRecognition.init()
V-->>P: 初始化成功
P-->>U: success 回调
U->>P: 2. ocrRecognize(imagePath)
P->>F: fileIo.open() 打开文件
F-->>P: 返回文件描述符
P->>I: createImageSource(fd)
I-->>P: 返回 ImageSource
P->>I: createPixelMap()
I-->>P: 返回 PixelMap
P->>V: recognizeText(pixelMap)
V-->>P: 返回识别结果
P->>I: pixelMap.release()
P-->>U: success 回调(text, blocks)
U->>P: 3. ocrRelease() 释放资源
P->>V: textRecognition.release()
V-->>P: 释放成功
P-->>U: success 回调
插件完整源码
为了方便大家使用,这里提供 jack-ocr 插件的完整源代码,可以直接复制到你的项目中使用。
1. utssdk/interface.uts(接口定义)
/**
* OCR 文字识别接口定义
*/
/**
* OCR 初始化参数
*/
export type OCRInitOptions = {
success ?: (res : OCRInitResult) => void
fail ?: (res : OCRFail) => void
complete ?: (res : any) => void
}
/**
* OCR 识别参数
*/
export type OCRRecognizeOptions = {
/** 图片路径(支持本地路径、相册URI) */
imagePath : string
/** 是否支持朝向检测 */
isDirectionDetectionSupported ?: boolean
success ?: (res : OCRRecognizeResult) => void
fail ?: (res : OCRFail) => void
complete ?: (res : any) => void
}
/**
* OCR 释放参数
*/
export type OCRReleaseOptions = {
success ?: (res : OCRReleaseResult) => void
fail ?: (res : OCRFail) => void
complete ?: (res : any) => void
}
/**
* 初始化结果
*/
export type OCRInitResult = {
success : boolean
message : string
}
/**
* 识别结果
*/
export type OCRRecognizeResult = {
success : boolean
message : string
/** 识别的文本内容 */
text : string
/** 文本块数组 */
blocks ?: Array<OCRTextBlock>
}
/**
* 文本块
*/
export type OCRTextBlock = {
/** 文本内容 */
text : string
/** 置信度 0-1 */
confidence : number
/** 文本框坐标(可选) */
bounds : OCRBounds | null
}
/**
* 文本框坐标
*/
export type OCRBounds = {
left : number
top : number
right : number
bottom : number
}
/**
* 释放结果
*/
export type OCRReleaseResult = {
success : boolean
message : string
}
/**
* 错误码
* - 9030001 初始化失败
* - 9030002 识别失败
* - 9030003 释放失败
* - 9030004 未初始化
* - 9030005 图片加载失败
*/
export type OCRErrorCode = 9030001 | 9030002 | 9030003 | 9030004 | 9030005;
/**
* OCR 错误回调参数
*/
export interface OCRFail extends IUniError {
errCode : OCRErrorCode
}
/* OCR 函数定义 */
export type OCRInit = (options : OCRInitOptions) => void
export type OCRRecognize = (options : OCRRecognizeOptions) => void
export type OCRRelease = (options : OCRReleaseOptions) => void
2. utssdk/unierror.uts(错误处理)
import { OCRFail, OCRErrorCode } from './interface.uts';
/**
* OCR 错误实现类
*/
export class OCRFailImpl extends UniError implements OCRFail {
override errCode : OCRErrorCode
constructor(errCode : OCRErrorCode) {
super()
this.errSubject = 'jack-ocr'
this.errCode = errCode
this.errMsg = this.getErrMsg(errCode)
}
private getErrMsg(errCode : OCRErrorCode) : string {
switch (errCode) {
case 9030001:
return 'OCR 初始化失败'
case 9030002:
return 'OCR 识别失败'
case 9030003:
return 'OCR 释放失败'
case 9030004:
return 'OCR 未初始化'
case 9030005:
return '图片加载失败'
default:
return '未知错误'
}
}
}
3. utssdk/app-harmony/index.uts(鸿蒙平台实现)
import {
OCRInit,
OCRRecognize,
OCRRelease,
OCRInitOptions,
OCRRecognizeOptions,
OCRReleaseOptions,
OCRInitResult,
OCRRecognizeResult,
OCRReleaseResult,
OCRFail,
OCRTextBlock
} from '../interface.uts';
import { OCRFailImpl } from '../unierror';
import { textRecognition } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
export {
OCRInit,
OCRRecognize,
OCRRelease,
OCRInitOptions,
OCRRecognizeOptions,
OCRReleaseOptions,
OCRInitResult,
OCRRecognizeResult,
OCRReleaseResult,
OCRFail,
OCRTextBlock
}
/**
* OCR 服务初始化状态
*/
let isInitialized : boolean = false;
/**
* 初始化 OCR 服务
*/
export const ocrInit : OCRInit = function (options : OCRInitOptions) {
textRecognition.init().then(() => {
isInitialized = true;
hilog.info(0x0000, 'OCR', 'OCR service initialization successful');
const res : OCRInitResult = {
success: true,
message: 'OCR 初始化成功'
};
options.success?.(res);
options.complete?.(res);
}).catch((err : BusinessError) => {
hilog.error(0x0000, 'OCR', `OCR service initialization failed. Code: ${err.code}, message: ${err.message}`);
const error = new OCRFailImpl(9030001);
options.fail?.(error);
options.complete?.(error);
});
}
/**
* 识别图片中的文字
*/
export const ocrRecognize : OCRRecognize = function (options : OCRRecognizeOptions) {
if (!isInitialized) {
const error = new OCRFailImpl(9030004);
options.fail?.(error);
options.complete?.(error);
return;
}
try {
// 加载图片
loadImageFromPath(options.imagePath).then((pixelMap : image.PixelMap) => {
// 创建 VisionInfo
const visionInfo : textRecognition.VisionInfo = {
pixelMap: pixelMap
};
// 配置识别参数
const textConfiguration : textRecognition.TextRecognitionConfiguration = {
isDirectionDetectionSupported: options.isDirectionDetectionSupported ?? false
};
// 调用识别接口
textRecognition.recognizeText(visionInfo, textConfiguration).then((data : textRecognition.TextRecognitionResult) => {
hilog.info(0x0000, 'OCR', `Text recognition successful: ${data.value}`);
// 构建文本块数组
const blocks : Array<OCRTextBlock> = [];
// 注意:根据华为文档,TextRecognitionResult 只有 value 属性
// 如果需要详细的文本块信息,可能需要使用其他 API
// 这里简化处理,将整个识别结果作为一个文本块
if (data.value && data.value.length > 0) {
blocks.push({
text: data.value,
confidence: 1.0, // 默认置信度
bounds: null
});
}
const res : OCRRecognizeResult = {
success: true,
message: 'OCR 识别成功',
text: data.value,
blocks: blocks
};
options.success?.(res);
options.complete?.(res);
// 释放 PixelMap
pixelMap.release();
}).catch((error : BusinessError) => {
hilog.error(0x0000, 'OCR', `Text recognition failed. Code: ${error.code}, message: ${error.message}`);
const err = new OCRFailImpl(9030002);
options.fail?.(err);
options.complete?.(err);
// 释放 PixelMap
pixelMap.release();
});
}).catch((error : Error) => {
hilog.error(0x0000, 'OCR', `Image loading failed: ${error.message}`);
const err = new OCRFailImpl(9030005);
options.fail?.(err);
options.complete?.(err);
});
} catch (e) {
hilog.error(0x0000, 'OCR', `OCR recognition exception: ${e}`);
const error = new OCRFailImpl(9030002);
options.fail?.(error);
options.complete?.(error);
}
}
/**
* 释放 OCR 服务资源
*/
export const ocrRelease : OCRRelease = function (options : OCRReleaseOptions) {
textRecognition.release().then(() => {
isInitialized = false;
hilog.info(0x0000, 'OCR', 'OCR service released successfully');
const res : OCRReleaseResult = {
success: true,
message: 'OCR 释放成功'
};
options.success?.(res);
options.complete?.(res);
}).catch((err : BusinessError) => {
hilog.error(0x0000, 'OCR', `OCR service release failed. Code: ${err.code}, message: ${err.message}`);
const error = new OCRFailImpl(9030003);
options.fail?.(error);
options.complete?.(error);
});
}
/**
* 从路径加载图片
*/
function loadImageFromPath(imagePath : string) : Promise<image.PixelMap> {
return new Promise<image.PixelMap>((resolve, reject) => {
try {
// 打开文件
fileIo.open(imagePath, fileIo.OpenMode.READ_ONLY).then((file : fileIo.File) => {
// 创建 ImageSource
const imageSource : image.ImageSource = image.createImageSource(file.fd);
// 创建 PixelMap
imageSource.createPixelMap().then((pixelMap : image.PixelMap) => {
// 关闭文件
fileIo.close(file).then(() => {
resolve(pixelMap);
}).catch((err : BusinessError) => {
hilog.error(0x0000, 'OCR', `Failed to close file: ${err.message}`);
resolve(pixelMap); // 即使关闭失败也返回 PixelMap
});
}).catch((err : BusinessError) => {
fileIo.close(file);
reject(new Error(`Failed to create PixelMap: ${err.message}`));
});
}).catch((err : BusinessError) => {
reject(new Error(`Failed to open file: ${err.message}`));
});
} catch (e) {
reject(new Error(`Exception in loadImageFromPath: ${e}`));
}
});
}
4. utssdk/app-harmony/types.uts(类型定义)
/**
* 华为 Core Vision Kit 类型定义
* 根据官方文档定义的类型
*/
/**
* 文本识别结果
* 根据华为文档,TextRecognitionResult 只包含 value 属性
*/
export type HarmonyTextRecognitionResult = {
/** 识别的文本内容 */
value : string
}
/**
* 视觉信息
*/
export type HarmonyVisionInfo = {
/** 图片的 PixelMap */
pixelMap : any
}
/**
* 文本识别配置
*/
export type HarmonyTextRecognitionConfiguration = {
/** 是否支持朝向检测 */
isDirectionDetectionSupported : boolean
}
鸿蒙平台实现原理
1. 引入鸿蒙 SDK
import { textRecognition } from '@kit.CoreVisionKit'; // 文字识别
import { image } from '@kit.ImageKit'; // 图像处理
import { fileIo } from '@kit.CoreFileKit'; // 文件操作
import { hilog } from '@kit.PerformanceAnalysisKit'; // 日志输出
import { BusinessError } from '@kit.BasicServicesKit'; // 错误处理
2. 初始化 OCR 服务
鸿蒙平台使用 textRecognition.init() 初始化 OCR 服务:
textRecognition.init().then(() => {
isInitialized = true;
console.log('OCR 初始化成功');
}).catch((err : BusinessError) => {
console.error('OCR 初始化失败:', err.code, err.message);
});
3. 图片加载与处理
这是 OCR 实现的关键步骤,需要将图片路径转换为 PixelMap:
function loadImageFromPath(imagePath : string) : Promise<image.PixelMap> {
return new Promise((resolve, reject) => {
// 1. 打开文件
fileIo.open(imagePath, fileIo.OpenMode.READ_ONLY).then((file) => {
// 2. 创建 ImageSource
const imageSource = image.createImageSource(file.fd);
// 3. 创建 PixelMap
imageSource.createPixelMap().then((pixelMap) => {
// 4. 关闭文件
fileIo.close(file);
resolve(pixelMap);
});
});
});
}
关键点说明:
- 文件描述符(fd):通过
fileIo.open()获取文件描述符 - ImageSource:使用文件描述符创建图像源
- PixelMap:从图像源创建像素图,这是 OCR 识别的输入格式
- 资源释放:使用完毕后需要调用
pixelMap.release()释放内存
4. 执行文字识别
// 创建 VisionInfo
const visionInfo : textRecognition.VisionInfo = {
pixelMap: pixelMap
};
// 配置识别参数
const textConfiguration : textRecognition.TextRecognitionConfiguration = {
isDirectionDetectionSupported: false // 是否检测文本方向
};
// 调用识别接口
textRecognition.recognizeText(visionInfo, textConfiguration)
.then((data : textRecognition.TextRecognitionResult) => {
console.log('识别结果:', data.value);
// 释放 PixelMap
pixelMap.release();
})
.catch((error : BusinessError) => {
console.error('识别失败:', error.code, error.message);
pixelMap.release();
});
5. 释放资源
textRecognition.release().then(() => {
isInitialized = false;
console.log('OCR 服务已释放');
});
核心技术要点
1. PixelMap 生命周期管理
let pixelMap : image.PixelMap | null = null;
try {
pixelMap = await loadImageFromPath(imagePath);
// 使用 pixelMap 进行识别...
} finally {
// 确保 PixelMap 被释放
if (pixelMap != null) {
pixelMap.release();
}
}
2. 异步操作处理
所有 OCR 操作都是异步的,需要正确处理 Promise:
// ✅ 正确:使用 Promise 链
textRecognition.init()
.then(() => loadImageFromPath(path))
.then((pixelMap) => recognizeText(pixelMap))
.then((result) => console.log(result))
.catch((error) => console.error(error));
// ❌ 错误:忘记处理异步
textRecognition.init();
const result = recognizeText(pixelMap); // 错误!init 可能还未完成
3. 错误处理机制
try {
// OCR 操作
} catch (e) {
// 同步错误
console.error('同步错误:', e);
}
promise.catch((err : BusinessError) => {
// 异步错误
console.error('异步错误:', err.code, err.message);
});
快速开始
第一步:安装插件
方式一:通过插件市场安装(推荐)
- 打开 HBuilderX
- 在项目中右键选择"从插件市场导入插件"
- 搜索
jack-ocr - 点击"导入"
方式二:手动创建
- 在你的 UniApp 项目的
uni_modules中创建新的UTS API插件 - 将上面「插件完整源码」章节中的代码,按照文件路径复制或替换到对应位置
第二步:在页面中使用
1. 导入插件
<script>
// 使用条件编译,仅在鸿蒙平台导入
// #ifdef APP-HARMONY
import { ocrInit, ocrRecognize, ocrRelease } from '@/uni_modules/jack-ocr'
// #endif
export default {
data() {
return {
ocrInitialized: false,
recognizedText: '',
isRecognizing: false
}
}
}
</script>
2. 初始化 OCR
onLoad() {
this.initOCR();
},
methods: {
initOCR() {
// #ifdef APP-HARMONY
ocrInit({
success: (res) => {
console.log('OCR 初始化成功', res);
this.ocrInitialized = true;
uni.showToast({
title: 'OCR 功能已就绪',
icon: 'success'
});
},
fail: (err) => {
console.error('OCR 初始化失败', err);
uni.showToast({
title: 'OCR 初始化失败',
icon: 'none'
});
}
});
// #endif
}
}
3. 选择图片并识别
chooseAndRecognize() {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
const imagePath = res.tempFilePaths[0];
this.recognizeImage(imagePath);
}
});
},
recognizeImage(imagePath) {
// #ifdef APP-HARMONY
if (!this.ocrInitialized) {
uni.showToast({ title: 'OCR 未初始化', icon: 'none' });
return;
}
this.isRecognizing = true;
uni.showLoading({ title: '识别中...' });
ocrRecognize({
imagePath: imagePath,
isDirectionDetectionSupported: false,
success: (res) => {
console.log('识别成功:', res.text);
this.recognizedText = res.text;
uni.hideLoading();
uni.showToast({
title: '识别成功',
icon: 'success'
});
},
fail: (err) => {
console.error('识别失败', err);
uni.hideLoading();
uni.showToast({
title: '识别失败',
icon: 'none'
});
},
complete: () => {
this.isRecognizing = false;
}
});
// #endif
}
4. 页面卸载时释放资源
onUnload() {
// #ifdef APP-HARMONY
if (this.ocrInitialized) {
ocrRelease({
success: (res) => {
console.log('OCR 资源已释放', res);
}
});
}
// #endif
}
完整示例:疫苗本识别应用
下面是一个完整的疫苗本识别示例页面,展示了如何在实际项目中使用 OCR 功能:
<template>
<view class="container">
<!-- 标题 -->
<view class="header">
<text class="title">📋 疫苗本识别</text>
<text class="subtitle">快速录入疫苗接种记录</text>
</view>
<!-- 图片预览区 -->
<view class="preview-area">
<image
v-if="selectedImage"
:src="selectedImage"
class="preview-image"
mode="aspectFit"
/>
<view v-else class="placeholder">
<text class="placeholder-icon">📷</text>
<text class="placeholder-text">请选择疫苗本照片</text>
</view>
</view>
<!-- 识别结果 -->
<view v-if="recognizedText" class="result-area">
<view class="result-header">
<text class="result-title">识别结果</text>
<button @click="copyText" class="copy-btn">复制</button>
</view>
<view class="result-content">
<text class="result-text">{{ recognizedText }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="actions">
<button
@click="chooseFromAlbum"
class="btn btn-primary"
:disabled="isRecognizing"
>
📁 从相册选择
</button>
<button
@click="takePhoto"
class="btn btn-secondary"
:disabled="isRecognizing"
>
📸 拍照识别
</button>
<button
v-if="selectedImage"
@click="recognizeImage"
class="btn btn-success"
:disabled="!ocrInitialized || isRecognizing"
:loading="isRecognizing"
>
{{ isRecognizing ? '识别中...' : '🔍 开始识别' }}
</button>
</view>
<!-- 使用提示 -->
<view class="tips">
<text class="tips-title">💡 使用提示</text>
<text class="tips-item">• 确保照片清晰,光线充足</text>
<text class="tips-item">• 文字尽量水平拍摄</text>
<text class="tips-item">• 避免反光和阴影</text>
<text class="tips-item">• 支持中文、英文等多语言</text>
</view>
</view>
</template>
<script>
// #ifdef APP-HARMONY
import { ocrInit, ocrRecognize, ocrRelease } from '@/uni_modules/jack-ocr'
// #endif
export default {
data() {
return {
ocrInitialized: false,
selectedImage: '',
recognizedText: '',
isRecognizing: false
}
},
onLoad() {
this.initOCR();
},
onUnload() {
this.releaseOCR();
},
methods: {
// 初始化 OCR
initOCR() {
// #ifdef APP-HARMONY
ocrInit({
success: (res) => {
console.log('[OCR] 初始化成功', res);
this.ocrInitialized = true;
},
fail: (err) => {
console.error('[OCR] 初始化失败', err);
uni.showModal({
title: '初始化失败',
content: `OCR 服务初始化失败:${err.errMsg}`,
showCancel: false
});
}
});
// #endif
// #ifndef APP-HARMONY
uni.showModal({
title: '提示',
content: '当前平台不支持 OCR 功能,请在鸿蒙设备上使用',
showCancel: false
});
// #endif
},
// 从相册选择
chooseFromAlbum() {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
this.selectedImage = res.tempFilePaths[0];
this.recognizedText = ''; // 清空之前的识别结果
},
fail: (err) => {
console.error('选择图片失败', err);
}
});
},
// 拍照
takePhoto() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
this.selectedImage = res.tempFilePaths[0];
this.recognizedText = '';
// 拍照后自动识别
this.recognizeImage();
},
fail: (err) => {
console.error('拍照失败', err);
}
});
},
// 识别图片
recognizeImage() {
// #ifdef APP-HARMONY
if (!this.ocrInitialized) {
uni.showToast({
title: 'OCR 未初始化',
icon: 'none'
});
return;
}
if (!this.selectedImage) {
uni.showToast({
title: '请先选择图片',
icon: 'none'
});
return;
}
this.isRecognizing = true;
this.recognizedText = '';
uni.showLoading({
title: '识别中...',
mask: true
});
ocrRecognize({
imagePath: this.selectedImage,
isDirectionDetectionSupported: false,
success: (res) => {
console.log('[OCR] 识别成功:', res);
this.recognizedText = res.text;
if (!res.text || res.text.trim() === '') {
uni.showToast({
title: '未识别到文字',
icon: 'none'
});
} else {
uni.showToast({
title: `识别成功,共 ${res.text.length} 字`,
icon: 'success'
});
}
},
fail: (err) => {
console.error('[OCR] 识别失败', err);
uni.showModal({
title: '识别失败',
content: `错误信息:${err.errMsg}\n错误码:${err.errCode}`,
showCancel: false
});
},
complete: () => {
this.isRecognizing = false;
uni.hideLoading();
}
});
// #endif
},
// 复制文本
copyText() {
if (!this.recognizedText) {
return;
}
uni.setClipboardData({
data: this.recognizedText,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success'
});
}
});
},
// 释放 OCR 资源
releaseOCR() {
// #ifdef APP-HARMONY
if (this.ocrInitialized) {
ocrRelease({
success: (res) => {
console.log('[OCR] 资源已释放', res);
},
fail: (err) => {
console.error('[OCR] 释放失败', err);
}
});
}
// #endif
}
}
}
</script>
<style scoped>
.container {
padding: 30rpx;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: white;
display: block;
margin-bottom: 10rpx;
}
.subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
display: block;
}
.preview-area {
background: white;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 30rpx;
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
width: 100%;
height: 400rpx;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx;
}
.placeholder-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.placeholder-text {
font-size: 28rpx;
color: #999;
}
.result-area {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.result-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.copy-btn {
padding: 10rpx 30rpx;
font-size: 24rpx;
background: #667eea;
color: white;
border: none;
border-radius: 30rpx;
}
.result-content {
background: #f5f5f5;
border-radius: 10rpx;
padding: 20rpx;
max-height: 400rpx;
overflow-y: auto;
}
.result-text {
font-size: 28rpx;
line-height: 1.8;
color: #333;
word-break: break-all;
}
.actions {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 30rpx;
}
.btn {
padding: 30rpx;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: bold;
border: none;
}
.btn-primary {
background: white;
color: #667eea;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.9);
color: #764ba2;
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn[disabled] {
opacity: 0.5;
}
.tips {
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 30rpx;
}
.tips-title {
font-size: 28rpx;
font-weight: bold;
color: white;
display: block;
margin-bottom: 15rpx;
}
.tips-item {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
display: block;
line-height: 2;
}
</style>
实战场景:BabyOne 应用中的 OCR 应用
在我开发的 BabyOne 母婴应用中,OCR 功能被应用在多个场景:
1. 疫苗接种记录识别
// 识别疫苗本上的接种记录
recognizeVaccineRecord(imagePath) {
ocrRecognize({
imagePath: imagePath,
success: (res) => {
// 解析识别结果,提取疫苗名称、接种日期等信息
const vaccineInfo = this.parseVaccineInfo(res.text);
// 自动填充表单
this.vaccineForm = {
name: vaccineInfo.name,
date: vaccineInfo.date,
batch: vaccineInfo.batch,
hospital: vaccineInfo.hospital
};
}
});
},
parseVaccineInfo(text) {
// 使用正则表达式提取关键信息
const nameMatch = text.match(/疫苗名称[::]\s*(.+)/);
const dateMatch = text.match(/接种日期[::]\s*(\d{4}[-/]\d{1,2}[-/]\d{1,2})/);
const batchMatch = text.match(/批号[::]\s*(.+)/);
return {
name: nameMatch ? nameMatch[1].trim() : '',
date: dateMatch ? dateMatch[1] : '',
batch: batchMatch ? batchMatch[1].trim() : ''
};
}
2. 体检报告数据提取
// 识别体检报告,提取身高、体重等数据
recognizeHealthReport(imagePath) {
ocrRecognize({
imagePath: imagePath,
success: (res) => {
const healthData = this.parseHealthData(res.text);
// 自动添加到成长记录
this.addGrowthRecord({
height: healthData.height,
weight: healthData.weight,
headCircumference: healthData.headCircumference,
date: healthData.date
});
}
});
},
parseHealthData(text) {
// 提取身高(支持多种格式)
const heightMatch = text.match(/身高[::]\s*(\d+\.?\d*)\s*cm/i);
// 提取体重
const weightMatch = text.match(/体重[::]\s*(\d+\.?\d*)\s*kg/i);
// 提取头围
const headMatch = text.match(/头围[::]\s*(\d+\.?\d*)\s*cm/i);
return {
height: heightMatch ? parseFloat(heightMatch[1]) : null,
weight: weightMatch ? parseFloat(weightMatch[1]) : null,
headCircumference: headMatch ? parseFloat(headMatch[1]) : null,
date: new Date().toISOString().split('T')[0]
};
}
3. 成长照片文字提取
// 识别照片上的文字(如生日蛋糕上的日期)
recognizePhotoText(imagePath) {
ocrRecognize({
imagePath: imagePath,
isDirectionDetectionSupported: true, // 启用方向检测
success: (res) => {
// 保存识别的文字作为照片描述
this.photoDescription = res.text;
// 尝试提取日期信息
const dateInfo = this.extractDateFromText(res.text);
if (dateInfo) {
this.photoDate = dateInfo;
}
}
});
}
性能优化建议
1. 单例模式管理
避免重复初始化,使用全局状态管理:
// utils/ocr-manager.js
let ocrInitialized = false;
export function initOCROnce(options = {}) {
if (ocrInitialized) {
console.log('[OCR] 已初始化,跳过');
options.success?.({ success: true, message: '已初始化' });
return;
}
// #ifdef APP-HARMONY
const { ocrInit } = require('@/uni_modules/jack-ocr');
ocrInit({
success: (res) => {
ocrInitialized = true;
console.log('[OCR] 全局初始化成功');
options.success?.(res);
},
fail: (err) => {
console.error('[OCR] 全局初始化失败', err);
options.fail?.(err);
}
});
// #endif
}
export function isOCRReady() {
return ocrInitialized;
}
在 App.vue 中全局初始化:
// App.vue
import { initOCROnce } from '@/utils/ocr-manager.js';
export default {
onLaunch() {
// 应用启动时初始化 OCR
initOCROnce({
success: () => {
console.log('OCR 全局初始化完成');
}
});
}
}
2. 批量识别优化
如果需要识别多张图片,建议使用队列机制:
// utils/ocr-queue.js
class OCRQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
}
add(imagePath, callback) {
this.queue.push({ imagePath, callback });
if (!this.isProcessing) {
this.processNext();
}
}
processNext() {
if (this.queue.length === 0) {
this.isProcessing = false;
return;
}
this.isProcessing = true;
const { imagePath, callback } = this.queue.shift();
// #ifdef APP-HARMONY
const { ocrRecognize } = require('@/uni_modules/jack-ocr');
ocrRecognize({
imagePath,
success: (res) => {
callback(null, res);
},
fail: (err) => {
callback(err, null);
},
complete: () => {
// 延迟处理下一个,避免过快
setTimeout(() => this.processNext(), 300);
}
});
// #endif
}
clear() {
this.queue = [];
this.isProcessing = false;
}
}
export default new OCRQueue();
使用队列:
import ocrQueue from '@/utils/ocr-queue.js';
// 批量识别
const images = ['/path/1.jpg', '/path/2.jpg', '/path/3.jpg'];
images.forEach(imagePath => {
ocrQueue.add(imagePath, (err, res) => {
if (err) {
console.error('识别失败:', err);
} else {
console.log('识别成功:', res.text);
}
});
});
错误处理与调试
错误码说明
jack-ocr 插件定义了以下错误码:
| 错误码 | 说明 | 可能原因 | 解决方案 |
|---|---|---|---|
| 9030001 | OCR 初始化失败 | 设备不支持、系统版本过低 | 检查设备兼容性,确保系统版本符合要求 |
| 9030002 | OCR 识别失败 | 图片质量差、格式不支持 | 提高图片质量,检查图片格式 |
| 9030003 | OCR 释放失败 | 资源已释放或未初始化 | 检查调用顺序 |
| 9030004 | OCR 未初始化 | 未调用 ocrInit | 先调用 ocrInit 初始化 |
| 9030005 | 图片加载失败 | 路径错误、文件不存在 | 检查图片路径是否正确 |
调试技巧
1. 添加详细日志
ocrRecognize({
imagePath: imagePath,
success: (res) => {
console.log('[OCR] 识别成功:', {
textLength: res.text.length,
text: res.text,
blocks: res.blocks,
timestamp: new Date().toISOString()
});
},
fail: (err) => {
console.error('[OCR] 识别失败:', {
errCode: err.errCode,
errMsg: err.errMsg,
errSubject: err.errSubject,
imagePath: imagePath,
timestamp: new Date().toISOString()
});
}
});
2. 状态管理
data() {
return {
ocrState: {
initialized: false,
recognizing: false,
lastError: null,
lastResult: null
}
}
},
methods: {
updateOCRState(updates) {
this.ocrState = { ...this.ocrState, ...updates };
console.log('[OCR] 状态更新:', this.ocrState);
}
}
3. 条件编译调试
recognizeImage(imagePath) {
// #ifdef APP-HARMONY
console.log('[OCR] 鸿蒙平台,执行识别');
ocrRecognize({
imagePath,
success: (res) => {
console.log('[OCR] 识别成功');
}
});
// #endif
// #ifndef APP-HARMONY
console.warn('[OCR] 非鸿蒙平台,功能不可用');
uni.showModal({
title: '提示',
content: '当前平台不支持 OCR 功能',
showCancel: false
});
// #endif
}
常见问题 FAQ
Q1: 为什么初始化失败?
A: 可能的原因:
- 设备不支持:部分老旧设备可能不支持 Core Vision Kit
- 系统版本过低:需要 HarmonyOS NEXT 或更高版本
解决方案:
ocrInit({
fail: (err) => {
if (err.errCode === 9030001) {
uni.showModal({
title: '初始化失败',
content: '您的设备可能不支持 OCR 功能,或系统版本过低',
showCancel: false
});
}
}
});
Q2: 识别结果不准确怎么办?
A: 提高识别准确率的方法:
-
提高图片质量:
- 确保光线充足
- 避免反光和阴影
- 保持镜头稳定,避免模糊
-
优化拍摄角度:
- 尽量垂直拍摄
- 文字水平对齐
- 避免倾斜和变形
-
图片预处理:
// 提示用户拍摄技巧 showPhotoTips() { uni.showModal({ title: '拍摄技巧', content: '1. 确保光线充足\n2. 文字清晰可见\n3. 避免反光\n4. 保持水平拍摄', showCancel: false }); }
Q3: 支持哪些图片格式?
A: 支持的格式:
- ✅ JPEG / JPG
- ✅ PNG
- ❌ GIF(不支持)
- ❌ BMP(不支持)
- ❌ WEBP(不支持)
Q4: 如何识别多语言文本?
A: Core Vision Kit 自动支持多语言识别,包括:
- 简体中文
- 繁体中文
- 英文
- 日文
- 韩文
无需额外配置,插件会自动识别文本语言。
进阶技巧
1. 结合正则表达式提取结构化数据
// 提取身份证号
extractIDCard(text) {
const idCardRegex = /\d{17}[\dXx]/g;
const matches = text.match(idCardRegex);
return matches ? matches[0] : null;
},
// 提取手机号
extractPhone(text) {
const phoneRegex = /1[3-9]\d{9}/g;
const matches = text.match(phoneRegex);
return matches || [];
},
// 提取日期
extractDate(text) {
const dateRegex = /\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?/g;
const matches = text.match(dateRegex);
return matches || [];
},
// 提取金额
extractAmount(text) {
const amountRegex = /¥?\d+\.?\d*/g;
const matches = text.match(amountRegex);
return matches || [];
}
2. 实现智能表单填充
// 智能识别并填充表单
smartFillForm(imagePath) {
ocrRecognize({
imagePath,
success: (res) => {
const text = res.text;
// 提取各类信息
const name = this.extractName(text);
const phone = this.extractPhone(text);
const idCard = this.extractIDCard(text);
const address = this.extractAddress(text);
// 自动填充表单
this.form = {
name: name || this.form.name,
phone: phone[0] || this.form.phone,
idCard: idCard || this.form.idCard,
address: address || this.form.address
};
// 提示用户
uni.showToast({
title: '已自动填充表单',
icon: 'success'
});
}
});
}
3. 实现 OCR 结果对比
// 对比两次识别结果
compareOCRResults(imagePath1, imagePath2) {
Promise.all([
this.recognizePromise(imagePath1),
this.recognizePromise(imagePath2)
]).then(([result1, result2]) => {
const similarity = this.calculateSimilarity(result1.text, result2.text);
console.log('相似度:', similarity);
if (similarity > 0.9) {
uni.showToast({
title: '两次识别结果一致',
icon: 'success'
});
} else {
uni.showModal({
title: '识别结果不一致',
content: `相似度:${(similarity * 100).toFixed(2)}%`,
showCancel: false
});
}
});
},
// 将识别封装为 Promise
recognizePromise(imagePath) {
return new Promise((resolve, reject) => {
ocrRecognize({
imagePath,
success: resolve,
fail: reject
});
});
},
// 计算文本相似度(简单实现)
calculateSimilarity(text1, text2) {
const len1 = text1.length;
const len2 = text2.length;
const maxLen = Math.max(len1, len2);
if (maxLen === 0) return 1;
let matches = 0;
const minLen = Math.min(len1, len2);
for (let i = 0; i < minLen; i++) {
if (text1[i] === text2[i]) {
matches++;
}
}
return matches / maxLen;
}
与其他 OCR 方案对比
| 特性 | jack-ocr (鸿蒙原生) | 百度 OCR | 腾讯 OCR | 阿里 OCR |
|---|---|---|---|---|
| 平台支持 | 仅鸿蒙 | 全平台 | 全平台 | 全平台 |
| 网络依赖 | 可离线 | 需要网络 | 需要网络 | 需要网络 |
| 费用 | 免费 | 有免费额度 | 有免费额度 | 有免费额度 |
| 识别速度 | 快(本地) | 中等 | 中等 | 中等 |
| 准确率 | 高 | 高 | 高 | 高 |
| 隐私性 | 高(本地处理) | 中(上传云端) | 中(上传云端) | 中(上传云端) |
| 集成难度 | 简单 | 中等 | 中等 | 中等 |
选择建议:
- ✅ 选择 jack-ocr:鸿蒙应用、注重隐私、离线场景
- ✅ 选择云端 OCR:跨平台应用、需要高级功能(如表格识别、票据识别)
总结
通过本文,我分享了自己在开发 BabyOne 应用过程中封装的 jack-ocr 插件,让大家可以轻松地在 UniApp 项目中集成鸿蒙原生 OCR 功能。该插件具有以下优势:
✅ 简单易用:API 设计简洁,上手快速
✅ 功能完善:支持多语言识别、方向检测
✅ 性能优异:基于鸿蒙原生 API,识别速度快
✅ 隐私保护:本地处理,无需上传云端
✅ 完全开源:本文提供完整源码,可直接复制使用
✅ 插件市场:已发布到 UniApp 插件市场,可直接安装
无论是开发母婴应用、教育应用、办公应用,还是医疗应用,jack-ocr 都能满足你的 OCR 需求。
希望本文能帮助你快速掌握在 UniApp 中使用鸿蒙 OCR 的方法!
💡 提示:本文示例代码已在 HarmonyOS NEXT 上测试通过。如果你在使用过程中遇到问题,欢迎在评论区留言!
⭐ 如果本文对你有帮助,欢迎点赞、收藏、关注!也欢迎分享给更多需要的开发者!