tl;dr:ofdkit-harmony-pro 新增手写签批能力。手写笔默认书写,手指保留阅读操作;笔迹按页面坐标保存为矢量 stroke,缩放后不糊;每条批注可携带用户、部门、业务 ID、时间等元数据,点击笔迹即可追溯。本文聊聊为什么签批不是“在 Canvas 上画几条线”这么简单,以及在 HarmonyOS NEXT 上怎么做。
缘起:OFD 阅读之后,下一步就是签批
前面我做了一个鸿蒙原生 OFD 阅读库 ofdkit-harmony,解决的是“能不能在 HarmonyOS NEXT 上原生打开 OFD”。
但真实政企场景里,打开只是第一步。
电子发票、公文、合同、审批单据,经常还会遇到这些需求:
- 文件里的红章要能正常显示
- 签章状态要能验
- 用户要能直接在 Pad 上手写批注
- 手写笔书写时,手掌和手指不能误触留下划痕
- 批注要能带上用户身份、时间、业务单号
- 后续审计时,能知道这条批注是谁写的、什么时候写的、属于哪个业务流程
所以我在 Pro 版里继续补了一套手写签批能力。
这次目标不是做一个演示 Canvas,而是让 OFD 阅读器真正进入“阅读 + 签批 + 追溯”的业务闭环。
不是画线,是一套文档批注系统
手写签批看起来很简单:监听触摸事件,然后在 Canvas 上画线。
但如果要放进真实文档系统里,问题会立刻变多:
- 文档缩放后,笔迹位置能不能对齐?
- 页面滚动后,笔迹能不能跟着页面走?
- Pad 上手掌误触怎么办?
- 批注要保存成图片,还是保存成结构化数据?
- 点击一条笔迹,能不能查到它背后的用户和业务信息?
- 后续要同步服务端、审计、防篡改,数据结构能不能继续扩展?
所以这次没有把签批做成“截图涂鸦”,而是拆成了几层:
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:
- Gitee 主仓:gitee.com/notcoder/of…
- GitHub 镜像:github.com/monotcoder/…
Issue / PR / 讨论请到 Gitee 主仓。
商业版 ofdkit-harmony-pro:
- 提供国密 SM2 / SM3 签章验签
- 提供光栅 / 矢量印章绘制
- 提供 Pad 手写签批、防误触、矢量笔迹、批注元数据追溯等政企能力
- 商业试用、报价、定制开发、技术咨询可以看开源版 README 文末联系方式
如果这篇文章对你有用,欢迎给开源仓库点个 ⭐。后续会继续推进真实压感、笔锋算法、批注防篡改、服务端同步,以及 PDF 同方案实现。