@joelouf/doc-renderer
v1.0.1
Published
A modular, zero-dependency document layout engine with dual rendering backends for browser preview and file generation.
Maintainers
Readme
@joelouf/doc-renderer
A modular, zero-dependency document layout engine with dual rendering backends for browser preview and file generation.
Define documents once with a declarative model, compute layout once with unified font metrics, and render to both outputs from the same positioned draw commands. A single document source, providing no drift between what the user sees and what they download.
Features
- Zero runtime dependencies - the PDF renderer uses Node.js built-in
zlibfor compression; everything else is hand-written - Single-source layout - one document definition, one layout computation, two pixel-identical outputs
- Declarative document model - plain objects with builder functions; no JSX, no framework dependency
- TrueType font parser - reads TTF binaries for glyph metrics and PDF font embedding
- Unified font metrics - text measurement from raw glyph tables ensures layout consistency across backends
- Canvas renderer (browser) - draws commands to
<canvas>via native API, produces PNG Blobs - PDF renderer (server) - writes valid PDF 1.4 with embedded CIDFont Type2 fonts, ToUnicode CMap for searchable/selectable text, and Flate compression
- Word wrapping - automatic line breaks within available width
- Rich layout nodes - text, stack, row, grid, table, box, divider, spacer, fragment, page with footer
Architecture
core/
types.js # Constants, utilities, shared type definitions
document.js # Declarative node builders (text, row, table, etc.)
font-parser.js # TrueType binary parser (cmap, hmtx, hhea, OS/2, etc.)
measure.js # Text measurement & word wrapping from parsed metrics
layout.js # Document model -> DrawCommand[][]
render/
canvas.js # Browser: DrawCommand[][] -> Canvas API -> PNG Blobs
pdf.js # Server: DrawCommand[][] -> PDF binary bufferThe layout engine is environment-agnostic, taking font metrics and producing draw commands. Both renderers are thin drawing loops that execute those commands on their respective surfaces without re-measuring anything.
Install
npm install @joelouf/doc-rendererQuick Start
Define a Document
import {
createDocument, page, text, row, box, divider, spacer,
flexCol, fixedCol
} from '@joelouf/doc-renderer';
const doc = createDocument({
fonts: {
// ArrayBuffer or Uint8Array
regular: regularFontBytes,
bold: boldFontBytes,
},
pages: [
page({ size: 'A4', margins: 40 }, [
text('INVOICE', { font: 'bold', size: 22, color: '#111' }),
text('Invoice #1001', { size: 9, color: '#666' }),
spacer(20),
row([
fixedCol(text('Date', { size: 9, color: '#666' }), 100),
flexCol(text('March 16, 2026', { size: 9, color: '#333' })),
]),
divider(),
box({ bg: '#f5f5f5', padding: 10, borderRadius: 4 }, [
row([
flexCol(text('Total', { font: 'bold', size: 11 })),
fixedCol(text('$1,500.00', { font: 'bold', size: 11, align: 'right' }), 100),
]),
]),
],
// Footer (absolute-positioned at page bottom).
text('Generated by MyApp', { size: 7, color: '#bbb', align: 'center' })
),
],
});Compute Layout (Server)
import { parseFont, createMetrics, computeLayout } from '@joelouf/doc-renderer';
import fs from 'fs';
const fonts = {
regular: parseFont(fs.readFileSync('fonts/regular.ttf')),
bold: parseFont(fs.readFileSync('fonts/bold.ttf')),
};
const metrics = {
regular: createMetrics(fonts.regular),
bold: createMetrics(fonts.bold),
};
const layout = computeLayout(doc, metrics);Render to PDF (Server)
import { renderToPdf } from '@joelouf/doc-renderer/render/pdf';
const pdfBuffer = renderToPdf(layout, fonts);
fs.writeFileSync('invoice.pdf', pdfBuffer);Render to Canvas Blobs (Browser)
import { renderToBlobs } from '@joelouf/doc-renderer/render/canvas';
const blobs = await renderToBlobs(layout, {
scale: 2,
fonts: [
{ name: 'regular', url: '/fonts/regular.ttf' },
{ name: 'bold', url: '/fonts/bold.ttf' },
],
});
// Each blob is a PNG image of one page.
// Pass to <img>, <document-page-viewer>, or any other image display.Document Model
All nodes are plain objects with a type discriminant. Builder functions filter out null/undefined children for clean conditional rendering.
Node Types
| Node | Builder | Description |
|---|---|---|
| text | text(content, style?) | Text content with font, size, color, alignment, letter spacing |
| stack | stack(children, style?) | Vertical layout with optional gap |
| row | row(children, style?) | Horizontal layout with flex/fixed/percent child widths |
| grid | grid(children, style?) | Wrapped multi-column layout |
| table | table(config) | Header + body rows + total rows with column definitions |
| box | box(style, children) | Container with background, padding, borders, border-radius |
| divider | divider(style?) | Horizontal line |
| spacer | spacer(height) | Fixed vertical whitespace |
| fragment | fragment(children) | Transparent grouping (no layout effect) |
| page | page(style, children, footer?) | Page with size, margins, and optional absolute footer |
Row Child Helpers
| Helper | Description |
|---|---|
| flexCol(node, flex?) | Flex-sized child (default flex: 1) |
| fixedCol(node, width) | Fixed-width child in points |
| pctCol(node, percent) | Percentage-width child |
| col(node, sizing) | Explicit sizing object |
Text Style
| Property | Type | Default |
|---|---|---|
| font | string | 'regular' |
| size | number | 10 |
| color | string | '#000000' |
| align | 'left' \| 'right' \| 'center' | 'left' |
| letterSpacing | number | 0 |
| textTransform | 'uppercase' \| 'lowercase' \| 'none' | 'none' |
| italic | boolean | false |
| lineHeight | number | 1.3 |
Font Parser
Parses TrueType (.ttf) binaries to extract glyph metrics, character mappings, and raw font bytes for PDF embedding.
Supported TTF Tables
head, hhea, hmtx, maxp, cmap (formats 4 and 12), OS/2, name, post
API
import { parseFont, createMetrics } from '@joelouf/doc-renderer';
const font = parseFont(ttfBuffer);
// font.name, font.family, font.unitsPerEm, font.ascender, font.descender, font.glyphWidths, font.cmapLookup, font.rawBytes, etc.
const metrics = createMetrics(font);
const width = metrics.measureText('Hello', 12);
const height = metrics.lineHeight(12, 1.3);Draw Commands
The layout engine produces an array of pages, each containing a flat list of draw commands:
{ type: 'text', x, y, content, style: { font, size, color, letterSpacing } }
{ type: 'rect', x, y, w, h, style: { fill?, stroke?, strokeWidth?, borderRadius? } }
{ type: 'line', x1, y1, x2, y2, style: { color, thickness } }Both renderers consume these identically. The commands contain no backend-specific logic.
PDF Output
- PDF 1.4 compliant
- CIDFont Type2 embedding with Identity-H encoding
- ToUnicode CMap for searchable/selectable text
- Flate compression on content and font streams (via Node.js
zlib) - No JavaScript, forms, or embedded files - static content only
Browser Support
The canvas renderer uses standard Web APIs: FontFace, HTMLCanvasElement, CanvasRenderingContext2D, canvas.toBlob(). Supported in all modern browsers.
License
MIT
