UniApp 中使用鸿蒙原生 OCR 文字识别技术详解

5 阅读8分钟

前言

大家好!我是全栈开发者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

核心特性

  1. 统一的 API 接口:简洁易用的调用方式
  2. 多语言支持:支持简体中文、英文、日文、韩文、繁体中文
  3. 朝向检测:自动检测文本方向
  4. 完善的错误处理:提供详细的错误码和错误信息
  5. 生命周期管理:支持初始化、识别、资源释放
  6. 开箱即用:本文提供完整源码,无需下载,直接复制即可使用

插件市场地址

插件已发布到 UniApp 插件市场,可以直接搜索安装:

🔗 插件市场地址ext.dcloud.net.cn/plugin?name…

为什么要封装这个插件?

在开发 BabyOne 应用时,我需要实现以下功能:

  1. 疫苗本识别:快速录入疫苗接种记录
  2. 体检报告识别:自动提取身高、体重等数据
  3. 成长档案:识别纸质照片上的文字信息

直接使用鸿蒙原生 API 存在以下问题:

  1. 代码复杂:需要处理图片加载、PixelMap 创建等底层细节
  2. 平台差异:鸿蒙 API 无法在 UniApp 中直接使用
  3. 错误处理繁琐:需要手动处理各种异常情况
  4. 资源管理困难:需要手动释放 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);
});

快速开始

第一步:安装插件

方式一:通过插件市场安装(推荐)

  1. 打开 HBuilderX
  2. 在项目中右键选择"从插件市场导入插件"
  3. 搜索 jack-ocr
  4. 点击"导入"

方式二:手动创建

  1. 在你的 UniApp 项目的 uni_modules 中创建新的UTS API插件
  2. 将上面「插件完整源码」章节中的代码,按照文件路径复制或替换到对应位置

第二步:在页面中使用

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 插件定义了以下错误码:

错误码说明可能原因解决方案
9030001OCR 初始化失败设备不支持、系统版本过低检查设备兼容性,确保系统版本符合要求
9030002OCR 识别失败图片质量差、格式不支持提高图片质量,检查图片格式
9030003OCR 释放失败资源已释放或未初始化检查调用顺序
9030004OCR 未初始化未调用 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: 可能的原因:

  1. 设备不支持:部分老旧设备可能不支持 Core Vision Kit
  2. 系统版本过低:需要 HarmonyOS NEXT 或更高版本

解决方案

ocrInit({
  fail: (err) => {
    if (err.errCode === 9030001) {
      uni.showModal({
        title: '初始化失败',
        content: '您的设备可能不支持 OCR 功能,或系统版本过低',
        showCancel: false
      });
    }
  }
});

Q2: 识别结果不准确怎么办?

A: 提高识别准确率的方法:

  1. 提高图片质量

    • 确保光线充足
    • 避免反光和阴影
    • 保持镜头稳定,避免模糊
  2. 优化拍摄角度

    • 尽量垂直拍摄
    • 文字水平对齐
    • 避免倾斜和变形
  3. 图片预处理

    // 提示用户拍摄技巧
    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 上测试通过。如果你在使用过程中遇到问题,欢迎在评论区留言!

如果本文对你有帮助,欢迎点赞、收藏、关注!也欢迎分享给更多需要的开发者!