npm package: @joelouf/doc-renderer
Description: A modular, zero-dependency document layout engine with dual rendering backends for browser preview and file generation.
Author: joelouf
Latest version: 1.0.1

# @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 `zlib` for 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 buffer
```

The 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

```bash
npm install @joelouf/doc-renderer
```

## Quick Start

### Define a Document

```js
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)

```js
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)

```js
import { renderToPdf } from '@joelouf/doc-renderer/render/pdf';

const pdfBuffer = renderToPdf(layout, fonts);
fs.writeFileSync('invoice.pdf', pdfBuffer);
```

### Render to Canvas Blobs (Browser)

```js
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

```js
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:

```js
{ 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