批注系统

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'  // 坐标系标识(仅此一个值)
}

⚠️ 易错点pageIndex0 开始,不是 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 等内部函数。