Annotation System
InkLayer’s Annotation Core v0.1 is a UI-framework-agnostic data model that defines the complete annotation lifecycle. Understanding its design helps with persistence, import/export, and custom rendering.
Design Principles
- Framework-agnostic: Decoupled from Konva, PDF.js, React, and Vue
- PDF coordinate system: Uses PDF user space (bottom-left origin, 1pt = 1/72 inch)
- Single source of truth: The only data format for persistence
- Extensible: Convert to any rendering engine via Adapter
14 Annotation Types
InkLayer supports the following annotation tool types (AnnotationType enum):
| Type | Enum Value | Description |
|---|---|---|
| Select | SELECT | Pointer/selection tool, default state |
| Highlight | HIGHLIGHT | Text highlight markup |
| Strikeout | STRIKEOUT | Text strikeout markup |
| Underline | UNDERLINE | Text underline markup |
| Free Text | FREETEXT | Free text note |
| Rectangle | RECTANGLE | Rectangle shape annotation |
| Circle | CIRCLE | Circle/ellipse shape annotation |
| Freehand | FREEHAND | Freehand drawing/scribble |
| Free Highlight | FREE_HIGHLIGHT | Free-area highlight |
| Signature | SIGNATURE | Handwritten/text/image signature |
| Stamp | STAMP | Predefined stamp template |
| Note | NOTE | Popup note |
| Arrow | ARROW | Arrow/line annotation |
| Cloud | CLOUD | Cloud shape annotation |
| None | NONE | No operation / deactivate tool |
Annotation Type Visual Guide
Understanding what each annotation type looks like on a PDF helps you pick the right tool:
| Type | Visual Effect |
|---|---|
| Highlight | Semi-transparent color overlay behind text, like a highlighter pen |
| Strikeout | Horizontal line through the middle of text |
| Underline | Horizontal line below text |
| Free Text | Text box that can be dragged to any position on the PDF, supports rich text |
| Rectangle | Rectangle border on the PDF page, can be filled with color |
| Circle | Ellipse/circle border on the PDF page |
| Freehand | Freehand drawn path with mouse/touch, supports pressure sensitivity |
| Free Highlight | Freehand drawn highlight area (not bound to text) |
| Signature | Handwritten (mouse draw), text, or image signature |
| Stamp | Predefined graphic/text overlay (like “Reviewed” in official documents) |
| Note | Popup sticky note, similar to PDF comment bubbles |
| Arrow | Line segment with arrowhead, connecting two points |
| Cloud | Cloud-shaped border, commonly used to mark revision areas |
Tip: All annotation rendering and editing is handled automatically by InkLayer. You only need to pass data via
initialAnnotationsor retrieve user-created annotations via theonSaveevent.
Annotation Data Model
Each annotation consists of the following core fields:
interface Annotation {
id: string // Globally unique UUID
kind: AnnotationKind // Semantic classification
target: AnnotationTarget // Anchor information
payload?: AnnotationPayload // Semantic content (optional)
appearance?: AnnotationAppearance // Visual properties
relations?: AnnotationRelations // Relationships (reply/popup/reference)
meta?: AnnotationMeta // Metadata
extensions?: Record<string, unknown> // Extension fields
}
AnnotationKind (Semantic Classification)
| Value | Covers |
|---|---|
text-markup | Highlight, underline, strikeout (applied to text) |
note | Free text, popup notes |
ink | Freehand drawing, free highlight |
shape | Rectangle, circle, cloud |
line | Arrow, line segments |
stamp | Stamps, signatures |
file | File attachment annotations |
AnnotationTarget (Anchor)
Defines the annotation’s position in the PDF:
interface AnnotationTarget {
pageIndex: number // Page index (0-based)
geometry: Geometry // Geometric shape
coordinateSystem: 'pdf-user-space' // Coordinate system identifier (only this value)
}
⚠️ Common pitfall:
pageIndexis 0-based, not 1-based. The first page of a PDF =pageIndex: 0, the second page =pageIndex: 1. If annotations appear on the wrong page, check yourpageIndexvalues first.
Geometry (Geometric Types)
Supports 5 geometry types:
| Type Value | Structure | Use Case |
|---|---|---|
'rect' | { rect: { x, y, width, height } } | Rectangular areas (highlight, rectangle) |
'quad' | { quad: PdfQuad } | Quadrilaterals (text selection) |
'path' | { points: PdfPoint[], closed?: boolean } | Paths (freehand drawing) |
'line' | { line: { start: PdfPoint, end: PdfPoint } } | Line segments (arrows) |
'poly' | { poly: { vertices: PdfPoint[] } } | Polygons |
Base Type Definitions
// PdfPoint: a point in PDF coordinate space (object format, NOT an array)
interface PdfPoint {
x: number
y: number
}
// PdfQuad: quadrilateral (used for text selection, corners ordered: top-left, top-right, bottom-right, bottom-left)
interface PdfQuad {
p1: PdfPoint // top-left
p2: PdfPoint // top-right
p3: PdfPoint // bottom-right
p4: PdfPoint // bottom-left
}
// RectGeometry
interface RectGeometry {
type: 'rect'
rect: {
x: number // bottom-left x (PDF coordinates)
y: number // bottom-left y (PDF coordinates, positive is up)
width: number
height: number
}
}
// PathGeometry (freehand drawing path)
interface PathGeometry {
type: 'path'
points: PdfPoint[] // continuous path points
closed?: boolean // whether the path is closed
}
// LineGeometry (arrow/line)
interface LineGeometry {
type: 'line'
line: {
start: PdfPoint
end: PdfPoint
}
}
// PolyGeometry (polygon)
interface PolyGeometry {
type: 'poly'
poly: {
vertices: PdfPoint[]
}
}
Coordinate values use PDF user space units (point, 1pt = 1/72 inch).
Complete Example
Here is a complete JSON data example of a highlight annotation:
{
"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": "This is important" },
"appearance": { "strokeColor": "#FFEB3B", "opacity": 0.6 },
"meta": {
"authorId": { "id": "user-1", "name": "Alice" },
"isNative": false,
"source": "inklayer",
"version": 1,
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-01T10:00:00Z"
}
}
Tip: The coordinate values in
target.geometryuse PDF user space coordinates (origin at bottom-left). InkLayer automatically handles coordinate conversion during rendering — you don’t need to manually transform anything.
Coordinate System
InkLayer uses PDF user space coordinates as the internal storage standard. Web developers are typically familiar with Canvas coordinates, which have a different orientation:
PDF Coordinate System Canvas/Screen Coordinate System
(0,h)────────(w,h) (0,0)────────(w,0)
│ │ │ │
│ ↑Y │ │ ↓Y │
│ │ │ │
(0,0)────────(w,0) (0,h)────────(w,h)
X → X →
Origin: bottom-left Origin: top-left
Y-axis: positive upward Y-axis: positive downward
InkLayer automatically handles coordinate conversion during rendering — you don’t need to manually transform anything:
// PDF space → Canvas space (auto-called during rendering)
pdfToCanvasPoint(pdfPoint, viewportCtx): CanvasPoint
// Canvas space → PDF space (auto-called during annotation extraction)
canvasToPdfPoint(canvasPoint, viewportCtx): PdfPoint
Important: When passing
initialAnnotations, thetarget.coordinateSystemmust be'pdf-user-space', otherwise annotation positions will be offset.
Persistence
The onSave callback gives you the complete Annotation[] directly — you can send this array to your backend as-is. InkLayer does not require you to use any internal storage utility functions.
import { PdfAnnotator } from 'inklayer-react'
import type { Annotation } from 'inklayer-react'
import { useState, useEffect } from 'react'
export default function AnnotatorPage() {
// Load saved annotations from server
const [initialAnnotations, setInitialAnnotations] = useState<Annotation[]>([])
useEffect(() => {
fetch('/api/annotations/doc-123')
.then(res => res.json())
.then((annotations: Annotation[]) => {
setInitialAnnotations(annotations)
})
}, [])
const handleSave = (annotations: Annotation[]) => {
// annotations is the complete Annotation[], send directly to your backend
fetch('/api/annotations/doc-123', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(annotations),
})
}
return (
<PdfAnnotator
url="/document.pdf"
user={{ id: 'user-1', name: 'Alice' }}
layoutStyle={{ width: '100%', height: '600px' }}
initialAnnotations={initialAnnotations}
onSave={handleSave}
/>
)
}
Recommended: Store
Annotation[]directly in your backend. InkLayer’s internal storage format may change across versions; theAnnotationinterface is the stable public API.
Annotation Appearance
interface AnnotationAppearance {
strokeColor?: string // Stroke color (hex)
fillColor?: string // Fill color (hex)
opacity?: number // Opacity 0-1
strokeWidth?: number // Line width (pt)
dashArray?: number[] // Dash style (e.g. [5, 5])
textAlign?: string // Text alignment (left/center/right)
zIndex?: number // Layer order
fontSize?: number // Font size (pt)
fontFamily?: string // Font family
}
Annotation Relationships
interface AnnotationRelations {
parentId?: string // Parent annotation ID (reply association)
popupFor?: string // Popup note ID
replies?: string[] // Child reply ID list
linkedAnnotationIds?: string[] // Linked annotation ID list
}
Annotation Metadata
interface AnnotationMeta {
authorId: string | { id: string; name?: string; avatarUrl?: string } // Author info
isNative?: boolean // Whether it's a PDF.js native annotation
source?: 'inklayer' | 'pdfjs' | 'import' // Data source
version?: number // Data version
createdAt?: string // Creation time (ISO 8601)
updatedAt?: string // Update time (ISO 8601)
}
Data Flow
Write flow:
Konva Node → Adapter.extract() → Annotation → your backend
Read flow:
your backend → Annotation → Adapter.render() → Konva Node → Canvas