Architecture
Understanding InkLayer’s layered architecture is essential for deep usage and extensibility. This chapter explains each layer’s responsibilities, data flow, and design decisions.
Layered Architecture Overview
┌─────────────────────────────────────────────┐
│ User API Layer │
│ PdfAnnotator / PdfViewer Components │
│ (React: FC + Provider, Vue: SFC + Slots) │
├─────────────────────────────────────────────┤
│ Extension System │
│ Toolbar / Sidebar / SelectionBar │
│ Painter (Konva rendering engine) │
│ Editor (14 annotation type editors) │
│ Transform (coordinate codec) │
│ Store (state mgmt: Zustand / Pinia) │
├─────────────────────────────────────────────┤
│ Core Abstraction │
│ Annotation Core (framework-agnostic) │
│ Adapter (Konva / PDF.js adapter interfaces)│
│ Integration (save/load/import/export) │
│ Store Mapper (bidirectional format mapping)│
├─────────────────────────────────────────────┤
│ Infrastructure Layer │
│ PDF.js (~4.3) - PDF rendering engine │
│ Konva (9.0) - Canvas 2D graphics │
│ pdf-lib - PDF manipulation │
│ ExcelJS - Excel export │
└─────────────────────────────────────────────┘
Layer Responsibilities
1. User API Layer
The top-level developer-facing interface, providing ready-to-use components:
- PdfAnnotator: Full annotator with toolbar + editing + sidebar
- PdfViewer: Lightweight viewer without annotation features
- React: Functional Component + Context Provider pattern
- Vue: Single File Component + Provide/Inject + Slots pattern
2. Extension System Layer
Pluggable functional extensions that users can customize or replace:
| Module | Responsibility | Customizable? |
|---|---|---|
| Toolbar | Annotation tool selector (highlight, pen, rectangle…) | ✅ Fully replaceable |
| Sidebar | Annotation list panel, search panel | ✅ Fully replaceable |
| SelectionBar | Text selection popup action bar | ⚠️ Partial customization |
| Painter | Konva annotation drawing engine | ❌ Core low-level |
| Editor | 14 annotation type create/edit logic | ⚠️ Extensible |
| Store | Annotation state management (Zustand / Pinia) | ⚠️ Readable |
3. Core Abstraction Layer
Fully decoupled from UI frameworks and rendering engines — InkLayer’s most important design:
- Annotation Core: Framework-agnostic annotation data model with complete type system
- Adapter: Rendering adapter interface mapping Annotation to specific rendering engines (Konva)
- Integration: Storage formats, import/export, format compatibility layer
- Store Mapper: Bidirectional mapping between runtime state and persisted data
Design highlight: React and Vue versions share exactly the same Core code. This means the annotation model you learn in one package directly applies to the other. Bug fixes in Core benefit both frameworks simultaneously.
4. Infrastructure Layer
| Dependency | Version | Purpose |
|---|---|---|
| PDF.js | ~4.3.136 | PDF parsing, page rendering, text content extraction |
| Konva | ^9.0.0 | Canvas-based 2D graphics, annotation rendering |
| pdf-lib | ^1.17.1 | PDF manipulation (create, modify, export annotations to PDF) |
| ExcelJS | ^4.4.0 | Annotation data export to Excel |
| web-highlighter | ^0.7.4 | Web text highlight selection |
Framework Binding Comparison
| Dimension | React (inklayer-react) | Vue (inklayer-vue) |
|---|---|---|
| Component Pattern | Functional Component | SFC (Single File Component) |
| State Management | Zustand | Pinia |
| Context | React Context | Vue Provide/Inject |
| UI Library | Radix UI Themes | shadcn-vue (reka-ui) |
| Styling | SASS/SCSS | Tailwind CSS 4 + CVA |
| i18n | i18next + react-i18next | vue-i18n |
| Icons | react-icons | @lucide/vue |
| Template Customization | Render Props / children | Named Slots |
| Programmatic Access | Ref + imperativeHandle | Ref + defineExpose |
Data Flow
InkLayer’s data flow is unidirectional, propagating outward from Core:
Write path:
User action → Konva Node create/modify
→ Painter Editor processes
→ Adapter.extract() produces Annotation
→ Store.commit() updates state
→ Integration serializes → Backend persist
Read path:
Backend data → Integration.parse() deserialize
→ Annotation[] objects
→ Store.load() populates state
→ Adapter.render() generates Konva Node
→ Canvas renders to display
Dual-Framework Shared Design
InkLayer’s two packages share the exact same core module. This is achieved through:
- Core module zero-dependency:
annotation.core.ts,adapters/,integration.tsimport no React/Vue code - Adapter pattern decoupling: Konva rendering is isolated via
AnnotationRendererAdapterinterface - Separate Store implementations: Zustand and Pinia each implement
IAnnotationStore - Separate UI bindings: Toolbar, sidebar, and other UI components are implemented per-framework
Custom Extensions
Register a Custom Annotation Adapter
import { AdapterRegistry } from 'inklayer-react/core'
const registry = AdapterRegistry.getInstance()
// Register adapter for custom annotation type
registry.register('custom-kind', {
render(annotation, context) {
// Custom render logic
},
update(node, annotation, context) {
// Custom update logic
},
extract(node, context) {
// Custom extraction logic
}
})
Custom Toolbar (React)
<PdfAnnotator
url="/doc.pdf"
actions={({ save, annotations }) => (
<button onClick={save}>
Save ({annotations.length} annotations)
</button>
)}
/>
Custom Sidebar (Vue)
<PdfAnnotator url="/doc.pdf">
<template #sidebar-header>
<MyCustomHeader />
</template>
<template #sidebar-content>
<MyCustomAnnotationList :annotations="store.annotations" />
</template>
</PdfAnnotator>