pretext-pdf 0.8.2 → 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
@@ -1,27 +1,60 @@
1
1
  # pretext-pdf
2
2
 
3
- > **The PDF library AI agents speak natively.**
3
+ > **The PDF library AI agents speak natively — and humans love writing.**
4
4
  >
5
- > A `PdfDocument` is plain JSON. LLMs emit it in one shot — no codegen, no headless browser, no `eval`. Humans get a strict-typed, declarative API for invoices, reports, 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.
6
7
 
7
8
  [![npm version](https://img.shields.io/npm/v/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
8
9
  [![npm downloads](https://img.shields.io/npm/dw/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
9
10
  [![CI](https://github.com/Himaan1998Y/pretext-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/Himaan1998Y/pretext-pdf/actions)
10
11
  [![TypeScript](https://img.shields.io/badge/typescript-strict-blue)](https://www.typescriptlang.org/)
11
- [![Tests](https://img.shields.io/badge/tests-598-brightgreen)](#test-coverage)
12
+ [![Tests](https://img.shields.io/badge/tests-650%2B-brightgreen)](#tests)
12
13
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
+ [![Bundle](https://img.shields.io/badge/runtime%20deps-7-informational)](#runtime-footprint)
13
15
 
14
- **[Live demo](https://himaan1998y.github.io/pretext-pdf/)** edit JSON, render PDFs instantly. No install.
15
- **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** — drop-in MCP server for Claude / Cursor / Windsurf.
16
- **[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)**
17
17
 
18
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).*
19
19
 
20
20
  ---
21
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
+
22
55
  ## Why pretext-pdf
23
56
 
24
- 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.
25
58
 
26
59
  | | pdfmake / jsPDF / pdfkit | Puppeteer / Playwright | LaTeX / WeasyPrint | **pretext-pdf** |
27
60
  |---|---|---|---|---|
@@ -29,54 +62,12 @@ There are three established camps in JS PDF generation, and one gap. pretext-pdf
29
62
  | Pure ESM, runs in serverless | ✅ | ⚠️ painful in Lambda | ❌ | ✅ |
30
63
  | Professional typography (kerning, hyphenation, RTL/CJK) | ❌ | ✅ | ✅ | ✅ |
31
64
  | Declarative — describe the document, don't draw it | ⚠️ partial | ❌ | ❌ | ✅ |
32
- | **LLM emits a working document in one shot** | ❌ requires a code-execution loop | ❌ requires HTML+CSS knowledge | ❌ requires LaTeX knowledge | ✅ pure JSON |
33
- | MCP server available out of the box | ❌ | ❌ | ❌ | ✅ |
34
-
35
- **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.
36
-
37
- ---
38
-
39
- ## Built for AI agents
40
-
41
- 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.
42
-
43
- ### Drop into Claude / Cursor / Windsurf via MCP
44
-
45
- ```json
46
- {
47
- "mcpServers": {
48
- "pretext-pdf": {
49
- "command": "npx",
50
- "args": ["-y", "pretext-pdf-mcp"]
51
- }
52
- }
53
- }
54
- ```
55
-
56
- 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.
57
-
58
- ### Or call from any agent framework
59
-
60
- ```typescript
61
- 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()` |
62
69
 
63
- // Whatever produced this JSON Claude, GPT, a workflow node, a form submissionworks the same
64
- const pdf = await render({
65
- metadata: { title: 'AI-generated quarterly report' },
66
- content: [
67
- { type: 'heading', level: 1, text: 'Q1 2026 Summary' },
68
- { type: 'paragraph', text: 'Revenue grew 18% YoY.' },
69
- { type: 'table', columns: [...], rows: [...] },
70
- ],
71
- })
72
- ```
73
-
74
- ### Why JSON-first matters for agents
75
-
76
- - **No code execution loop.** The model returns JSON; you call `render()`. No sandbox, no `vm`, no Vercel Sandbox roundtrip.
77
- - **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.
78
- - **Self-correcting errors.** Every failure throws `PretextPdfError` with a typed `code`. Feed it back to the model and it fixes itself.
79
- - **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.
80
71
 
81
72
  ---
82
73
 
@@ -88,23 +79,25 @@ npm install pretext-pdf
88
79
 
89
80
  > **ESM only** — use `import`, not `require`. Requires Node.js ≥ 18.
90
81
 
91
- Optional peer dependencies — install only what you need:
82
+ Optional peer dependencies — install only what you use:
92
83
 
93
- ```bash
94
- npm install @napi-rs/canvas # SVG / qr-code / barcode / chart elements
95
- npm install qrcode # qr-code element
96
- npm install bwip-js # barcode element
97
- npm install vega vega-lite # chart element (Vega-Lite specs → vector SVG)
98
- npm install marked # pretext-pdf/markdown entry point
99
- npm install @signpdf/signpdf # PKCS#7 cryptographic signing
100
- ```
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 |
101
92
 
102
- > **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.
103
94
 
104
95
  ---
105
96
 
106
97
  ## Quick start
107
98
 
99
+ ### Library API
100
+
108
101
  ```typescript
109
102
  import { render } from 'pretext-pdf'
110
103
  import { writeFileSync } from 'fs'
@@ -112,20 +105,21 @@ import { writeFileSync } from 'fs'
112
105
  const pdf = await render({
113
106
  pageSize: 'A4',
114
107
  margins: { top: 40, bottom: 40, left: 50, right: 50 },
115
- metadata: { title: 'My Invoice', author: 'Acme Corp' },
108
+ metadata: { title: 'Invoice #001', author: 'Acme Corp' },
116
109
  content: [
117
110
  { type: 'heading', level: 1, text: 'Invoice #12345' },
118
111
  { type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
119
112
  {
120
113
  type: 'table',
121
114
  columns: [
122
- { name: 'Item', width: 200 },
123
- { name: 'Qty', width: 50, align: 'right' },
124
- { name: 'Price', width: 100, align: 'right' },
115
+ { width: 200 },
116
+ { width: 50, align: 'right' },
117
+ { width: 100, align: 'right' },
125
118
  ],
126
119
  rows: [
127
- { Item: 'Professional Services', Qty: '10', Price: '$1,000' },
128
- { 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' }] },
129
123
  ],
130
124
  },
131
125
  { type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
@@ -135,147 +129,196 @@ const pdf = await render({
135
129
  writeFileSync('invoice.pdf', pdf)
136
130
  ```
137
131
 
138
- ### Builder API (fluent style)
132
+ ### CLI
139
133
 
140
- ```typescript
141
- 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.
142
135
 
143
- const pdf = await createPdf({ pageSize: 'A4' })
144
- .addHeading('My Report', 1)
145
- .addText('Fluent chainable API.')
146
- .addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
147
- .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
148
149
  ```
149
150
 
150
- ---
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 |
151
159
 
152
- ## Output samples
160
+ Exit codes: `0` success, `1` user error (bad args, invalid JSON), `2` render error.
153
161
 
154
- Real documents generated with pretext-pdf:
162
+ ### Markdown
155
163
 
156
- | Invoice | Market Report | Resume / CV |
157
- |---------|--------------|-------------|
158
- | [![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) |
159
- | [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.
160
165
 
161
- ---
166
+ ```typescript
167
+ import { markdownToContent } from 'pretext-pdf/markdown'
168
+ import { render } from 'pretext-pdf'
162
169
 
163
- ## What's in v0.8.0
170
+ const md = `
171
+ # Q1 2026 Report
164
172
 
165
- Five new capabilities, all behind optional peer dependencies (zero extra weight if unused):
173
+ Revenue grew **18%** year-over-year.
166
174
 
167
- - **`qr-code`** scannable QR codes for UPI payments, URLs, vCards. Requires `qrcode`.
168
- - **`barcode`** — 100+ symbologies (EAN-13, Code128, PDF417, DataMatrix…). Requires `bwip-js`.
169
- - **`chart`** embed Vega-Lite specs as crisp vector SVG. Requires `vega` + `vega-lite`.
170
- - **`pretext-pdf/markdown`** convert any Markdown string to `ContentElement[]` in one call. Requires `marked`.
171
- - **`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 |
172
179
 
173
- See [CHANGELOG.md](CHANGELOG.md) for the full history.
180
+ - [x] Cloud expansion launched
181
+ - [x] Enterprise pipeline doubled
182
+ - [ ] APAC region opening Q2
174
183
 
175
- ---
184
+ > All figures in USD millions.
185
+ `
176
186
 
177
- ## India / GST invoicing
187
+ const content = await markdownToContent(md, { codeFontFamily: 'Courier New' })
188
+ const pdf = await render({ content })
189
+ ```
178
190
 
179
- 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.
180
192
 
181
- - **₹ symbol** renders correctly (bundled Inter font includes the Rupee glyph)
182
- - **Indian number formatting** — helper for 1,00,000 notation (not 100,000)
183
- - **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) table layouts
184
- - **Amount in words** — Indian numbering system (Lakh/Crore)
185
- - **SAC/HSN codes** — column support in line-item tables
193
+ ### Templates
186
194
 
187
- Use the `createGstInvoice` template for a complete GST-compliant invoice in one call:
195
+ Pre-built zero-dependency template functions:
188
196
 
189
197
  ```typescript
190
- import { createGstInvoice } from 'pretext-pdf/templates'
198
+ import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
191
199
  import { render } from 'pretext-pdf'
192
200
 
193
- const content = createGstInvoice({
194
- supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
195
- buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
196
- invoiceNumber: 'INV/2026-27/001',
197
- invoiceDate: '20 Apr 2026',
198
- placeOfSupply: 'Maharashtra (27)',
199
- items: [
200
- { description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
201
- ],
202
- isInterState: true, // auto-detected from state fields if omitted
203
- qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
204
- 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',
205
209
  })
206
210
  const pdf = await render({ content })
207
211
  ```
208
212
 
209
- See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for the raw element approach.
210
-
211
- ---
213
+ Available: `createInvoice` (any currency), `createGstInvoice` (India GST/IGST/CGST+SGST + UPI QR + amount-in-words), `createReport` (with optional TOC).
212
214
 
213
- ## Markdown PDF (`pretext-pdf/markdown`)
215
+ ### Migrating from pdfmake
214
216
 
215
- 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.
216
218
 
217
219
  ```typescript
218
- import { markdownToContent } from 'pretext-pdf/markdown'
220
+ import { fromPdfmake } from 'pretext-pdf/compat'
219
221
  import { render } from 'pretext-pdf'
220
- import { writeFileSync } from 'fs'
221
222
 
222
- const md = `
223
- # Q1 2026 Report
224
-
225
- 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
+ }
226
250
 
227
- - Cloud services (+32%)
228
- - Enterprise licenses (+12%)
251
+ const pdf = await render(fromPdfmake(pdfmakeDoc))
252
+ ```
229
253
 
230
- > All figures are in USD millions.
231
- `
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:
232
278
 
233
- const content = await markdownToContent(md, {
234
- codeFontFamily: 'Courier New', // enables fenced code block rendering
235
- })
236
- const pdf = await render({ content })
237
- writeFileSync('report.pdf', pdf)
279
+ ```json
280
+ {
281
+ "mcpServers": {
282
+ "pretext-pdf": {
283
+ "command": "npx",
284
+ "args": ["-y", "pretext-pdf-mcp"]
285
+ }
286
+ }
287
+ }
238
288
  ```
239
289
 
240
- 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).
241
291
 
242
292
  ---
243
293
 
244
- ## Invoice & report templates (`pretext-pdf/templates`)
294
+ ## Built for AI agents
245
295
 
246
- 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.
247
297
 
248
298
  ```typescript
249
- import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
250
299
  import { render } from 'pretext-pdf'
251
300
 
252
- // Generic invoice (any currency)
253
- const invoiceContent = createInvoice({
254
- from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
255
- to: { name: 'Client Ltd', address: '456 Oak Ave' },
256
- invoiceNumber: 'INV-2026-001', date: '2026-04-20',
257
- items: [{ description: 'Consulting', quantity: 10, unitPrice: 150 }],
258
- currency: '$', taxRate: 10, taxLabel: 'GST',
259
- qrData: 'upi://pay?pa=acme@bank&am=1650',
260
- })
261
-
262
- // Research report with optional TOC
263
- const reportContent = createReport({
264
- title: 'Annual Performance Report',
265
- author: 'Finance Team', date: 'April 2026',
266
- abstract: 'Revenue grew 18% YoY across all segments.',
267
- includeTableOfContents: true,
268
- sections: [
269
- { 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: [/* ... */] },
270
308
  ],
271
309
  })
272
-
273
- const pdf = await render({ content: reportContent })
274
310
  ```
275
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
+
276
319
  ---
277
320
 
278
- ## Element type reference
321
+ ## Element catalog
279
322
 
280
323
  ```
281
324
  paragraph heading(1-4) spacer hr page-break
@@ -287,42 +330,39 @@ toc qr-code barcode chart footnote-def
287
330
  | Element | What it does |
288
331
  | --- | --- |
289
332
  | `paragraph` | Text block — font, size, color, align, background, letterSpacing, smallCaps, tabularNumbers, multi-column (`columns` + `columnGap`), RTL (`dir`) |
290
- | `heading` | H1–H4 with bookmarks, URL links, internal anchors, tabularNumbers, RTL (`dir`) |
291
- | `table` | Fixed/proportional columns, colspan, rowspan, repeating headers across page breaks |
292
- | `image` | PNG/JPG/WebP with sizing, alignment, float left/right with `floatText` or rich `floatSpans` (mixed-format caption) |
293
- | `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'` |
294
337
  | `code` | Monospace block with background and padding |
295
338
  | `blockquote` | Left border + background |
296
339
  | `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
297
340
  | `svg` | Embedded SVG graphics with auto-sizing from viewBox |
298
341
  | `toc` | Auto-generated table of contents with accurate page numbers (two-pass) |
299
- | `qr-code` | Scannable QR code — UPI payment links, URLs, vCards. `data`, `size`, `errorCorrectionLevel`, `foreground`/`background` color. Requires `qrcode` peer dep. |
300
- | `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix, and more via `symbology` field. Requires `bwip-js` peer dep. |
301
- | `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`. |
302
345
  | `comment` | PDF sticky-note annotation (visible in Acrobat/Preview sidebar) |
303
- | `hr` | Horizontal rule |
304
- | `spacer` | Fixed-height gap |
305
- | `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 |
306
350
 
307
351
  ### Document-level features
308
352
 
309
353
  | Feature | Config key | Notes |
310
354
  | --- | --- | --- |
311
355
  | Watermarks | `doc.watermark` | Text or image, opacity, rotation |
312
- | 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` |
313
358
  | PDF Bookmarks | `doc.bookmarks` | Auto-generated from headings |
314
- | Hyphenation | `doc.hyphenation` | Liang's algorithm, `language: 'en-us'` |
315
- | Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}`, `{{author}}` tokens |
316
- | Per-section overrides | `doc.sections` | Different header/footer/margins per page range |
317
- | 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 |
318
363
  | Hyperlinks | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` | External, mailto, internal anchors |
319
- | Inline formatting | `span.verticalAlign: 'superscript'\|'subscript'`, `paragraph.letterSpacing`, `heading.smallCaps` | |
320
- | Sticky notes | `{ type: 'comment', contents: '...' }`, `paragraph.annotation` | |
321
364
  | Document assembly | `merge(pdfs)`, `assemble(parts)` | Combine pre-rendered + freshly rendered |
322
- | Interactive forms | `{ type: 'form-field', fieldType: 'text'\|'checkbox'\|'radio'\|'dropdown'\|'button' }`, `doc.flattenForms` | |
323
- | Cryptographic signing | `doc.signature: { p12, passphrase, signerName, reason, location }` | PKCS#7 via optional `@signpdf/signpdf` |
324
- | Visual signature placeholder | `doc.signature: { signerName, reason, location, x, y, page }` | |
325
- | Callout boxes | `{ type: 'callout', content, style: 'info'\|'warning'\|'tip'\|'note', title }` | |
365
+ | Path-traversal lockdown | `doc.allowedFileDirs` | Restrict file-source reads to listed dirs |
326
366
 
327
367
  ---
328
368
 
@@ -334,22 +374,17 @@ toc qr-code barcode chart footnote-def
334
374
  import { render } from 'pretext-pdf'
335
375
 
336
376
  const pdf = await render({
337
- pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | [w, h]
377
+ pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | 'Tabloid' | [w, h]
338
378
  margins: { top: 72, bottom: 72, left: 72, right: 72 },
339
- defaultFont: 'Inter', // Inter 400 bundled; load others via doc.fonts
379
+ defaultFont: 'Inter', // Inter 400/700 bundled
340
380
  defaultFontSize: 12,
341
- metadata: {
342
- title: 'Document Title',
343
- author: 'Author Name',
344
- subject: 'Description',
345
- keywords: ['pdf', 'report'],
346
- },
381
+ metadata: { title: '...', author: '...', keywords: ['pdf'] },
347
382
  watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
348
383
  encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
349
384
  bookmarks: { minLevel: 1, maxLevel: 3 },
350
- hyphenation: { language: 'en-us', minWordLength: 6 }, // ⚠️ Use lowercase: 'en-us' not 'en-US' — matches the npm package name hyphenation.en-us
351
- header: { text: 'My Document — {{pageNumber}} of {{totalPages}}', align: 'right' },
352
- 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' },
353
388
  content: [ /* ContentElement[] */ ],
354
389
  })
355
390
  ```
@@ -360,24 +395,74 @@ Combine pre-rendered PDFs:
360
395
 
361
396
  ```typescript
362
397
  import { merge } from 'pretext-pdf'
363
-
364
398
  const combined = await merge([coverPdf, bodyPdf, appendixPdf])
365
399
  ```
366
400
 
367
401
  ### `assemble(parts): Promise<Uint8Array>`
368
402
 
369
- Mix new document configs with existing PDFs:
403
+ Mix new docs with existing PDFs:
370
404
 
371
405
  ```typescript
372
406
  import { assemble } from 'pretext-pdf'
373
407
 
374
408
  const report = await assemble([
375
409
  { pdf: existingCoverPdf },
376
- { doc: { content: [...] } }, // rendered fresh
410
+ { doc: { content: [/* fresh */] } },
377
411
  { pdf: standardTermsPdf },
378
412
  ])
379
413
  ```
380
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
+
381
466
  ---
382
467
 
383
468
  ## Custom fonts
@@ -391,13 +476,13 @@ const pdf = await render({
391
476
  ],
392
477
  defaultFont: 'Roboto',
393
478
  content: [
394
- { type: 'paragraph', text: 'Uses Roboto font' },
395
- { type: 'paragraph', text: 'Bold text', fontWeight: 700 },
479
+ { type: 'paragraph', text: 'Uses Roboto' },
480
+ { type: 'paragraph', text: 'Bold', fontWeight: 700 },
396
481
  ],
397
482
  })
398
483
  ```
399
484
 
400
- > **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.
401
486
 
402
487
  ---
403
488
 
@@ -427,7 +512,7 @@ const pdf = await render({
427
512
 
428
513
  ## Footnotes
429
514
 
430
- Use `createFootnoteSet()` to generate matched reference/definition pairs with guaranteed unique IDs:
515
+ `createFootnoteSet()` produces matched reference/definition pairs with guaranteed unique IDs:
431
516
 
432
517
  ```typescript
433
518
  import { render, createFootnoteSet } from 'pretext-pdf'
@@ -456,37 +541,29 @@ await render({
456
541
 
457
542
  ## Examples
458
543
 
459
- Run working examples from the `examples/` directory:
460
-
461
544
  ```bash
462
- # v0.8.0 new element examples (install optional deps first)
463
- # npm install qrcode bwip-js vega vega-lite marked
464
-
465
- # Phase 7 examples
466
545
  npm run example # Basic invoice
546
+ npm run example:gst # India GST invoice
467
547
  npm run example:watermark # Text/image watermarks
468
548
  npm run example:bookmarks # PDF outline/bookmarks
469
549
  npm run example:toc # Auto table of contents
470
550
  npm run example:rtl # Arabic/Hebrew RTL text
471
551
  npm run example:encryption # Password-protected PDF
472
-
473
- # Phase 8 examples
474
- npm run example:hyperlinks # External links, email links, internal anchors
475
- npm run example:annotations # Sticky notes on elements
476
- npm run example:assembly # Merge and assemble multiple PDFs
477
- 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
478
556
  npm run example:forms # Interactive form fields
479
- npm run example:callout # Callout boxes (info, warning, tip, note)
480
- npm run example:gst # India GST-compliant invoice
557
+ npm run example:callout # Callout boxes
481
558
  ```
482
559
 
483
- All examples write output to `output/*.pdf`.
560
+ All write to `output/*.pdf`.
484
561
 
485
562
  ---
486
563
 
487
564
  ## Error handling
488
565
 
489
- 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`:
490
567
 
491
568
  ```typescript
492
569
  import { render, PretextPdfError } from 'pretext-pdf'
@@ -499,36 +576,32 @@ try {
499
576
  case 'VALIDATION_ERROR': // Invalid config
500
577
  case 'FONT_LOAD_FAILED': // Font file not found
501
578
  case 'IMAGE_TOO_TALL': // Image doesn't fit on page
502
- case 'ASSEMBLY_EMPTY': // merge/assemble called with empty array
503
- // ... 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
504
582
  }
505
583
  }
506
584
  }
507
585
  ```
508
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
+
509
589
  ---
510
590
 
511
591
  ## Troubleshooting
512
592
 
513
593
  ### Hyphenation language not found
514
594
 
515
- ```
516
- UNSUPPORTED_LANGUAGE: Language 'en-US' not supported
517
- ```
518
-
519
595
  Use **lowercase** language codes that match the npm package name:
520
596
 
521
597
  ```typescript
522
- // Wrong 'en-US' fails on Linux (case-sensitive filesystem)
523
- hyphenation: { language: 'en-US' }
524
-
525
- // Correct — matches 'hyphenation.en-us' package name
526
- hyphenation: { language: 'en-us' }
598
+ hyphenation: { language: 'en-us' } //
599
+ hyphenation: { language: 'en-US' } // ❌ fails on Linux (case-sensitive FS)
527
600
  ```
528
601
 
529
- ### SVG rendering requires optional dependency
602
+ ### SVG / chart / qr-code / barcode rendering
530
603
 
531
- Install `@napi-rs/canvas` for SVG / chart / qr-code / barcode support:
604
+ Install `@napi-rs/canvas` (Node only browsers use native `OffscreenCanvas`):
532
605
 
533
606
  ```bash
534
607
  npm install @napi-rs/canvas
@@ -536,16 +609,19 @@ npm install @napi-rs/canvas
536
609
 
537
610
  ### PDF is blank or too small
538
611
 
539
- 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:
540
613
 
541
614
  ```typescript
542
- // For narrow pages, reduce margins:
543
615
  margins: { top: 36, bottom: 36, left: 36, right: 36 }
544
616
  ```
545
617
 
546
- ### Form fields not interactive after `flattenForms`
618
+ ### Form fields not interactive
547
619
 
548
- `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.
549
625
 
550
626
  ---
551
627
 
@@ -555,66 +631,79 @@ What pretext-pdf is **not** trying to be — pick a different tool for these:
555
631
 
556
632
  - **Editing or parsing existing PDFs** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdf-parse`](https://www.npmjs.com/package/pdf-parse)
557
633
  - **Filling existing PDF form templates** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdftk`](https://www.pdflabs.com/tools/pdftk-server/)
558
- - **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)
559
635
  - **PDF/A archival, PDF/UA accessibility tagging** → not yet
560
- - **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
561
637
 
562
638
  ---
563
639
 
564
- ## Runtime requirements
640
+ ## Runtime footprint
641
+
642
+ Mandatory runtime dependencies:
565
643
 
566
- - **Node.js ≥ 18** with `@napi-rs/canvas` peer dep (lazy-loaded only required when you use SVG/chart/QR/barcode elements)
567
- - **`Intl.Segmenter`** (built-in on Node 18+ and all modern browsers)
568
- - **Browser support**works directly in modern browsers; bring your own font bytes
569
- - **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.
570
- - **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.
571
653
 
572
654
  ---
573
655
 
574
656
  ## Performance
575
657
 
576
- 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.
577
659
 
578
660
  | Document | Render time | PDF size |
579
661
  | --- | --- | --- |
580
662
  | 1 page (heading + paragraph + list) | ~220 ms | ~45 KB |
581
- | 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
582
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 |
583
665
 
584
- **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.
585
667
 
586
- 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`.
587
669
 
588
670
  ---
589
671
 
590
- ## Test coverage
672
+ ## Tests
591
673
 
592
- 598+ tests across all phases with 100% pass rate:
674
+ 650+ tests across all phases with 100% pass rate:
593
675
 
594
676
  ```bash
595
- npm test # Full suite (unit + e2e + all phases including v0.8.0)
596
- npm run test:unit # Validation, builder, rich-text unit tests
597
- npm run test:e2e # End-to-end render tests
598
- npm run test:10a # QR code + barcode tests
599
- npm run test:10b # Vega-Lite chart tests
600
- npm run test:10c # Markdown converter tests
601
- npm run test:10d # Template function tests
602
- 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
603
684
  ```
604
685
 
605
- **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.
606
687
 
607
688
  ---
608
689
 
609
690
  ## Security
610
691
 
611
- 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:
612
698
 
613
- Highlights:
614
- - Zero known path-traversal vulnerabilities; opt-in `allowedFileDirs` lockdown for user-controlled inputs
615
- - 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)
616
701
  - Async file I/O throughout (non-blocking)
617
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.
618
707
 
619
708
  ---
620
709
 
@@ -627,15 +716,11 @@ Highlights:
627
716
  | 8A–H | Annotations, forms, assembly, callouts, signatures, metadata, hyperlinks, inline formatting | ✅ |
628
717
  | 9A–C | Cryptographic signatures (PKCS#7), image floats, font subsetting | ✅ |
629
718
  | 10A–D | QR codes, barcodes, Vega-Lite charts, Markdown, templates | ✅ |
630
- | 11+ | Variable fonts, OpenType features, PDF/A, PDF/UA accessibility | 🔜 |
631
-
632
- See [docs/ROADMAP.md](docs/ROADMAP.md) for the full plan.
633
-
634
- ---
635
-
636
- ## 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 | 🔜 |
637
722
 
638
- 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).
639
724
 
640
725
  ---
641
726
 
@@ -643,6 +728,16 @@ Coming from pdfmake? See the **[Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md)
643
728
 
644
729
  See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
645
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
+
646
741
  ---
647
742
 
648
743
  ## License