21 KiB
PDF Engine Lab - Comprehensive Architecture Plan
Overview
Project Goal: Build a complete visual PDF document editor and generation system as a Single Page Application (SPA).
The system enables users to design PDF documents visually using blocks, layouts, and composition rules without writing code.
System Architecture
flowchart TB
subgraph SPA[Single Page Application - SvelteKit]
subgraph UI[Visual Editor UI]
Canvas[Document Canvas]
Toolbar[Block Toolbar]
PropsPanel[Properties Panel]
Preview[PDF Preview]
end
subgraph Core[Core Engine]
DocModel[Document Model]
LayoutEngine[Layout Engine]
Paginator[Paginator]
end
subgraph Renderers[Renderer Layer]
CanvasRender[Canvas Renderer]
PDFRender[PDF Renderer]
end
end
UI --> DocModel
DocModel --> LayoutEngine
LayoutEngine --> Paginator
Paginator --> Renderers
CanvasRender --> Canvas
PDFRender --> Preview
Directory Structure
src/
├── lib/
│ ├── core/ # Core engine - framework agnostic
│ │ ├── model/ # Document model types and utilities
│ │ │ ├── types.ts # Node types, interfaces
│ │ │ ├── node.ts # Node creation/manipulation
│ │ │ ├── tree.ts # Tree traversal utilities
│ │ │ └── index.ts
│ │ │
│ │ ├── layout/ # Layout engine
│ │ │ ├── engine.ts # Main layout calculation
│ │ │ ├── constraints.ts # Size constraints
│ │ │ ├── flex.ts # Flex-like layout logic
│ │ │ └── index.ts
│ │ │
│ │ ├── pagination/ # Pagination system
│ │ │ ├── paginator.ts # Page break logic
│ │ │ ├── fragmentation.ts # Node splitting across pages
│ │ │ └── index.ts
│ │ │
│ │ └── index.ts
│ │
│ ├── components/ # Document component definitions
│ │ ├── primitives/ # Basic building blocks
│ │ │ ├── text.ts
│ │ │ ├── image.ts
│ │ │ ├── spacer.ts
│ │ │ └── divider.ts
│ │ │
│ │ ├── containers/ # Layout containers
│ │ │ ├── row.ts
│ │ │ ├── column.ts
│ │ │ └── stack.ts
│ │ │
│ │ ├── complex/ # Complex components
│ │ │ ├── table.ts
│ │ │ ├── box.ts
│ │ │ └── page.ts
│ │ │
│ │ ├── registry.ts # Component registration system
│ │ └── index.ts
│ │
│ ├── renderer/ # Rendering layer
│ │ ├── types.ts # Renderer interfaces
│ │ ├── canvas/ # Canvas/HTML preview renderer
│ │ │ ├── renderer.ts
│ │ │ ├── text.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── jspdf/ # jsPDF renderer
│ │ │ ├── renderer.ts
│ │ │ ├── text.ts
│ │ │ ├── shapes.ts
│ │ │ └── index.ts
│ │ │
│ │ └── index.ts
│ │
│ ├── editor/ # Visual editor - Svelte components
│ │ ├── canvas/ # Document canvas
│ │ │ ├── Canvas.svelte
│ │ │ ├── Page.svelte
│ │ │ └── NodeRenderer.svelte
│ │ │
│ │ ├── toolbar/ # Block insertion toolbar
│ │ │ ├── Toolbar.svelte
│ │ │ ├── BlockButton.svelte
│ │ │ └── LayoutPicker.svelte
│ │ │
│ │ ├── panels/ # Side panels
│ │ │ ├── PropertiesPanel.svelte
│ │ │ ├── TreePanel.svelte
│ │ │ └── StylesPanel.svelte
│ │ │
│ │ ├── interactions/ # User interactions
│ │ │ ├── selection.ts
│ │ │ ├── dragdrop.ts
│ │ │ └── history.ts # Undo/redo
│ │ │
│ │ └── index.ts
│ │
│ ├── stores/ # Svelte stores for state
│ │ ├── document.ts # Document state
│ │ ├── selection.ts # Selection state
│ │ ├── history.ts # Undo/redo state
│ │ └── index.ts
│ │
│ └── utils/ # Utility functions
│ ├── math.ts
│ ├── colors.ts
│ └── index.ts
│
├── routes/
│ └── +page.svelte # Main SPA entry point
│
└── app.html
Core Document Model
Node Types
// src/lib/core/model/types.ts
/** Base node structure */
interface BaseNode {
id: string
type: string
props?: Record<string, unknown>
style?: NodeStyle
children?: Node[]
}
/** Style properties - CSS-inspired */
interface NodeStyle {
// Layout
width?: Dimension
height?: Dimension
minWidth?: Dimension
maxWidth?: Dimension
minHeight?: Dimension
minHeight?: Dimension
// Spacing
margin?: Spacing
padding?: Spacing
// Flex container
direction?: 'row' | 'column'
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
align?: 'start' | 'center' | 'end' | 'stretch'
gap?: number
// Visual
backgroundColor?: string
borderColor?: string
borderWidth?: number
borderRadius?: number
// Typography
fontFamily?: string
fontSize?: number
fontWeight?: string
color?: string
textAlign?: 'left' | 'center' | 'right'
lineHeight?: number
}
type Dimension = number | string // 100 or '100%' or 'auto'
type Spacing = number | [number] | [number, number] | [number, number, number, number]
/** Union of all node types */
type Node =
| ContainerNode
| TextNode
| ImageNode
| SpacerNode
| DividerNode
| TableNode
| BoxNode
Component Definitions
// Container nodes
interface ContainerNode extends BaseNode {
type: 'row' | 'column' | 'stack'
children: Node[]
}
// Primitive nodes
interface TextNode extends BaseNode {
type: 'text'
props: {
value: string
}
}
interface ImageNode extends BaseNode {
type: 'image'
props: {
src: string
alt?: string
}
}
interface SpacerNode extends BaseNode {
type: 'spacer'
props?: {
size?: number
}
}
interface DividerNode extends BaseNode {
type: 'divider'
props?: {
thickness?: number
style?: 'solid' | 'dashed' | 'dotted'
}
}
// Complex nodes
interface TableNode extends BaseNode {
type: 'table'
props: {
columns: TableColumn[]
data: Record<string, unknown>[]
}
}
interface BoxNode extends BaseNode {
type: 'box'
children: Node[]
}
Layout Engine Architecture
flowchart LR
subgraph Input
DocTree[Document Tree]
PageSize[Page Size]
end
subgraph LayoutEngine[Layout Engine]
Constraints[Constraint Solver]
FlexLayout[Flex Layout]
Measure[Text Measurement]
end
subgraph Output
LayoutTree[Layout Tree]
Boxes[Positioned Boxes]
end
DocTree --> Constraints
PageSize --> Constraints
Constraints --> FlexLayout
FlexLayout --> Measure
Measure --> LayoutTree
LayoutTree --> Boxes
Layout Algorithm
// src/lib/core/layout/engine.ts
interface LayoutContext {
pageWidth: number
pageHeight: number
pageMargins: [number, number, number, number]
}
interface LayoutBox {
nodeId: string
x: number
y: number
width: number
height: number
children?: LayoutBox[]
}
function calculateLayout(node: Node, context: LayoutContext): LayoutBox {
// 1. Resolve constraints (%, auto, fixed)
// 2. Calculate available space
// 3. Layout children based on container type
// 4. Return positioned box tree
}
Pagination System
flowchart TB
Content[Content Tree]
Layout[Layout Boxes]
subgraph Pagination[Pagination Process]
Check{Fits in page?}
AddPage[Add new page]
Fragment[Fragment node]
Continue[Continue layout]
end
Pages[Paginated Pages]
Content --> Layout
Layout --> Check
Check -->|Yes| Continue
Check -->|No| Fragment
Fragment --> AddPage
AddPage --> Continue
Continue --> Pages
Fragmentation Strategy
// src/lib/core/pagination/fragmentation.ts
interface FragmentResult {
first: Node | null // Part that fits on current page
second: Node | null // Part that goes to next page
}
function canFragment(node: Node): boolean {
// Text: yes
// Table: yes
// Container: yes (if children can be split)
// Image: no
}
function fragmentNode(node: Node, availableHeight: number): FragmentResult {
// Split node across pages
}
Renderer Abstraction
// src/lib/renderer/types.ts
interface RenderContext {
pageWidth: number
pageHeight: number
currentPage: number
}
interface Renderer {
name: string
// Document lifecycle
beginDocument(context: RenderContext): void
endDocument(): void
// Page lifecycle
beginPage(pageNumber: number): void
endPage(): void
// Drawing primitives
drawText(text: string, box: LayoutBox, style: TextStyle): void
drawRect(box: LayoutBox, style: RectStyle): void
drawImage(src: string, box: LayoutBox): void
drawLine(from: Point, to: Point, style: LineStyle): void
// Output
getOutput(): unknown // PDF blob, canvas, etc.
}
Visual Editor Architecture
flowchart TB
subgraph EditorState[Editor State - Svelte Stores]
DocStore[Document Store]
SelectStore[Selection Store]
HistoryStore[History Store]
end
subgraph UI[Editor UI Components]
Canvas[Document Canvas]
Toolbar[Block Toolbar]
PropsPanel[Properties Panel]
TreeView[Tree View]
end
subgraph Interactions[User Interactions]
Selection[Selection Manager]
DragDrop[Drag and Drop]
Keyboard[Keyboard Shortcuts]
end
DocStore --> Canvas
SelectStore --> PropsPanel
SelectStore --> Canvas
Canvas --> Selection
Toolbar --> DragDrop
DragDrop --> DocStore
UI --> Interactions
Interactions --> EditorState
Editor Stores
// src/lib/stores/document.ts
import { writable } from 'svelte/store'
interface DocumentState {
root: Node
pageSettings: PageSettings
dirty: boolean
}
function createDocumentStore() {
const { subscribe, set, update } = writable<DocumentState>({
root: createEmptyDocument(),
pageSettings: { size: 'A4', margins: [40, 40, 40, 40] },
dirty: false
})
return {
subscribe,
setRoot: (node: Node) => update(s => ({ ...s, root: node, dirty: true })),
updateNode: (id: string, updates: Partial<Node>) => { /* ... */ },
insertNode: (parent: string, node: Node, index?: number) => { /* ... */ },
removeNode: (id: string) => { /* ... */ },
moveNode: (id: string, newParent: string, newIndex: number) => { /* ... */ }
}
}
// src/lib/stores/selection.ts
interface SelectionState {
selectedId: string | null
hoveredId: string | null
}
// src/lib/stores/history.ts
interface HistoryState {
past: DocumentState[]
future: DocumentState[]
}
Phase-by-Phase Implementation Roadmap
Phase 1: Foundation
Goal: Core document model and basic PDF export
Deliverables:
-
Document Model Types (
src/lib/core/model/types.ts)- Define all Node interfaces
- Style type definitions
- Type guards for node types
-
Node Utilities (
src/lib/core/model/node.ts)createNode()factory functioncloneNode()deep clonetraverseTree()iteratorfindNodeById()lookup
-
Basic Layout Engine (
src/lib/core/layout/)- Constraint resolution (%, px, auto)
- Row layout algorithm
- Column layout algorithm
- Text measurement
-
jsPDF Renderer (
src/lib/renderer/jspdf/)- Implement Renderer interface
- Text rendering
- Rectangle rendering
- Image rendering
- PDF output generation
-
Component Registry (
src/lib/components/registry.ts)- Register primitive components
- Component metadata (icon, label, defaults)
-
Basic Test Document
- Create sample document tree
- Generate PDF output
Success Criteria:
- Can define a document as JSON
- Can render Text, Row, Column to PDF
- PDF output is correctly positioned
Phase 2: Visual Editor Core
Goal: Basic visual editing capabilities
Deliverables:
-
Document Store (
src/lib/stores/document.ts)- State management for document tree
- CRUD operations for nodes
-
Selection Store (
src/lib/stores/selection.ts)- Track selected/hovered nodes
-
Canvas Component (
src/lib/editor/canvas/)- Page.svelte - Single page representation
- Canvas.svelte - Document container
- NodeRenderer.svelte - Render nodes to HTML preview
-
Block Toolbar (
src/lib/editor/toolbar/)- Toolbar.svelte - Component toolbar
- BlockButton.svelte - Insert button for each block type
- LayoutPicker.svelte - Predefined layout templates
-
Properties Panel (
src/lib/editor/panels/)- PropertiesPanel.svelte - Edit selected node props
- Basic text/spacing inputs
-
Canvas Renderer (
src/lib/renderer/canvas/)- HTML-based preview renderer
- Visual selection indicators
Success Criteria:
- Can see document in browser
- Can insert new blocks
- Can select and edit basic properties
- Preview updates in real-time
Phase 3: Advanced Editing
Goal: Professional editing experience
Deliverables:
-
Drag and Drop (
src/lib/editor/interactions/dragdrop.ts)- Reorder nodes within container
- Move nodes between containers
- Visual drop indicators
-
History/Undo-Redo (
src/lib/stores/history.ts)- Command pattern implementation
- Undo/redo stack
- Keyboard shortcuts (Ctrl+Z, Ctrl+Y)
-
Tree Panel (
src/lib/editor/panels/TreePanel.svelte)- Hierarchical document view
- Expand/collapse containers
- Click to select
-
Styles Panel (
src/lib/editor/panels/StylesPanel.svelte)- Margin/padding controls
- Typography controls
- Color pickers
-
Keyboard Shortcuts
- Delete selected (Delete/Backspace)
- Copy/paste nodes (Ctrl+C, Ctrl+V)
- Duplicate (Ctrl+D)
-
Context Menu
- Right-click on selected node
- Cut, copy, paste, delete, duplicate
Success Criteria:
- Drag and drop works smoothly
- Undo/redo functions correctly
- All style properties editable
- Keyboard shortcuts work
Phase 4: Pagination & Complex Components
Goal: Production-ready document generation
Deliverables:
-
Pagination Engine (
src/lib/core/pagination/)- Page break calculation
- Content fragmentation
- Text splitting across pages
- Table row pagination
-
Table Component (
src/lib/components/complex/table.ts)- Define table structure
- Header row repeat
- Cell layout
- Border handling
-
Box Component (
src/lib/components/complex/box.ts)- Container with visual styling
- Border, background, padding
-
Multi-page Preview
- Show all pages in canvas
- Page navigation
- Page thumbnails
-
Page Settings
- Size selection (A4, A5, Letter, etc.)
- Margin configuration
- Orientation (portrait/landscape)
-
Header/Footer
- Repeat on each page
- Page numbers
- Custom content
Success Criteria:
- Long documents paginate correctly
- Tables split across pages properly
- Headers/footers appear on all pages
- PDF output matches preview
Phase 5: Extensibility & AI
Goal: Platform capabilities
Deliverables:
-
Plugin System
- Custom component registration
- Custom renderer registration
- Plugin lifecycle hooks
-
AI Integration Hooks
- Document generation from prompt
- Smart layout suggestions
- Content transformation
-
Template System
- Save/load document templates
- Template variables
- Data binding
-
Export Options
- PDF quality settings
- PDF metadata (title, author)
- Multiple renderer support
-
Import/Export
- JSON document export
- JSON document import
- Document validation
Success Criteria:
- Plugins can extend functionality
- AI can generate document structures
- Templates work with dynamic data
- Multiple export formats available
Technical Decisions
✅ Recommended Patterns
| Aspect | Decision | Rationale |
|---|---|---|
| State Management | Svelte Stores | Built-in, reactive, simple |
| Layout Model | Flex-like | Familiar, powerful, proven |
| Document Format | JSON | Serializable, AI-compatible |
| PDF Library | jsPDF | Mature, works in browser |
| Component Model | Registry pattern | Extensible, decoupled |
| History | Command pattern | Clean undo/redo |
❌ Anti-Patterns to Avoid
| Anti-Pattern | Why Avoid |
|---|---|
| Absolute positioning | Breaks responsiveness, pagination |
| Coupling engine to jsPDF | Limits future renderer options |
| Storing UI state in document | Mixes concerns |
| Imperative rendering | Hard to maintain, debug |
| Skipping TypeScript | Complex domain needs types |
Component Registry Design
// src/lib/components/registry.ts
interface ComponentDefinition {
type: string
category: 'container' | 'primitive' | 'complex'
label: string
icon: string
defaultProps?: Record<string, unknown>
defaultStyle?: NodeStyle
allowsChildren?: boolean
canFragment?: boolean
}
const componentRegistry = new Map<string, ComponentDefinition>()
function registerComponent(definition: ComponentDefinition) {
componentRegistry.set(definition.type, definition)
}
function getComponent(type: string): ComponentDefinition | undefined {
return componentRegistry.get(type)
}
function getComponentsByCategory(category: string): ComponentDefinition[] {
return Array.from(componentRegistry.values())
.filter(c => c.category === category)
}
// Register built-in components
registerComponent({
type: 'text',
category: 'primitive',
label: 'Text',
icon: 'type',
defaultProps: { value: 'New text' },
canFragment: true
})
registerComponent({
type: 'row',
category: 'container',
label: 'Row',
icon: 'columns',
allowsChildren: true,
defaultStyle: { direction: 'row', gap: 8 }
})
Layout Templates
// Predefined layout structures for quick insertion
const layoutTemplates = [
{
name: 'Two Columns',
icon: 'layout-two-columns',
node: {
type: 'row',
style: { gap: 16 },
children: [
{ type: 'column', children: [] },
{ type: 'column', children: [] }
]
}
},
{
name: 'Header + Content',
icon: 'layout-header',
node: {
type: 'column',
children: [
{
type: 'box',
style: { padding: 16, backgroundColor: '#f0f0f0' },
children: [{ type: 'text', props: { value: 'Header' } }]
},
{ type: 'column', children: [] }
]
}
},
{
name: 'Invoice Layout',
icon: 'file-text',
node: {
type: 'column',
style: { gap: 24 },
children: [
{ type: 'text', props: { value: 'INVOICE' }, style: { fontSize: 24, fontWeight: 'bold' } },
{
type: 'row',
style: { justify: 'between' },
children: [
{ type: 'column', children: [{ type: 'text', props: { value: 'From:' } }] },
{ type: 'column', children: [{ type: 'text', props: { value: 'To:' } }] }
]
},
{ type: 'table', props: { columns: [], data: [] } },
{
type: 'row',
style: { justify: 'end' },
children: [{ type: 'text', props: { value: 'Total: $0.00' } }]
}
]
}
}
]
Testing Strategy
Unit Tests
- Document model operations
- Layout calculations
- Pagination logic
- Node fragmentation
Integration Tests
- Full render pipeline
- Editor interactions
- Store updates
Visual Regression Tests
- PDF output comparison
- Canvas preview comparison
Performance Considerations
-
Lazy Layout Calculation
- Only calculate layout for visible pages
- Cache layout results
-
Efficient Updates
- Incremental layout recalculation
- Debounced property updates
-
Large Documents
- Virtual scrolling in tree view
- Chunked pagination
Next Steps
- Review this plan - Confirm architecture decisions
- Prioritize Phase 1 - Begin with document model and basic rendering
- Set up testing - Establish test patterns early
- Create component registry - Foundation for extensibility
Questions for Clarification
- Should the editor support collaborative editing in the future?
- Are there specific PDF features required (forms, annotations, digital signatures)?
- What level of styling granularity is needed for text (inline styles vs blocks)?
- Should templates support conditional content and loops for data binding?