tl;drofdkit-harmony-pro 新增手写签批能力。手写笔默认书写,手指保留阅读操作;笔迹按页面坐标保存为矢量 stroke,缩放后不糊;每条批注可携带用户、部门、业务 ID、时间等元数据,点击笔迹即可追溯。本文聊聊为什么签批不是“在 Canvas 上画几条线”这么简单,以及在 HarmonyOS NEXT 上怎么做。

缘起:OFD 阅读之后,下一步就是签批

前面我做了一个鸿蒙原生 OFD 阅读库 ofdkit-harmony,解决的是“能不能在 HarmonyOS NEXT 上原生打开 OFD”。

但真实政企场景里,打开只是第一步。

电子发票、公文、合同、审批单据,经常还会遇到这些需求:

  • 文件里的红章要能正常显示
  • 签章状态要能验
  • 用户要能直接在 Pad 上手写批注
  • 手写笔书写时,手掌和手指不能误触留下划痕
  • 批注要能带上用户身份、时间、业务单号
  • 后续审计时,能知道这条批注是谁写的、什么时候写的、属于哪个业务流程

所以我在 Pro 版里继续补了一套手写签批能力。

这次目标不是做一个演示 Canvas,而是让 OFD 阅读器真正进入“阅读 + 签批 + 追溯”的业务闭环。

不是画线,是一套文档批注系统

手写签批看起来很简单:监听触摸事件,然后在 Canvas 上画线。

但如果要放进真实文档系统里,问题会立刻变多:

  1. 文档缩放后,笔迹位置能不能对齐?
  2. 页面滚动后,笔迹能不能跟着页面走?
  3. Pad 上手掌误触怎么办?
  4. 批注要保存成图片,还是保存成结构化数据?
  5. 点击一条笔迹,能不能查到它背后的用户和业务信息?
  6. 后续要同步服务端、审计、防篡改,数据结构能不能继续扩展?

所以这次没有把签批做成“截图涂鸦”,而是拆成了几层:

pro/
├── core/          坐标系统、页面坐标转换
├── signature/     数字签章验签、印章解析和渲染
├── annotation/    手写签批、矢量笔迹、防误触、橡皮擦
└── metadata/      批注元数据、点击追溯、字段脱敏

开源版 ofdkit-harmony 继续负责 OFD 解析、页面渲染、搜索、缩略图等基础能力;Pro 版只叠加商业场景需要的签章和签批能力。

1. 手写笔写,手指读

Pad 上做签批,最核心的体验问题是:用户不能一直切模式。

如果每次签字前要点“进入书写模式”,签完再切回“阅读模式”,体验很割裂。尤其是在看合同、公文、审批单时,用户很自然地会一边滑动页面,一边拿笔批注。

所以 Pro 版采用这个交互规则:

  • 手写笔:默认触发书写
  • 手指:保留阅读操作,比如滚动、缩放、翻页
  • 橡皮擦:作为独立工具模式
  • 点击笔迹:展示批注元数据

在 HarmonyOS NEXT 里,可以通过输入源区分手写笔和手指:

PanGesture({ fingers: 1 })
  .allowedTypes([SourceTool.Pen])
  .onActionStart((event) => {
    this.handlePenActionStart(event);
  })
  .onActionUpdate((event) => {
    this.handlePenActionUpdate(event);
  })

阅读手势则保留给手指:

PinchGesture({ fingers: 2 })
  .allowedTypes([SourceTool.Finger])

PanGesture({ fingers: 1 })
  .allowedTypes([SourceTool.Finger])

这样用户拿笔就写,用手就翻,不需要在工具栏里来回切。

2. 笔迹必须是矢量的

如果把批注直接保存成图片,短期实现很快,但后面会有几个问题:

  • 放大后会糊
  • 页面缩放后对齐困难
  • 橡皮擦只能擦像素,不能擦具体 stroke
  • 点击命中很难追溯到某条批注
  • 后续做审计、防篡改、服务端同步都不方便

所以 Pro 版保存的是页面坐标系下的矢量 stroke:

interface InkPoint {
  pageIndex: number;
  x: number;
  y: number;
  pressure?: number;
  timestamp: number;
}

interface InkStroke {
  id: string;
  points: InkPoint[];
  style: InkStrokeStyle;
}

interface InkAnnotation {
  id: string;
  pageIndex: number;
  strokes: InkStroke[];
  metadata?: AnnotationMetadata;
}

这里的 x / y 不是屏幕像素,而是 OFD 页面物理坐标。

也就是说,用户在 100% 缩放下写的字,放大到 300% 后不是把图片拉大,而是重新按页面坐标绘制一遍。

这就是“矢量笔迹”的意义:清晰、可编辑、可命中、可追溯。

3. 缩放和平移后的坐标换算

阅读器本身支持缩放和平移,签批层必须跟它保持一致。

用户看到的是屏幕坐标,但批注要保存到页面坐标:

private pointFromGesture(event: GestureEvent): InkPoint | undefined {
  const finger = event.fingerList[0];

  return {
    pageIndex: this.page.pageIndex,
    x: this.normalizeLocalX(finger.localX) / this.context.width * this.page.physicalBox.width,
    y: this.normalizeLocalY(finger.localY) / this.context.height * this.page.physicalBox.height,
    timestamp: Date.now()
  };
}

渲染时再从页面坐标转回 Canvas 坐标:

const scaleX = this.context.width / this.page.physicalBox.width;
const scaleY = this.context.height / this.page.physicalBox.height;

ctx.moveTo(point.x * scaleX, point.y * scaleY);
ctx.lineTo(next.x * scaleX, next.y * scaleY);

这个设计保证了:

  • 单页模式能签
  • 连续滚动模式也能签
  • 缩放后笔迹仍然对齐
  • 页面尺寸不同也能正常工作

4. 批注要带业务元数据

政企场景里的签批,不只是“某个地方有一条线”。

它通常要回答:

  • 谁写的?
  • 什么部门?
  • 什么角色?
  • 什么时候写的?
  • 对应哪个业务单号?
  • 有没有客户自己的字段?

所以每条批注都可以带 metadata:

metadataProvider: () => ({
  userId: 'u001',
  userName: '张三',
  department: '法务部',
  role: '签批人',
  businessId: 'contract-2026-001',
  createdAt: Date.now(),
  customData: new Map<string, string>([
    ['device', 'HarmonyOS NEXT'],
    ['source', 'local-app']
  ])
})

点击笔迹时,组件会做命中检测,然后把可展示字段回传给 App:

onMetadataTap: (items) => {
  // App 可以用 Toast、气泡、弹窗、侧边栏展示
}

同时元数据展示支持字段配置和脱敏:

  • 姓名脱敏
  • 手机号脱敏
  • 邮箱脱敏
  • 证件号脱敏
  • 自定义前后保留位数

这部分是为了给后续审计和客户定制留接口。

5. 橡皮擦不是擦像素,而是擦 stroke

因为笔迹保存的是结构化 stroke,所以橡皮擦不需要擦 Canvas 像素。

它做的是命中检测:

if (this.store.eraseStrokeAt(point, ERASER_TOLERANCE_MM)) {
  this.notifyAnnotationsChange();
  this.requestInkRender();
}

命中后删除对应 stroke,再重新绘制当前页。

这样做的好处是数据干净,后续导出、同步、审计都不会混进一堆图片碎片。

6. App 也从 Demo 变成了正式阅读器界面

这次还顺手把 Pro App 的界面从 Demo 形态整理成了更接近正式产品的结构。

现在 App 里已经能操作这些能力:

  • 打开 OFD 文档
  • 连续滚动阅读
  • 全文搜索
  • 数字签章显示
  • 验签详情查看
  • 手写签批
  • 橡皮擦
  • 撤销 / 清空
  • 签批身份配置
  • 批注保存、导入、导出
  • 点击笔迹查看元数据

Pad 上采用侧边栏工作台,手机上采用底部功能分组,避免工具面板把阅读区挤没。

这一步很重要,因为 SDK 能力必须能被真实 App 调起来,而不是只停留在 README 里的 API 示例。

当前能力演示

当前 Pro 版已经支持:

✅ 数字签章显示
✅ SM2 / SM3 签章验签
✅ 光栅印章绘制
✅ 矢量印章递归解析渲染
✅ 手写笔默认签批
✅ 手指滚动、缩放、翻页
✅ 手写笔防误触
✅ 矢量笔迹保存
✅ 缩放后笔迹清晰重绘
✅ 单页 / 连续滚动模式签批
✅ 撤销、清空、基础橡皮擦
✅ 批注 JSON 持久化
✅ 批注元数据追溯
✅ 字段配置和脱敏

快速接入

import { installDefaultExtensions } from 'ofdkit-harmony';
import {
  installProExtensions,
  ProInkPageView,
  ProInkDocumentScroll
} from 'ofdkit-harmony-pro';

aboutToAppear(): void {
  installDefaultExtensions();
  installProExtensions();
}

单页签批:

ProInkPageView({
  page,
  metadataProvider: () => ({
    userName: '张三',
    department: '法务部',
    role: '签批人',
    businessId: '合同-2026-001',
    createdAt: Date.now()
  }),
  onAnnotationsChange: (annotations) => {
    // 保存批注 JSON 或同步到业务服务
  },
  onMetadataTap: (items) => {
    // 展示批注追溯信息
  }
})

连续滚动签批:

ProInkDocumentScroll({
  pages: doc.pages,
  initialAnnotations: annotations,
  metadataProvider: () => createMetadata(),
  onAnnotationsChange: (annotations) => saveAnnotations(annotations)
})

下一步

手写签批这套能力已经跑通核心闭环,但还有一些值得继续打磨的地方:

  • 接入真实压感和笔锋算法
  • 做批注防篡改
  • 支持服务端同步
  • 批注文件 hash 绑定
  • PDF 同方案实现
  • 更完整的政企权限和审计链路

可以,文章结尾补这一段:

项目地址 + 开放协作

开源版 ofdkit-harmony

Issue / PR / 讨论请到 Gitee 主仓。

商业版 ofdkit-harmony-pro

  • 提供国密 SM2 / SM3 签章验签
  • 提供光栅 / 矢量印章绘制
  • 提供 Pad 手写签批、防误触、矢量笔迹、批注元数据追溯等政企能力
  • 商业试用、报价、定制开发、技术咨询可以看开源版 README 文末联系方式

如果这篇文章对你有用,欢迎给开源仓库点个 ⭐。后续会继续推进真实压感、笔锋算法、批注防篡改、服务端同步,以及 PDF 同方案实现。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐