pretext-pdf 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,27 +2,59 @@
2
2
 
3
3
  > **The PDF library AI agents speak natively — and humans love writing.**
4
4
  >
5
- > LLMs emit `PdfDocument` JSON in one shot — no codegen, no headless browser, no `eval`.
6
- > Humans get a strict-typed, declarative API for invoices, reports, resumes, and templates.
5
+ > A `PdfDocument` is plain JSON. LLMs emit it in one shot — no codegen, no headless browser, no `eval`.
6
+ > Humans get a strict-typed declarative API for invoices, reports, resumes, and templates.
7
7
 
8
8
  [![npm version](https://img.shields.io/npm/v/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
9
9
  [![npm downloads](https://img.shields.io/npm/dw/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
10
10
  [![CI](https://github.com/Himaan1998Y/pretext-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/Himaan1998Y/pretext-pdf/actions)
11
11
  [![TypeScript](https://img.shields.io/badge/typescript-strict-blue)](https://www.typescriptlang.org/)
12
- [![Tests](https://img.shields.io/badge/tests-598-brightgreen)](#test-coverage)
12
+ [![Tests](https://img.shields.io/badge/tests-650%2B-brightgreen)](#tests)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
+ [![Bundle](https://img.shields.io/badge/runtime%20deps-7-informational)](#runtime-footprint)
14
15
 
15
- **[Live demo](https://himaan1998y.github.io/pretext-pdf/)** edit JSON, render PDFs instantly. No install.
16
- **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** — drop-in MCP server for Claude / Cursor / Windsurf.
17
- **[Migrating from pdfmake?](docs/MIGRATION_FROM_PDFMAKE.md)** — every pattern mapped.
16
+ **[Live demo](https://himaan1998y.github.io/pretext-pdf/)**  ·  **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** (MCP server)  ·  **[Migrating from pdfmake?](#migrating-from-pdfmake)**
18
17
 
19
18
  *Layout powered by [`@chenglou/pretext`](https://github.com/chenglou/pretext) — the precision text-layout engine by [Cheng Lou](https://github.com/chenglou) (React core team, Midjourney).*
20
19
 
21
20
  ---
22
21
 
22
+ ## Table of contents
23
+
24
+ - [Why pretext-pdf](#why-pretext-pdf)
25
+ - [Install](#install)
26
+ - [Quick start](#quick-start)
27
+ - [Library API](#library-api)
28
+ - [CLI](#cli)
29
+ - [Markdown](#markdown)
30
+ - [Templates](#templates)
31
+ - [pdfmake migration](#migrating-from-pdfmake)
32
+ - [MCP server (Claude / Cursor / Windsurf)](#mcp-server-claude--cursor--windsurf)
33
+ - [Built for AI agents](#built-for-ai-agents)
34
+ - [Element catalog](#element-catalog)
35
+ - [Document features](#document-level-features)
36
+ - [API reference](#api-reference)
37
+ - [India / GST invoicing](#india--gst-invoicing)
38
+ - [Custom fonts](#custom-fonts)
39
+ - [Rich text](#rich-text)
40
+ - [Footnotes](#footnotes)
41
+ - [Examples](#examples)
42
+ - [Error handling](#error-handling)
43
+ - [Troubleshooting](#troubleshooting)
44
+ - [Non-goals](#non-goals)
45
+ - [Runtime footprint](#runtime-footprint)
46
+ - [Performance](#performance)
47
+ - [Tests](#tests)
48
+ - [Security](#security)
49
+ - [Roadmap](#roadmap)
50
+ - [Contributing](#contributing)
51
+ - [Credits](#credits)
52
+
53
+ ---
54
+
23
55
  ## Why pretext-pdf
24
56
 
25
- There are three established camps in JS PDF generation, and one gap. pretext-pdf lives in the gap.
57
+ Three established camps in JS PDF generation, and one gap. pretext-pdf lives in the gap.
26
58
 
27
59
  | | pdfmake / jsPDF / pdfkit | Puppeteer / Playwright | LaTeX / WeasyPrint | **pretext-pdf** |
28
60
  |---|---|---|---|---|
@@ -30,54 +62,12 @@ There are three established camps in JS PDF generation, and one gap. pretext-pdf
30
62
  | Pure ESM, runs in serverless | ✅ | ⚠️ painful in Lambda | ❌ | ✅ |
31
63
  | Professional typography (kerning, hyphenation, RTL/CJK) | ❌ | ✅ | ✅ | ✅ |
32
64
  | Declarative — describe the document, don't draw it | ⚠️ partial | ❌ | ❌ | ✅ |
33
- | **LLM emits a working document in one shot** | ❌ requires a code-execution loop | ❌ requires HTML+CSS knowledge | ❌ requires LaTeX knowledge | ✅ pure JSON |
34
- | MCP server available out of the box | ❌ | ❌ | ❌ | ✅ |
35
-
36
- **The headline:** every other JS PDF library asks an LLM to *write code*. pretext-pdf asks it for a JSON object. That difference is what makes agent-generated PDFs reliable.
37
-
38
- ---
39
-
40
- ## Built for AI agents
41
-
42
- A `PdfDocument` is a plain JSON object. No functions are required. No classes to instantiate. Every field is optional except `type` and a few element-specific essentials. That shape is exactly what an LLM can produce reliably with no tool-use loop.
43
-
44
- ### Drop into Claude / Cursor / Windsurf via MCP
45
-
46
- ```json
47
- {
48
- "mcpServers": {
49
- "pretext-pdf": {
50
- "command": "npx",
51
- "args": ["-y", "pretext-pdf-mcp"]
52
- }
53
- }
54
- }
55
- ```
56
-
57
- Tools exposed: `generate_pdf`, `generate_invoice`, `generate_report`, `generate_from_markdown`, `list_element_types`. Built on the live [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp) package — versioned alongside this library.
58
-
59
- ### Or call from any agent framework
60
-
61
- ```typescript
62
- import { render } from 'pretext-pdf'
65
+ | **LLM emits a working document in one shot** | ❌ requires codegen loop | ❌ requires HTML+CSS knowledge | ❌ requires LaTeX knowledge | ✅ pure JSON |
66
+ | MCP server out of the box | ❌ | ❌ | ❌ | ✅ |
67
+ | Drop-in CLI for shell pipelines | ❌ | ⚠️ wrap with code | ⚠️ separate binary | ✅ `pretext-pdf in.json out.pdf` |
68
+ | pdfmake migration shim | | | | `fromPdfmake()` |
63
69
 
64
- // Whatever produced this JSON Claude, GPT, a workflow node, a form submissionworks the same
65
- const pdf = await render({
66
- metadata: { title: 'AI-generated quarterly report' },
67
- content: [
68
- { type: 'heading', level: 1, text: 'Q1 2026 Summary' },
69
- { type: 'paragraph', text: 'Revenue grew 18% YoY.' },
70
- { type: 'table', columns: [...], rows: [...] },
71
- ],
72
- })
73
- ```
74
-
75
- ### Why JSON-first matters for agents
76
-
77
- - **No code execution loop.** The model returns JSON; you call `render()`. No sandbox, no `vm`, no Vercel Sandbox roundtrip.
78
- - **Schema-validatable.** Strict TypeScript types double as the contract. Pair with [Anthropic tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) or [Vercel AI SDK structured output](https://sdk.vercel.ai/docs/ai-sdk-core/generating-structured-data) for guaranteed-shape results.
79
- - **Self-correcting errors.** Every failure throws `PretextPdfError` with a typed `code`. Feed it back to the model and it fixes itself.
80
- - **Progressive disclosure.** Optional peer deps mean an agent can ask for QR codes, charts, or markdown only when needed — token-efficient prompts.
70
+ **The headline:** every other JS PDF library asks an LLM (or you) to *write code*. pretext-pdf asks for a JSON object. That difference is what makes agent-generated PDFs reliable and the same shape happens to be a clean declarative API for humans too.
81
71
 
82
72
  ---
83
73
 
@@ -89,23 +79,25 @@ npm install pretext-pdf
89
79
 
90
80
  > **ESM only** — use `import`, not `require`. Requires Node.js ≥ 18.
91
81
 
92
- Optional peer dependencies — install only what you need:
82
+ Optional peer dependencies — install only what you use:
93
83
 
94
- ```bash
95
- npm install @napi-rs/canvas # SVG / qr-code / barcode / chart elements
96
- npm install qrcode # qr-code element
97
- npm install bwip-js # barcode element
98
- npm install vega vega-lite # chart element (Vega-Lite specs → vector SVG)
99
- npm install marked # pretext-pdf/markdown entry point
100
- npm install @signpdf/signpdf # PKCS#7 cryptographic signing
101
- ```
84
+ | Peer | When you need it |
85
+ |---|---|
86
+ | `@napi-rs/canvas` | SVG / qr-code / barcode / chart elements (Node only — browser uses `OffscreenCanvas`) |
87
+ | `qrcode` | `qr-code` element |
88
+ | `bwip-js` | `barcode` element (100+ symbologies) |
89
+ | `vega` + `vega-lite` | `chart` element |
90
+ | `marked` | `pretext-pdf/markdown` entry point and `--markdown` CLI flag |
91
+ | `@signpdf/signpdf` | PKCS#7 cryptographic signing |
102
92
 
103
- > **Encryption is built-in** since v0.4.0 — no extra install needed. Just add `encryption` to your document config.
93
+ > **Encryption is built-in** since v0.4.0 — no extra install.
104
94
 
105
95
  ---
106
96
 
107
97
  ## Quick start
108
98
 
99
+ ### Library API
100
+
109
101
  ```typescript
110
102
  import { render } from 'pretext-pdf'
111
103
  import { writeFileSync } from 'fs'
@@ -113,20 +105,21 @@ import { writeFileSync } from 'fs'
113
105
  const pdf = await render({
114
106
  pageSize: 'A4',
115
107
  margins: { top: 40, bottom: 40, left: 50, right: 50 },
116
- metadata: { title: 'My Invoice', author: 'Acme Corp' },
108
+ metadata: { title: 'Invoice #001', author: 'Acme Corp' },
117
109
  content: [
118
110
  { type: 'heading', level: 1, text: 'Invoice #12345' },
119
111
  { type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
120
112
  {
121
113
  type: 'table',
122
114
  columns: [
123
- { name: 'Item', width: 200 },
124
- { name: 'Qty', width: 50, align: 'right' },
125
- { name: 'Price', width: 100, align: 'right' },
115
+ { width: 200 },
116
+ { width: 50, align: 'right' },
117
+ { width: 100, align: 'right' },
126
118
  ],
127
119
  rows: [
128
- { Item: 'Professional Services', Qty: '10', Price: '$1,000' },
129
- { Item: 'Hosting (annual)', Qty: '1', Price: '$500' },
120
+ { isHeader: true, cells: [{ text: 'Item', fontWeight: 700 }, { text: 'Qty', fontWeight: 700 }, { text: 'Price', fontWeight: 700 }] },
121
+ { cells: [{ text: 'Professional Services' }, { text: '10' }, { text: '$1,000' }] },
122
+ { cells: [{ text: 'Hosting (annual)' }, { text: '1' }, { text: '$500' }] },
130
123
  ],
131
124
  },
132
125
  { type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
@@ -136,147 +129,196 @@ const pdf = await render({
136
129
  writeFileSync('invoice.pdf', pdf)
137
130
  ```
138
131
 
139
- ### Builder API (fluent style)
132
+ ### CLI
140
133
 
141
- ```typescript
142
- import { createPdf } from 'pretext-pdf'
134
+ `pretext-pdf` ships with a binary that turns a JSON or Markdown file into a PDF — no Node code required.
143
135
 
144
- const pdf = await createPdf({ pageSize: 'A4' })
145
- .addHeading('My Report', 1)
146
- .addText('Fluent chainable API.')
147
- .addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
148
- .build()
136
+ ```bash
137
+ # JSON in, PDF out
138
+ pretext-pdf doc.json invoice.pdf
139
+
140
+ # Stdin → stdout (pipe-friendly)
141
+ echo '{"content":[{"type":"heading","level":1,"text":"Hi"}]}' | pretext-pdf > out.pdf
142
+
143
+ # Markdown straight to PDF
144
+ pretext-pdf --markdown --code-font 'Courier New' README.md docs.pdf
145
+
146
+ # Help / version
147
+ pretext-pdf --help
148
+ pretext-pdf --version
149
149
  ```
150
150
 
151
- ---
151
+ | Flag | Meaning |
152
+ |---|---|
153
+ | `-i, --input <path>` | Read input from file (default: first positional, or stdin) |
154
+ | `-o, --output <path>` | Write PDF to file (default: second positional, or stdout) |
155
+ | `--markdown` | Treat input as Markdown — converts via `pretext-pdf/markdown` |
156
+ | `--code-font <name>` | With `--markdown`, font family for fenced code blocks |
157
+ | `-v, --version` | Print version |
158
+ | `-h, --help` | Print help |
152
159
 
153
- ## Output samples
160
+ Exit codes: `0` success, `1` user error (bad args, invalid JSON), `2` render error.
154
161
 
155
- Real documents generated with pretext-pdf:
162
+ ### Markdown
156
163
 
157
- | Invoice | Market Report | Resume / CV |
158
- |---------|--------------|-------------|
159
- | [![Invoice](docs/screenshots/showcase-invoice.png)](examples/showcase-invoice.ts) | [![Report](docs/screenshots/showcase-report.png)](examples/showcase-report.ts) | [![Resume](docs/screenshots/showcase-resume.png)](examples/showcase-resume.ts) |
160
- | [View source](examples/showcase-invoice.ts) | [View source](examples/showcase-report.ts) | [View source](examples/showcase-resume.ts) |
164
+ Convert any Markdown string to `ContentElement[]` in one call. Requires `marked` peer dep.
161
165
 
162
- ---
166
+ ```typescript
167
+ import { markdownToContent } from 'pretext-pdf/markdown'
168
+ import { render } from 'pretext-pdf'
163
169
 
164
- ## What's in v0.8.0
170
+ const md = `
171
+ # Q1 2026 Report
165
172
 
166
- Five new capabilities, all behind optional peer dependencies (zero extra weight if unused):
173
+ Revenue grew **18%** year-over-year.
167
174
 
168
- - **`qr-code`** scannable QR codes for UPI payments, URLs, vCards. Requires `qrcode`.
169
- - **`barcode`** — 100+ symbologies (EAN-13, Code128, PDF417, DataMatrix…). Requires `bwip-js`.
170
- - **`chart`** embed Vega-Lite specs as crisp vector SVG. Requires `vega` + `vega-lite`.
171
- - **`pretext-pdf/markdown`** convert any Markdown string to `ContentElement[]` in one call. Requires `marked`.
172
- - **`pretext-pdf/templates`** — zero-dep template helpers: `createInvoice`, `createGstInvoice` (India GST / IGST / CGST+SGST), `createReport`.
175
+ | Metric | Q4 2025 | Q1 2026 | Change |
176
+ |--------|--------:|--------:|:------:|
177
+ | Revenue | $45M | $60M | +33% |
178
+ | Margin | 62% | 68% | +6pp |
173
179
 
174
- See [CHANGELOG.md](CHANGELOG.md) for the full history.
180
+ - [x] Cloud expansion launched
181
+ - [x] Enterprise pipeline doubled
182
+ - [ ] APAC region opening Q2
175
183
 
176
- ---
184
+ > All figures in USD millions.
185
+ `
177
186
 
178
- ## India / GST invoicing
187
+ const content = await markdownToContent(md, { codeFontFamily: 'Courier New' })
188
+ const pdf = await render({ content })
189
+ ```
179
190
 
180
- pretext-pdf has built-in support for Indian invoice requirements:
191
+ Supported: headings h1–h4, bold, italic, strikethrough, inline code, links, ordered/unordered lists (recursive nesting), **GFM tables (with column alignment)**, **GFM task lists** (☑/☐), fenced code blocks, blockquotes, horizontal rules.
181
192
 
182
- - **₹ symbol** renders correctly (bundled Inter font includes the Rupee glyph)
183
- - **Indian number formatting** — helper for 1,00,000 notation (not 100,000)
184
- - **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) table layouts
185
- - **Amount in words** — Indian numbering system (Lakh/Crore)
186
- - **SAC/HSN codes** — column support in line-item tables
193
+ ### Templates
187
194
 
188
- Use the `createGstInvoice` template for a complete GST-compliant invoice in one call:
195
+ Pre-built zero-dependency template functions:
189
196
 
190
197
  ```typescript
191
- import { createGstInvoice } from 'pretext-pdf/templates'
198
+ import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
192
199
  import { render } from 'pretext-pdf'
193
200
 
194
- const content = createGstInvoice({
195
- supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
196
- buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
197
- invoiceNumber: 'INV/2026-27/001',
198
- invoiceDate: '20 Apr 2026',
199
- placeOfSupply: 'Maharashtra (27)',
200
- items: [
201
- { description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
202
- ],
203
- isInterState: true, // auto-detected from state fields if omitted
204
- qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
205
- bankName: 'HDFC Bank', accountNumber: '501001234567', ifscCode: 'HDFC0001234',
201
+ const content = createInvoice({
202
+ from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
203
+ to: { name: 'Client Ltd', address: '456 Oak Ave' },
204
+ invoiceNumber: 'INV-2026-001',
205
+ date: '2026-04-20',
206
+ items: [{ description: 'Consulting', quantity: 10, unitPrice: 150 }],
207
+ currency: '$', taxRate: 10, taxLabel: 'GST',
208
+ qrData: 'upi://pay?pa=acme@bank&am=1650',
206
209
  })
207
210
  const pdf = await render({ content })
208
211
  ```
209
212
 
210
- See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for the raw element approach.
211
-
212
- ---
213
+ Available: `createInvoice` (any currency), `createGstInvoice` (India GST/IGST/CGST+SGST + UPI QR + amount-in-words), `createReport` (with optional TOC).
213
214
 
214
- ## Markdown PDF (`pretext-pdf/markdown`)
215
+ ### Migrating from pdfmake
215
216
 
216
- Convert any Markdown string to a `pretext-pdf` document in one call. Requires `marked` peer dep.
217
+ `pretext-pdf/compat` translates pdfmake document descriptors into a `PdfDocument` most common patterns work without code changes.
217
218
 
218
219
  ```typescript
219
- import { markdownToContent } from 'pretext-pdf/markdown'
220
+ import { fromPdfmake } from 'pretext-pdf/compat'
220
221
  import { render } from 'pretext-pdf'
221
- import { writeFileSync } from 'fs'
222
222
 
223
- const md = `
224
- # Q1 2026 Report
225
-
226
- Revenue grew **18%** year-over-year, driven by:
223
+ // Existing pdfmake document, unchanged
224
+ const pdfmakeDoc = {
225
+ pageSize: 'LETTER',
226
+ pageMargins: [40, 60, 40, 60],
227
+ defaultStyle: { fontSize: 11 },
228
+ styles: {
229
+ header: { fontSize: 22, bold: true },
230
+ subheader: { fontSize: 16 },
231
+ },
232
+ content: [
233
+ { text: 'Invoice #001', style: 'header' },
234
+ { text: 'Acme Corp', style: 'subheader' },
235
+ 'Thanks for your business.',
236
+ {
237
+ table: {
238
+ widths: ['*', 'auto', 80],
239
+ headerRows: 1,
240
+ body: [
241
+ ['Item', 'Qty', 'Price'],
242
+ ['Widget', '3', '$30'],
243
+ ['Sprocket', '5', '$50'],
244
+ ],
245
+ },
246
+ },
247
+ { ul: ['Net 30 terms', 'Late fee: 1.5%/mo'] },
248
+ ],
249
+ }
227
250
 
228
- - Cloud services (+32%)
229
- - Enterprise licenses (+12%)
251
+ const pdf = await render(fromPdfmake(pdfmakeDoc))
252
+ ```
230
253
 
231
- > All figures are in USD millions.
232
- `
254
+ | pdfmake feature | Compat support |
255
+ |---|---|
256
+ | `string` content | ✅ → paragraph |
257
+ | `{ text, bold, italics, color, fontSize, alignment, font }` | ✅ → paragraph or rich-paragraph |
258
+ | `{ text, style: 'header' }` (style lookup) | ✅ — `header`/`h1`/`title` map to heading 1, `subheader`/`h2` to 2, etc. |
259
+ | `{ ul }` / `{ ol }` (recursive) | ✅ → list |
260
+ | `{ table: { body, widths, headerRows } }` | ✅ → table |
261
+ | `{ image, width, height }` | ✅ → image |
262
+ | `{ qr, fit }` | ✅ → qr-code |
263
+ | `{ pageBreak: 'before' \| 'after' }` | ✅ → page-break |
264
+ | `{ stack }` | ✅ → flattened inline |
265
+ | `{ link }` on inline text | ✅ → span.href |
266
+ | `pageSize`, `pageOrientation`, `pageMargins` | ✅ |
267
+ | `info` (title/author/subject/keywords) | ✅ → metadata |
268
+ | `header`, `footer` (string form) | ✅ |
269
+ | `{ columns }` | ⚠️ flattened with a warning |
270
+ | `{ canvas }` | ❌ unsupported (drawing primitives) |
271
+ | Function-style `header`/`footer` | ❌ pass a string |
272
+
273
+ Override the heading-name mapping via `fromPdfmake(doc, { headingMap: { ... } })`.
274
+
275
+ ### MCP server (Claude / Cursor / Windsurf)
276
+
277
+ Drop into any MCP-aware AI agent in 60 seconds:
233
278
 
234
- const content = await markdownToContent(md, {
235
- codeFontFamily: 'Courier New', // enables fenced code block rendering
236
- })
237
- const pdf = await render({ content })
238
- writeFileSync('report.pdf', pdf)
279
+ ```json
280
+ {
281
+ "mcpServers": {
282
+ "pretext-pdf": {
283
+ "command": "npx",
284
+ "args": ["-y", "pretext-pdf-mcp"]
285
+ }
286
+ }
287
+ }
239
288
  ```
240
289
 
241
- Supported Markdown: headings h1–h4, bold, italic, strikethrough, inline code, links, ordered/unordered lists (2 levels), fenced code blocks, blockquotes, horizontal rules.
290
+ Exposes: `generate_pdf`, `generate_invoice`, `generate_report`, `generate_from_markdown`, `list_element_types`. Versioned alongside this library see [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp).
242
291
 
243
292
  ---
244
293
 
245
- ## Invoice & report templates (`pretext-pdf/templates`)
294
+ ## Built for AI agents
246
295
 
247
- Pre-built zero-dependency template functions that generate `ContentElement[]` arrays:
296
+ A `PdfDocument` is a plain JSON object. No functions are required. Every field is optional except `type` and a few element-specific essentials. That shape is exactly what an LLM can produce reliably with no tool-use loop.
248
297
 
249
298
  ```typescript
250
- import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
251
299
  import { render } from 'pretext-pdf'
252
300
 
253
- // Generic invoice (any currency)
254
- const invoiceContent = createInvoice({
255
- from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
256
- to: { name: 'Client Ltd', address: '456 Oak Ave' },
257
- invoiceNumber: 'INV-2026-001', date: '2026-04-20',
258
- items: [{ description: 'Consulting', quantity: 10, unitPrice: 150 }],
259
- currency: '$', taxRate: 10, taxLabel: 'GST',
260
- qrData: 'upi://pay?pa=acme@bank&am=1650',
261
- })
262
-
263
- // Research report with optional TOC
264
- const reportContent = createReport({
265
- title: 'Annual Performance Report',
266
- author: 'Finance Team', date: 'April 2026',
267
- abstract: 'Revenue grew 18% YoY across all segments.',
268
- includeTableOfContents: true,
269
- sections: [
270
- { title: 'Revenue', paragraphs: ['Cloud +32%, Enterprise +12%.'], bullets: ['SaaS: $2.8M', 'Services: $1.1M'] },
301
+ // Whatever produced this JSON — Claude, GPT, a workflow node, a form submission — works the same
302
+ const pdf = await render({
303
+ metadata: { title: 'AI-generated quarterly report' },
304
+ content: [
305
+ { type: 'heading', level: 1, text: 'Q1 2026 Summary' },
306
+ { type: 'paragraph', text: 'Revenue grew 18% YoY.' },
307
+ { type: 'table', columns: [/* ... */], rows: [/* ... */] },
271
308
  ],
272
309
  })
273
-
274
- const pdf = await render({ content: reportContent })
275
310
  ```
276
311
 
312
+ ### Why JSON-first matters for agents
313
+
314
+ - **No code execution loop.** Model returns JSON; you call `render()`. No sandbox, no `vm`, no Vercel Sandbox roundtrip.
315
+ - **Schema-validatable.** Strict TypeScript types double as the contract. Pair with [Anthropic tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) or [Vercel AI SDK structured output](https://sdk.vercel.ai/docs/ai-sdk-core/generating-structured-data).
316
+ - **Self-correcting errors.** Every failure throws `PretextPdfError` with a typed `code`. Feed it back to the model and it fixes itself.
317
+ - **Progressive disclosure.** Optional peer deps mean agents only ask for QR codes, charts, or markdown when needed — token-efficient prompts.
318
+
277
319
  ---
278
320
 
279
- ## Element type reference
321
+ ## Element catalog
280
322
 
281
323
  ```
282
324
  paragraph heading(1-4) spacer hr page-break
@@ -288,42 +330,39 @@ toc qr-code barcode chart footnote-def
288
330
  | Element | What it does |
289
331
  | --- | --- |
290
332
  | `paragraph` | Text block — font, size, color, align, background, letterSpacing, smallCaps, tabularNumbers, multi-column (`columns` + `columnGap`), RTL (`dir`) |
291
- | `heading` | H1–H4 with bookmarks, URL links, internal anchors, tabularNumbers, RTL (`dir`) |
292
- | `table` | Fixed/proportional columns, colspan, rowspan, repeating headers across page breaks |
293
- | `image` | PNG/JPG/WebP with sizing, alignment, float left/right with `floatText` or rich `floatSpans` (mixed-format caption) |
294
- | `list` | Ordered/unordered, 2-level nesting, `nestedNumberingStyle: 'restart' \| 'continue'` |
333
+ | `heading` | H1–H4 with bookmarks, URL links, internal anchors, tabularNumbers, RTL |
334
+ | `table` | Fixed/proportional/auto columns, colspan, rowspan, repeating headers across page breaks |
335
+ | `image` | PNG/JPG/WebP with sizing, alignment, float left/right with `floatText` or rich `floatSpans` |
336
+ | `list` | Ordered/unordered, recursive nesting, `nestedNumberingStyle: 'restart' \| 'continue'` |
295
337
  | `code` | Monospace block with background and padding |
296
338
  | `blockquote` | Left border + background |
297
339
  | `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
298
340
  | `svg` | Embedded SVG graphics with auto-sizing from viewBox |
299
341
  | `toc` | Auto-generated table of contents with accurate page numbers (two-pass) |
300
- | `qr-code` | Scannable QR code — UPI payment links, URLs, vCards. `data`, `size`, `errorCorrectionLevel`, `foreground`/`background` color. Requires `qrcode` peer dep. |
301
- | `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix, and more via `symbology` field. Requires `bwip-js` peer dep. |
302
- | `chart` | Vega-Lite data visualisation — pass any valid Vega-Lite spec to `spec`. Rendered as vector SVG. Requires `vega` + `vega-lite` peer deps. |
342
+ | `qr-code` | Scannable QR code — UPI, URLs, vCards. Requires `qrcode` peer dep. |
343
+ | `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix, etc. Requires `bwip-js`. |
344
+ | `chart` | Vega-Lite data visualisation as vector SVG. Requires `vega` + `vega-lite`. |
303
345
  | `comment` | PDF sticky-note annotation (visible in Acrobat/Preview sidebar) |
304
- | `hr` | Horizontal rule |
305
- | `spacer` | Fixed-height gap |
306
- | `page-break` | Force new page |
346
+ | `form-field` | Interactive text/checkbox/radio/dropdown/button (with `flattenForms` to bake) |
347
+ | `callout` | Info / warning / tip / note callout boxes |
348
+ | `footnote-def` | Paired with `span.footnoteRef` for proper footnote numbering + zone reservation |
349
+ | `hr` / `spacer` / `page-break` | Layout primitives |
307
350
 
308
351
  ### Document-level features
309
352
 
310
353
  | Feature | Config key | Notes |
311
354
  | --- | --- | --- |
312
355
  | Watermarks | `doc.watermark` | Text or image, opacity, rotation |
313
- | Encryption | `doc.encryption` | Password + granular permissions |
356
+ | Encryption | `doc.encryption` | Password + granular permissions, built-in |
357
+ | Cryptographic signing | `doc.signature: { p12, passphrase, ... }` | PKCS#7, optional `@signpdf/signpdf` |
314
358
  | PDF Bookmarks | `doc.bookmarks` | Auto-generated from headings |
315
- | Hyphenation | `doc.hyphenation` | Liang's algorithm, `language: 'en-us'` |
316
- | Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}`, `{{author}}` tokens |
317
- | Per-section overrides | `doc.sections` | Different header/footer/margins per page range |
318
- | Metadata | `doc.metadata` | Title, author, subject, keywords, `language` (PDF /Lang), `producer` |
359
+ | Hyphenation | `doc.hyphenation` | Liang's algorithm, e.g. `language: 'en-us'` |
360
+ | Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}` tokens |
361
+ | Per-section overrides | `doc.sections` | Different header/footer per page range |
362
+ | Metadata | `doc.metadata` | Title, author, subject, keywords, language, producer |
319
363
  | Hyperlinks | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` | External, mailto, internal anchors |
320
- | Inline formatting | `span.verticalAlign: 'superscript'\|'subscript'`, `paragraph.letterSpacing`, `heading.smallCaps` | |
321
- | Sticky notes | `{ type: 'comment', contents: '...' }`, `paragraph.annotation` | |
322
364
  | Document assembly | `merge(pdfs)`, `assemble(parts)` | Combine pre-rendered + freshly rendered |
323
- | Interactive forms | `{ type: 'form-field', fieldType: 'text'\|'checkbox'\|'radio'\|'dropdown'\|'button' }`, `doc.flattenForms` | |
324
- | Cryptographic signing | `doc.signature: { p12, passphrase, signerName, reason, location }` | PKCS#7 via optional `@signpdf/signpdf` |
325
- | Visual signature placeholder | `doc.signature: { signerName, reason, location, x, y, page }` | |
326
- | Callout boxes | `{ type: 'callout', content, style: 'info'\|'warning'\|'tip'\|'note', title }` | |
365
+ | Path-traversal lockdown | `doc.allowedFileDirs` | Restrict file-source reads to listed dirs |
327
366
 
328
367
  ---
329
368
 
@@ -335,22 +374,17 @@ toc qr-code barcode chart footnote-def
335
374
  import { render } from 'pretext-pdf'
336
375
 
337
376
  const pdf = await render({
338
- pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | [w, h]
377
+ pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | 'Tabloid' | [w, h]
339
378
  margins: { top: 72, bottom: 72, left: 72, right: 72 },
340
- defaultFont: 'Inter', // Inter 400 bundled; load others via doc.fonts
379
+ defaultFont: 'Inter', // Inter 400/700 bundled
341
380
  defaultFontSize: 12,
342
- metadata: {
343
- title: 'Document Title',
344
- author: 'Author Name',
345
- subject: 'Description',
346
- keywords: ['pdf', 'report'],
347
- },
381
+ metadata: { title: '...', author: '...', keywords: ['pdf'] },
348
382
  watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
349
383
  encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
350
384
  bookmarks: { minLevel: 1, maxLevel: 3 },
351
- hyphenation: { language: 'en-us', minWordLength: 6 }, // ⚠️ Use lowercase: 'en-us' not 'en-US' — matches the npm package name hyphenation.en-us
352
- header: { text: 'My Document — {{pageNumber}} of {{totalPages}}', align: 'right' },
353
- footer: { text: 'Confidential', align: 'center', color: '#999999' },
385
+ hyphenation: { language: 'en-us', minWordLength: 6 },
386
+ header: { text: '{{pageNumber}} of {{totalPages}}', align: 'right' },
387
+ footer: { text: 'Confidential', align: 'center', color: '#999' },
354
388
  content: [ /* ContentElement[] */ ],
355
389
  })
356
390
  ```
@@ -361,24 +395,74 @@ Combine pre-rendered PDFs:
361
395
 
362
396
  ```typescript
363
397
  import { merge } from 'pretext-pdf'
364
-
365
398
  const combined = await merge([coverPdf, bodyPdf, appendixPdf])
366
399
  ```
367
400
 
368
401
  ### `assemble(parts): Promise<Uint8Array>`
369
402
 
370
- Mix new document configs with existing PDFs:
403
+ Mix new docs with existing PDFs:
371
404
 
372
405
  ```typescript
373
406
  import { assemble } from 'pretext-pdf'
374
407
 
375
408
  const report = await assemble([
376
409
  { pdf: existingCoverPdf },
377
- { doc: { content: [...] } }, // rendered fresh
410
+ { doc: { content: [/* fresh */] } },
378
411
  { pdf: standardTermsPdf },
379
412
  ])
380
413
  ```
381
414
 
415
+ ### `createPdf(opts): PdfBuilder` (fluent builder)
416
+
417
+ ```typescript
418
+ import { createPdf } from 'pretext-pdf'
419
+
420
+ const pdf = await createPdf({ pageSize: 'A4' })
421
+ .addHeading('My Report', 1)
422
+ .addText('Fluent chainable API.')
423
+ .addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
424
+ .build()
425
+ ```
426
+
427
+ ### `markdownToContent(md, opts?)` *(from `pretext-pdf/markdown`)*
428
+
429
+ ### `createInvoice / createGstInvoice / createReport` *(from `pretext-pdf/templates`)*
430
+
431
+ ### `fromPdfmake(doc, opts?)` *(from `pretext-pdf/compat`)*
432
+
433
+ ---
434
+
435
+ ## India / GST invoicing
436
+
437
+ Built-in support for Indian invoice requirements:
438
+
439
+ - **₹ symbol** renders correctly (bundled Inter includes the Rupee glyph)
440
+ - **Indian number formatting** (`1,00,000` not `100,000`)
441
+ - **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) layouts (auto-detected from state fields)
442
+ - **Amount in words** — Indian numbering system (Lakh/Crore), with correct sub-rupee handling
443
+ - **SAC/HSN codes** — column support in line-item tables
444
+
445
+ ```typescript
446
+ import { createGstInvoice } from 'pretext-pdf/templates'
447
+ import { render } from 'pretext-pdf'
448
+
449
+ const content = createGstInvoice({
450
+ supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
451
+ buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
452
+ invoiceNumber: 'INV/2026-27/001',
453
+ invoiceDate: '20 Apr 2026',
454
+ placeOfSupply: 'Maharashtra (27)',
455
+ items: [
456
+ { description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
457
+ ],
458
+ qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
459
+ bankName: 'HDFC Bank', accountNumber: '501001234567', ifscCode: 'HDFC0001234',
460
+ })
461
+ const pdf = await render({ content })
462
+ ```
463
+
464
+ See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for a fully wired example.
465
+
382
466
  ---
383
467
 
384
468
  ## Custom fonts
@@ -392,13 +476,13 @@ const pdf = await render({
392
476
  ],
393
477
  defaultFont: 'Roboto',
394
478
  content: [
395
- { type: 'paragraph', text: 'Uses Roboto font' },
396
- { type: 'paragraph', text: 'Bold text', fontWeight: 700 },
479
+ { type: 'paragraph', text: 'Uses Roboto' },
480
+ { type: 'paragraph', text: 'Bold', fontWeight: 700 },
397
481
  ],
398
482
  })
399
483
  ```
400
484
 
401
- > **Avoid `system-ui`** as a font name on macOS it triggers a known layout-measurement inaccuracy in Pretext. Always name fonts explicitly.
485
+ > **Avoid `system-ui`** — known Pretext layout-measurement inaccuracy on macOS. Always name fonts explicitly.
402
486
 
403
487
  ---
404
488
 
@@ -428,7 +512,7 @@ const pdf = await render({
428
512
 
429
513
  ## Footnotes
430
514
 
431
- Use `createFootnoteSet()` to generate matched reference/definition pairs with guaranteed unique IDs:
515
+ `createFootnoteSet()` produces matched reference/definition pairs with guaranteed unique IDs:
432
516
 
433
517
  ```typescript
434
518
  import { render, createFootnoteSet } from 'pretext-pdf'
@@ -457,37 +541,29 @@ await render({
457
541
 
458
542
  ## Examples
459
543
 
460
- Run working examples from the `examples/` directory:
461
-
462
544
  ```bash
463
- # v0.8.0 new element examples (install optional deps first)
464
- # npm install qrcode bwip-js vega vega-lite marked
465
-
466
- # Phase 7 examples
467
545
  npm run example # Basic invoice
546
+ npm run example:gst # India GST invoice
468
547
  npm run example:watermark # Text/image watermarks
469
548
  npm run example:bookmarks # PDF outline/bookmarks
470
549
  npm run example:toc # Auto table of contents
471
550
  npm run example:rtl # Arabic/Hebrew RTL text
472
551
  npm run example:encryption # Password-protected PDF
473
-
474
- # Phase 8 examples
475
- npm run example:hyperlinks # External links, email links, internal anchors
476
- npm run example:annotations # Sticky notes on elements
477
- npm run example:assembly # Merge and assemble multiple PDFs
478
- npm run example:inline # Superscript, subscript, letter-spacing, small-caps
552
+ npm run example:hyperlinks # External + email + internal anchors
553
+ npm run example:annotations # Sticky notes
554
+ npm run example:assembly # Merge + assemble multiple PDFs
555
+ npm run example:inline # Super/subscript, letterSpacing, smallCaps
479
556
  npm run example:forms # Interactive form fields
480
- npm run example:callout # Callout boxes (info, warning, tip, note)
481
- npm run example:gst # India GST-compliant invoice
557
+ npm run example:callout # Callout boxes
482
558
  ```
483
559
 
484
- All examples write output to `output/*.pdf`.
560
+ All write to `output/*.pdf`.
485
561
 
486
562
  ---
487
563
 
488
564
  ## Error handling
489
565
 
490
- Every error throws `PretextPdfError` with a typed `code` — designed so an LLM (or a human) can self-correct:
566
+ Every error throws `PretextPdfError` with a typed `code`:
491
567
 
492
568
  ```typescript
493
569
  import { render, PretextPdfError } from 'pretext-pdf'
@@ -500,36 +576,32 @@ try {
500
576
  case 'VALIDATION_ERROR': // Invalid config
501
577
  case 'FONT_LOAD_FAILED': // Font file not found
502
578
  case 'IMAGE_TOO_TALL': // Image doesn't fit on page
503
- case 'ASSEMBLY_EMPTY': // merge/assemble called with empty array
504
- // ... see CHANGELOG.md for full list
579
+ case 'IMAGE_LOAD_FAILED': // URL fetch / safety check failed
580
+ case 'ASSEMBLY_EMPTY': // merge / assemble called with empty array
581
+ // ... see CHANGELOG.md for the full list
505
582
  }
506
583
  }
507
584
  }
508
585
  ```
509
586
 
587
+ This shape is also designed for AI self-correction loops — the typed `code` is enough context for an LLM to fix its own output.
588
+
510
589
  ---
511
590
 
512
591
  ## Troubleshooting
513
592
 
514
593
  ### Hyphenation language not found
515
594
 
516
- ```
517
- UNSUPPORTED_LANGUAGE: Language 'en-US' not supported
518
- ```
519
-
520
595
  Use **lowercase** language codes that match the npm package name:
521
596
 
522
597
  ```typescript
523
- // Wrong 'en-US' fails on Linux (case-sensitive filesystem)
524
- hyphenation: { language: 'en-US' }
525
-
526
- // Correct — matches 'hyphenation.en-us' package name
527
- hyphenation: { language: 'en-us' }
598
+ hyphenation: { language: 'en-us' } //
599
+ hyphenation: { language: 'en-US' } // ❌ fails on Linux (case-sensitive FS)
528
600
  ```
529
601
 
530
- ### SVG rendering requires optional dependency
602
+ ### SVG / chart / qr-code / barcode rendering
531
603
 
532
- Install `@napi-rs/canvas` for SVG / chart / qr-code / barcode support:
604
+ Install `@napi-rs/canvas` (Node only browsers use native `OffscreenCanvas`):
533
605
 
534
606
  ```bash
535
607
  npm install @napi-rs/canvas
@@ -537,16 +609,19 @@ npm install @napi-rs/canvas
537
609
 
538
610
  ### PDF is blank or too small
539
611
 
540
- Check margins if left+right margins exceed page width, content width becomes negative:
612
+ Check margins. If `left + right` exceeds page width, content width becomes negative:
541
613
 
542
614
  ```typescript
543
- // For narrow pages, reduce margins:
544
615
  margins: { top: 36, bottom: 36, left: 36, right: 36 }
545
616
  ```
546
617
 
547
- ### Form fields not interactive after `flattenForms`
618
+ ### Form fields not interactive
548
619
 
549
- `flattenForms: true` bakes fields into static content — by design. Remove it to keep them interactive.
620
+ `flattenForms: true` bakes fields into static content — by design. Remove the flag to keep them interactive.
621
+
622
+ ### Browser usage
623
+
624
+ Supply font bytes via `doc.fonts: [{ family: 'Inter', weight: 400, src: <Uint8Array> }]` — the bundled Inter loader is Node-only. Also register the same font with `document.fonts.add(new FontFace(...))` so pretext's measurement matches pdf-lib's drawing.
550
625
 
551
626
  ---
552
627
 
@@ -556,66 +631,79 @@ What pretext-pdf is **not** trying to be — pick a different tool for these:
556
631
 
557
632
  - **Editing or parsing existing PDFs** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdf-parse`](https://www.npmjs.com/package/pdf-parse)
558
633
  - **Filling existing PDF form templates** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdftk`](https://www.pdflabs.com/tools/pdftk-server/)
559
- - **Heavily art-directed pages** with CSS grids, SVG illustrations, floats, background images → headless Chrome (Puppeteer) still wins
634
+ - **Heavily art-directed pages** with CSS grids, SVG illustrations, floats, background images → headless Chrome (Puppeteer)
560
635
  - **PDF/A archival, PDF/UA accessibility tagging** → not yet
561
- - **Print-shop kerning pairs, OpenType ligatures, variable-font axes beyond weight** → Pretext itself doesn't model these
636
+ - **Print-shop kerning pairs, OpenType ligatures, variable-font axes beyond weight** → upstream Pretext doesn't model these
562
637
 
563
638
  ---
564
639
 
565
- ## Runtime requirements
640
+ ## Runtime footprint
641
+
642
+ Mandatory runtime dependencies:
566
643
 
567
- - **Node.js ≥ 18** with `@napi-rs/canvas` peer dep (lazy-loaded only required when you use SVG/chart/QR/barcode elements)
568
- - **`Intl.Segmenter`** (built-in on Node 18+ and all modern browsers)
569
- - **Browser support**works directly in modern browsers; bring your own font bytes
570
- - **Cold-start cost** on serverless: `@napi-rs/canvas` adds ~5–10 MB and a few hundred ms on the first request. Subsequent requests in a warm container are sub-second.
571
- - **Fonts must be fully loaded** before `render()` runs — for browser usage, await `document.fonts.ready` first
644
+ - `@cantoo/pdf-lib` — PDF assembly
645
+ - `@chenglou/pretext` — text-layout engine
646
+ - `@fontsource/inter` + `@pdf-lib/fontkit` bundled Inter + font subsetting
647
+ - `bidi-js` bidirectional text resolution
648
+ - `hypher` + `hyphenation.en-us` — hyphenation
649
+
650
+ All other capabilities (SVG, charts, QR, barcodes, markdown, signing) are optional peer deps — install only what you use.
651
+
652
+ **Browser:** the library imports cleanly from any non-`file://` URL (esm.sh, Vite dev server, browser bundles) since v0.8.1. Bring your own Inter font via `doc.fonts` and register it with `document.fonts.add(...)` for accurate measurement.
572
653
 
573
654
  ---
574
655
 
575
656
  ## Performance
576
657
 
577
- Benchmarked on Windows 11 / Node 22 / Intel i7-12th Gen. Numbers are averages over 10 runs, excluding the first cold JIT run.
658
+ Benchmarked on Windows 11 / Node 22 / Intel i7-12th Gen. Averages over 10 runs, excluding the first cold JIT.
578
659
 
579
660
  | Document | Render time | PDF size |
580
661
  | --- | --- | --- |
581
662
  | 1 page (heading + paragraph + list) | ~220 ms | ~45 KB |
582
- | 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
583
663
  | Mixed (heading + paragraph + 20-row table + list + hr) | ~290 ms | ~60 KB |
664
+ | 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
584
665
 
585
- **Font subsetting** is automatic for TTF/OTF fonts. Only the glyphs used in the document are embedded, typically reducing PDF size by 40–60% compared to full font embedding. A typical single-font invoice renders under 65 KB. WOFF2 fonts are embedded without subsetting due to an upstream library limitation.
666
+ **Font subsetting** is automatic for TTF/OTF fonts. Only used glyphs are embedded typically 40–60% smaller than full-font embedding. Single-font invoices render under 65 KB.
586
667
 
587
- For large documents (10,000+ elements), set `NODE_OPTIONS=--max-old-space-size=4096` to prevent GC pressure.
668
+ For documents with 10,000+ elements, set `NODE_OPTIONS=--max-old-space-size=4096`.
588
669
 
589
670
  ---
590
671
 
591
- ## Test coverage
672
+ ## Tests
592
673
 
593
- 598+ tests across all phases with 100% pass rate:
674
+ 650+ tests across all phases with 100% pass rate:
594
675
 
595
676
  ```bash
596
- npm test # Full suite (unit + e2e + all phases including v0.8.0)
597
- npm run test:unit # Validation, builder, rich-text unit tests
598
- npm run test:e2e # End-to-end render tests
599
- npm run test:10a # QR code + barcode tests
600
- npm run test:10b # Vega-Lite chart tests
601
- npm run test:10c # Markdown converter tests
602
- npm run test:10d # Template function tests
603
- npm run test:phases # All phase tests (7–11, performance, signatures)
677
+ npm test # Full suite (contract + unit + e2e + phases + 2f stress)
678
+ npm run test:unit # Validation, builder, rich-text
679
+ npm run test:e2e # End-to-end render
680
+ npm run test:phases # All phase tests including v0.8/v0.9 features
681
+ npm run test:rich # Rich-paragraph compositor (incl. v0.8.2 whitespace regressions)
682
+ npm run test:contract # Public API surface contracts
683
+ npm run test:visual # Pixel-diff visual regressions
604
684
  ```
605
685
 
606
- **Coverage**: type safety, path validation, error handling, boundary cases, crypto signing, document assembly, all content elements, optional-dep error codes, MCP tool validation.
686
+ **Coverage**: type safety, path validation, SSRF, error handling, boundary cases, crypto signing, document assembly, every content element, optional-dep error codes, MCP tool validation, browser import simulation.
607
687
 
608
688
  ---
609
689
 
610
690
  ## Security
611
691
 
612
- Comprehensive April 2026 security audit completed 41 issues identified and fixed across path-traversal protection, async I/O, error sanitization, type-safety, and explicit failure modes. See [SECURITY.md](SECURITY.md) for the disclosure policy and [CHANGELOG.md](CHANGELOG.md) for audit details.
692
+ A comprehensive April 2026 audit fixed 41 issues across path-traversal protection, async I/O, error sanitization, type safety, and explicit failure modes. Subsequent fixes:
693
+
694
+ - **v0.8.3** — IPv4-mapped IPv6 SSRF bypass closed; `fetch` redirects now revalidated per hop.
695
+ - **v0.8.1** — Browser module-init crashes fixed (Node-only APIs gated behind `IS_NODE` checks).
696
+
697
+ Highlights of the current security posture:
613
698
 
614
- Highlights:
615
- - Zero known path-traversal vulnerabilities; opt-in `allowedFileDirs` lockdown for user-controlled inputs
616
- - All error messages sanitized — no filesystem paths or secrets leak through
699
+ - Opt-in `allowedFileDirs` lockdown for user-controlled file inputs
700
+ - All error messages sanitized (no filesystem paths or secrets leak)
617
701
  - Async file I/O throughout (non-blocking)
618
702
  - Strict TypeScript with documented `any`-casts only at pdf-lib internal boundaries
703
+ - HTTPS-only fetch with private-IP / SSRF guard, including IPv6
704
+ - HTTP redirect chain re-validated against the same SSRF guard
705
+
706
+ See [SECURITY.md](SECURITY.md) for disclosure policy.
619
707
 
620
708
  ---
621
709
 
@@ -628,15 +716,11 @@ Highlights:
628
716
  | 8A–H | Annotations, forms, assembly, callouts, signatures, metadata, hyperlinks, inline formatting | ✅ |
629
717
  | 9A–C | Cryptographic signatures (PKCS#7), image floats, font subsetting | ✅ |
630
718
  | 10A–D | QR codes, barcodes, Vega-Lite charts, Markdown, templates | ✅ |
631
- | 11+ | Variable fonts, OpenType features, PDF/A, PDF/UA accessibility | 🔜 |
632
-
633
- See [docs/ROADMAP.md](docs/ROADMAP.md) for the full plan.
634
-
635
- ---
636
-
637
- ## Migration from pdfmake
719
+ | 11+ | Performance enhancements, hardening | |
720
+ | **0.9.0** | **CLI, pdfmake compat shim, GFM tables + task lists** | ✅ |
721
+ | Future | Variable fonts, OpenType features, PDF/A, PDF/UA accessibility | 🔜 |
638
722
 
639
- Coming from pdfmake? See the **[Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md)** — every common pdfmake pattern mapped to its pretext-pdf equivalent.
723
+ See [docs/ROADMAP.md](docs/ROADMAP.md).
640
724
 
641
725
  ---
642
726
 
@@ -644,6 +728,16 @@ Coming from pdfmake? See the **[Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md)
644
728
 
645
729
  See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
646
730
 
731
+ Useful commands:
732
+
733
+ ```bash
734
+ npm install # one-time setup
735
+ npm run build # tsc → dist/
736
+ npm run typecheck # tsc --noEmit
737
+ npm test # full suite
738
+ npm run example # run a sample render
739
+ ```
740
+
647
741
  ---
648
742
 
649
743
  ## License