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):

TypeEnum ValueDescription
SelectSELECTPointer/selection tool, default state
HighlightHIGHLIGHTText highlight markup
StrikeoutSTRIKEOUTText strikeout markup
UnderlineUNDERLINEText underline markup
Free TextFREETEXTFree text note
RectangleRECTANGLERectangle shape annotation
CircleCIRCLECircle/ellipse shape annotation
FreehandFREEHANDFreehand drawing/scribble
Free HighlightFREE_HIGHLIGHTFree-area highlight
SignatureSIGNATUREHandwritten/text/image signature
StampSTAMPPredefined stamp template
NoteNOTEPopup note
ArrowARROWArrow/line annotation
CloudCLOUDCloud shape annotation
NoneNONENo operation / deactivate tool

Annotation Type Visual Guide

Understanding what each annotation type looks like on a PDF helps you pick the right tool:

TypeVisual Effect
HighlightSemi-transparent color overlay behind text, like a highlighter pen
StrikeoutHorizontal line through the middle of text
UnderlineHorizontal line below text
Free TextText box that can be dragged to any position on the PDF, supports rich text
RectangleRectangle border on the PDF page, can be filled with color
CircleEllipse/circle border on the PDF page
FreehandFreehand drawn path with mouse/touch, supports pressure sensitivity
Free HighlightFreehand drawn highlight area (not bound to text)
SignatureHandwritten (mouse draw), text, or image signature
StampPredefined graphic/text overlay (like “Reviewed” in official documents)
NotePopup sticky note, similar to PDF comment bubbles
ArrowLine segment with arrowhead, connecting two points
CloudCloud-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 initialAnnotations or retrieve user-created annotations via the onSave event.

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)

ValueCovers
text-markupHighlight, underline, strikeout (applied to text)
noteFree text, popup notes
inkFreehand drawing, free highlight
shapeRectangle, circle, cloud
lineArrow, line segments
stampStamps, signatures
fileFile 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: pageIndex is 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 your pageIndex values first.

Geometry (Geometric Types)

Supports 5 geometry types:

Type ValueStructureUse 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.geometry use 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, the target.coordinateSystem must 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; the Annotation interface 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