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/CHANGELOG.md +43 -0
- package/README.md +363 -268
- package/dist/assets.d.ts +8 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +63 -16
- package/dist/assets.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +210 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +120 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +429 -0
- package/dist/compat.js.map +1 -0
- package/dist/markdown.d.ts +5 -3
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +55 -11
- package/dist/markdown.js.map +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +5 -2
- package/dist/templates.js.map +1 -1
- package/package.json +9 -2
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`.
|
|
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
|
[](https://www.npmjs.com/package/pretext-pdf)
|
|
8
9
|
[](https://www.npmjs.com/package/pretext-pdf)
|
|
9
10
|
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
|
|
10
11
|
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](#tests)
|
|
12
13
|
[](LICENSE)
|
|
14
|
+
[](#runtime-footprint)
|
|
13
15
|
|
|
14
|
-
**[Live demo](https://himaan1998y.github.io/pretext-pdf/)**
|
|
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
|
-
|
|
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
|
|
33
|
-
| MCP server
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
82
|
+
Optional peer dependencies — install only what you use:
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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: '
|
|
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
|
-
{
|
|
123
|
-
{
|
|
124
|
-
{
|
|
115
|
+
{ width: 200 },
|
|
116
|
+
{ width: 50, align: 'right' },
|
|
117
|
+
{ width: 100, align: 'right' },
|
|
125
118
|
],
|
|
126
119
|
rows: [
|
|
127
|
-
{
|
|
128
|
-
{
|
|
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
|
-
###
|
|
132
|
+
### CLI
|
|
139
133
|
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
160
|
+
Exit codes: `0` success, `1` user error (bad args, invalid JSON), `2` render error.
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
### Markdown
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
|---------|--------------|-------------|
|
|
158
|
-
| [](examples/showcase-invoice.ts) | [](examples/showcase-report.ts) | [](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
|
-
|
|
170
|
+
const md = `
|
|
171
|
+
# Q1 2026 Report
|
|
164
172
|
|
|
165
|
-
|
|
173
|
+
Revenue grew **18%** year-over-year.
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
const content = await markdownToContent(md, { codeFontFamily: 'Courier New' })
|
|
188
|
+
const pdf = await render({ content })
|
|
189
|
+
```
|
|
178
190
|
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
invoiceNumber: 'INV
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
### Migrating from pdfmake
|
|
214
216
|
|
|
215
|
-
|
|
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 {
|
|
220
|
+
import { fromPdfmake } from 'pretext-pdf/compat'
|
|
219
221
|
import { render } from 'pretext-pdf'
|
|
220
|
-
import { writeFileSync } from 'fs'
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
251
|
+
const pdf = await render(fromPdfmake(pdfmakeDoc))
|
|
252
|
+
```
|
|
229
253
|
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
294
|
+
## Built for AI agents
|
|
245
295
|
|
|
246
|
-
|
|
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
|
-
//
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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
|
|
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`
|
|
293
|
-
| `list` | Ordered/unordered,
|
|
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
|
|
300
|
-
| `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix,
|
|
301
|
-
| `chart` | Vega-Lite data visualisation
|
|
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
|
-
| `
|
|
304
|
-
| `
|
|
305
|
-
| `
|
|
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}}
|
|
316
|
-
| Per-section overrides | `doc.sections` | Different header/footer
|
|
317
|
-
| Metadata | `doc.metadata` | Title, author, subject, keywords,
|
|
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
|
-
|
|
|
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
|
|
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 },
|
|
351
|
-
header: { text: '
|
|
352
|
-
footer: { text: 'Confidential', align: 'center', color: '#
|
|
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
|
|
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: [
|
|
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
|
|
395
|
-
{ type: 'paragraph', text: 'Bold
|
|
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`**
|
|
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
|
-
|
|
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
|
-
#
|
|
474
|
-
npm run example:
|
|
475
|
-
npm run example:
|
|
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
|
|
480
|
-
npm run example:gst # India GST-compliant invoice
|
|
557
|
+
npm run example:callout # Callout boxes
|
|
481
558
|
```
|
|
482
559
|
|
|
483
|
-
All
|
|
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
|
|
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 '
|
|
503
|
-
//
|
|
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
|
-
|
|
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
|
|
602
|
+
### SVG / chart / qr-code / barcode rendering
|
|
530
603
|
|
|
531
|
-
Install `@napi-rs/canvas`
|
|
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
|
|
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
|
|
618
|
+
### Form fields not interactive
|
|
547
619
|
|
|
548
|
-
`flattenForms: true` bakes fields into static content — by design. Remove
|
|
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)
|
|
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
|
|
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
|
|
640
|
+
## Runtime footprint
|
|
641
|
+
|
|
642
|
+
Mandatory runtime dependencies:
|
|
565
643
|
|
|
566
|
-
-
|
|
567
|
-
-
|
|
568
|
-
-
|
|
569
|
-
-
|
|
570
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
668
|
+
For documents with 10,000+ elements, set `NODE_OPTIONS=--max-old-space-size=4096`.
|
|
587
669
|
|
|
588
670
|
---
|
|
589
671
|
|
|
590
|
-
##
|
|
672
|
+
## Tests
|
|
591
673
|
|
|
592
|
-
|
|
674
|
+
650+ tests across all phases with 100% pass rate:
|
|
593
675
|
|
|
594
676
|
```bash
|
|
595
|
-
npm test # Full suite (unit + e2e +
|
|
596
|
-
npm run test:unit # Validation, builder, rich-text
|
|
597
|
-
npm run test:e2e # End-to-end render
|
|
598
|
-
npm run test:
|
|
599
|
-
npm run test:
|
|
600
|
-
npm run test:
|
|
601
|
-
npm run test:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
614
|
-
-
|
|
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+ |
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|