批注系统
InkLayer 的 Annotation Core v0.1 是与 UI 框架无关的数据模型,定义了批注的完整生命周期。了解其设计能帮助你更好地进行持久化、导入导出和自定义渲染。
设计原则
- 框架无关:与 Konva、PDF.js、React、Vue 全部解耦
- PDF 坐标系:统一使用 PDF 用户空间坐标系(左下角为原点,1pt = 1/72 英寸)
- 单一事实来源:持久化存储的唯一数据格式
- 可扩展:通过 Adapter 可转换为任意渲染引擎
14 种批注类型
InkLayer 支持以下批注工具类型(AnnotationType 枚举):
| 类型 | 枚举值 | 说明 |
|---|---|---|
| 选择 | SELECT | 指针/选择工具,默认状态 |
| 高亮 | HIGHLIGHT | 文本高亮标记 |
| 删除线 | STRIKEOUT | 文本删除线标记 |
| 下划线 | UNDERLINE | 文本下划线标记 |
| 自由文本 | FREETEXT | 自由文本注释 |
| 矩形 | RECTANGLE | 矩形图形批注 |
| 圆形 | CIRCLE | 圆形/椭圆图形批注 |
| 自由画笔 | FREEHAND | 自由手写/涂鸦 |
| 自由高亮 | FREE_HIGHLIGHT | 自由区域高亮 |
| 签名 | SIGNATURE | 手写/文字/图片签名 |
| 印章 | STAMP | 预定义印章模板 |
| 注释 | NOTE | 弹出式注释框 |
| 箭头 | ARROW | 箭头/线条批注 |
| 云朵 | CLOUD | 云朵形状图形批注 |
| 无 | NONE | 无操作/停用工具 |
批注类型视觉说明
了解每种批注类型在 PDF 上的实际外观,有助于你选择正确的工具:
| 类型 | 视觉效果 |
|---|---|
| 高亮 | 半透明的彩色背景覆盖在文本后,类似荧光笔效果 |
| 删除线 | 穿过文本中部的横线 |
| 下划线 | 文本底部的横线 |
| 自由文本 | 可拖拽到 PDF 任意位置的文本框,支持富文本 |
| 矩形 | PDF 页面上的矩形边框,可填充颜色 |
| 圆形 | PDF 页面上的椭圆/圆形边框 |
| 自由画笔 | 鼠标/触屏自由绘制的路径,支持压感 |
| 自由高亮 | 自由绘制的 highlight 区域(非文本绑定) |
| 签名 | 手写签名(鼠标绘制)、文字签名或图片签名 |
| 印章 | 类似公文盖章的预设图形/文字(如「已审阅」) |
| 注释 | 弹出式便签,类似 PDF 注释气泡 |
| 箭头 | 带箭头的线段,连接两点 |
| 云朵 | 云朵形状的图形边框,常用于标记修改区域 |
提示:以上所有批注类型的编辑和渲染都由 InkLayer 自动处理,你只需通过
initialAnnotations传入数据,或通过onSave事件获取用户操作产生的批注数据。
Annotation 数据模型
每个批注由以下核心字段组成:
interface Annotation {
id: string // 全局唯一 UUID
kind: AnnotationKind // 语义分类
target: AnnotationTarget // 锚点信息
payload?: AnnotationPayload // 语义内容(可选)
appearance?: AnnotationAppearance // 外观属性
relations?: AnnotationRelations // 关系(回复/Popup/引用)
meta?: AnnotationMeta // 元数据
extensions?: Record<string, unknown> // 扩展字段
}
AnnotationKind(语义分类)
| 值 | 覆盖类型 |
|---|---|
text-markup | 高亮、下划线、删除线(作用于文本) |
note | 自由文本、弹出式注释 |
ink | 自由画笔、自由高亮 |
shape | 矩形、圆形、云朵 |
line | 箭头、线段 |
stamp | 印章、签名 |
file | 文件附件批注 |
AnnotationTarget(锚点)
定义批注在 PDF 中的位置:
interface AnnotationTarget {
pageIndex: number // 所在页码(从 0 开始)
geometry: Geometry // 几何图形
coordinateSystem: 'pdf-user-space' // 坐标系标识(仅此一个值)
}
⚠️ 易错点:
pageIndex从 0 开始,不是 1。PDF 第一页 =pageIndex: 0,第二页 =pageIndex: 1。如果批注显示在了错误的页面,首先检查pageIndex值。
Geometry(几何类型)
支持 6 种几何图形:
| 类型值 | 结构 | 用途 |
|---|---|---|
'rect' | { rect: { x, y, width, height } } | 矩形区域(高亮、矩形) |
'quad' | { quad: PdfQuad } | 四边形(文本选区) |
'path' | { points: PdfPoint[], closed?: boolean } | 路径(自由画笔) |
'line' | { line: { start: PdfPoint, end: PdfPoint } } | 线段(箭头) |
'poly' | { poly: { vertices: PdfPoint[] } } | 多边形 |
基础类型定义
// PdfPoint:PDF 坐标系中的点(对象格式,不是数组)
interface PdfPoint {
x: number
y: number
}
// PdfQuad:四边形(用于文本选区,4 个角点按左上/右上/右下/左下顺序排列)
interface PdfQuad {
p1: PdfPoint // 左上角
p2: PdfPoint // 右上角
p3: PdfPoint // 右下角
p4: PdfPoint // 左下角
}
// RectGeometry
interface RectGeometry {
type: 'rect'
rect: {
x: number // 左下角 x(PDF 坐标系)
y: number // 左下角 y(PDF 坐标系,向上为正)
width: number
height: number
}
}
// PathGeometry(自由画笔路径)
interface PathGeometry {
type: 'path'
points: PdfPoint[] // 连续路径点
closed?: boolean // 是否闭合路径
}
// LineGeometry(箭头/线段)
interface LineGeometry {
type: 'line'
line: {
start: PdfPoint
end: PdfPoint
}
}
// PolyGeometry(多边形)
interface PolyGeometry {
type: 'poly'
poly: {
vertices: PdfPoint[]
}
}
坐标值以 PDF 用户空间单位(point,1pt = 1/72 英寸)表示。
完整示例
以下是一条高亮批注的完整 JSON 数据:
{
"id": "ann-uuid-1234",
"kind": "text-markup",
"target": {
"pageIndex": 0,
"geometry": {
"type": "quad",
"quad": {
"p1": { "x": 100, "y": 720 },
"p2": { "x": 300, "y": 720 },
"p3": { "x": 300, "y": 700 },
"p4": { "x": 100, "y": 700 }
}
},
"coordinateSystem": "pdf-user-space"
},
"payload": { "text": "这段话很重要" },
"appearance": { "strokeColor": "#FFEB3B", "opacity": 0.6 },
"meta": {
"authorId": { "id": "user-1", "name": "张三" },
"isNative": false,
"source": "inklayer",
"version": 1,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
}
}
提示:
target.geometry中的坐标值使用 PDF 用户空间坐标系(原点在左下角)。InkLayer 在渲染时自动完成坐标系转换,你无需手动处理。
坐标系统
InkLayer 使用 PDF 用户空间坐标系作为内部存储标准。Web 开发者通常熟悉 Canvas 坐标系,它们方向不同:
PDF 坐标系 Canvas/屏幕坐标系
(0,h)────────(w,h) (0,0)────────(w,0)
│ │ │ │
│ ↑Y │ │ ↓Y │
│ │ │ │
(0,0)────────(w,0) (0,h)────────(w,h)
X → X →
原点:左下角 原点:左上角
Y 轴:向上为正 Y 轴:向下为正
InkLayer 在渲染时自动完成坐标转换,你无需手动处理:
// PDF 空间 → Canvas 空间(渲染时自动调用)
pdfToCanvasPoint(pdfPoint, viewportCtx): CanvasPoint
// Canvas 空间 → PDF 空间(提取批注时自动调用)
canvasToPdfPoint(canvasPoint, viewportCtx): PdfPoint
重要:你存入
initialAnnotations的批注数据,其target.coordinateSystem必须为'pdf-user-space',否则批注位置会偏移。
批注外观
interface AnnotationAppearance {
strokeColor?: string // 描边颜色(十六进制)
fillColor?: string // 填充颜色
opacity?: number // 透明度 0-1
strokeWidth?: number // 线条宽度(pt)
dashArray?: number[] // 虚线样式(如 [5, 5])
textAlign?: string // 文字对齐(left/center/right)
zIndex?: number // 图层顺序
fontSize?: number // 字体大小(pt)
fontFamily?: string // 字体族
}
批注关系
interface AnnotationRelations {
parentId?: string // 父批注 ID(回复关联)
popupFor?: string // Popup 注释 ID
replies?: string[] // 子回复 ID 列表
linkedAnnotationIds?: string[] // 关联批注 ID 列表
}
批注元数据
interface AnnotationMeta {
authorId: string | { id: string; name?: string; avatarUrl?: string } // 作者信息
isNative?: boolean // 是否 PDF.js 原生批注
source?: 'inklayer' | 'pdfjs' | 'import' // 数据来源
version?: number // 数据版本
createdAt?: string // 创建时间(ISO 8601)
updatedAt?: string // 更新时间(ISO 8601)
}
数据流
写入流程:
Konva Node → Adapter.extract() → Annotation → 你的后端
读取流程:
你的后端 → Annotation → Adapter.render() → Konva Node → Canvas
持久化建议:
onSave回调直接给出完整的Annotation[],你可以直接将此数组发送到自己的后端。后端存储格式由你决定,无需使用createAnnotationStorage等内部函数。