pretext-pdf 0.8.0 → 0.8.2

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,749 +1,658 @@
1
- # pretext-pdf
2
-
3
- > **Declarative JSON PDF generation with professional typography.**
4
- >
5
- > Build sophisticated, multi-page documents with precise text layout, international support, and zero browser overhead.
6
-
7
- [![npm version](https://img.shields.io/npm/v/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
8
- [![npm downloads](https://img.shields.io/npm/dw/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
9
- [![CI](https://github.com/Himaan1998Y/pretext-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/Himaan1998Y/pretext-pdf/actions)
10
- [![TypeScript](https://img.shields.io/badge/typescript-strict-blue)](https://www.typescriptlang.org/)
11
- [![Type Safety](https://img.shields.io/badge/type--safety-documented--casts-blueviolet)](#type-safety-v046)
12
- [![Tests](https://img.shields.io/badge/tests-598-brightgreen)](#test-coverage)
13
- [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
-
15
- **[Try the Live Demo](https://stackblitz.com/github/Himaan1998Y/pretext-pdf/tree/master/demo/stackblitz?file=public%2Findex.html)** — edit JSON, generate PDFs instantly. No install required.
16
-
17
- **Coming from pdfmake?** See the [Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md) — maps every pdfmake pattern to its pretext-pdf equivalent.
18
-
19
- ---
20
-
21
- ## v0.8.0 — QR Codes, Barcodes, Charts, Markdown, Templates
22
-
23
- Five new capabilities added, all via optional peer dependencies (zero extra weight if unused):
24
-
25
- - **`qr-code` element** — embed scannable QR codes (UPI payments, URLs, vCards). Requires `qrcode`.
26
- - **`barcode` element** 100+ symbologies (EAN-13, Code128, PDF417, QR, DataMatrix…). Requires `bwip-js`.
27
- - **`chart` element** — embed Vega-Lite data visualisations as crisp vector SVG. Requires `vega` + `vega-lite`.
28
- - **`pretext-pdf/markdown`** entry point convert any Markdown string to `ContentElement[]` in one call. Requires `marked`.
29
- - **`pretext-pdf/templates`** entry point zero-dep helper functions: `createInvoice`, `createGstInvoice` (India GST / IGST / CGST+SGST), `createReport`.
30
-
31
- Install only what you need:
32
-
33
- ```bash
34
- npm install pretext-pdf@^0.8.0
35
- npm install qrcode # for qr-code element
36
- npm install bwip-js # for barcode element
37
- npm install vega vega-lite # for chart element
38
- npm install marked # for pretext-pdf/markdown
39
- ```
40
-
41
- > **ESM only** pretext-pdf is a pure ESM package (`"type": "module"`). Use `import`, not `require`.
42
-
43
- ---
44
-
45
- ## v0.4.6 — Security & Quality Hardening
46
-
47
- All 41 issues from comprehensive April 2026 security audit resolved:
48
-
49
- - **Phase 1**: Security hardening (path traversal protection, async file I/O, explicit error handling)
50
- - **Phase 2**: Type safety (reduced and documented any-casts, proper module typing, strict inference)
51
- - **Phase 3**: Test coverage (false-positive fixes, boundary case validation)
52
- - **Phase 4**: Code quality (silent failures → explicit errors, improved decoupling)
53
-
54
- **Result**: 188+ comprehensive tests, 100% pass rate, production-ready reliability.
55
-
56
- ---
57
-
58
- ## Why pretext-pdf?
59
-
60
- | | pdfmake | Puppeteer | **pretext-pdf** |
61
- |---|---|---|---|
62
- | Easy declarative API | ✅ | ❌ | ✅ |
63
- | Professional typography | | | |
64
- | Lightweight (no browser) | ✅ | ❌ | ✅ |
65
- | International text (RTL/CJK) | | ✅ | ✅ |
66
- | Pure Node.js | ✅ | ❌ | ✅ |
67
- | Hyperlinks + annotations | | | ✅ |
68
- | Document assembly | | | ✅ |
69
-
70
- ### Powered by [pretext](https://github.com/chenglou/pretext)
71
-
72
- Pretext is a precision text layout engine by [Cheng Lou](https://github.com/chenglou) (React core team, Midjourney).
73
-
74
- ```
75
- JSON descriptor → pretext layout → pdf-lib renderer → PDF bytes
76
- (kerning, (annotations,
77
- hyphenation, encryption,
78
- RTL, CJK) hyperlinks)
79
- ```
80
-
81
- ---
82
-
83
- ## Output Samples
84
-
85
- Real documents generated with pretext-pdf:
86
-
87
- | Invoice | Market Report | Resume / CV |
88
- |---------|--------------|-------------|
89
- | [![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) |
90
- | [View source](examples/showcase-invoice.ts) | [View source](examples/showcase-report.ts) | [View source](examples/showcase-resume.ts) |
91
-
92
- ---
93
-
94
- ## Install
95
-
96
- ```bash
97
- npm install pretext-pdf@^0.8.0
98
- ```
99
-
100
- > **ESM only** — use `import`, not `require`.
101
-
102
- Optional peer dependencies — install only what you need:
103
-
104
- ```bash
105
- npm install @napi-rs/canvas # SVG elements (qr-code / barcode / chart all require this too)
106
- npm install qrcode # qr-code element
107
- npm install bwip-js # barcode element
108
- npm install vega vega-lite # chart element
109
- npm install marked # pretext-pdf/markdown entry point
110
- npm install @signpdf/signpdf # PKCS#7 cryptographic signing
111
- ```
112
-
113
- > **Encryption is built-in since v0.4.0** — no extra install needed. Just add `encryption` to your document config.
114
-
115
- ---
116
-
117
- ## Quick Start
118
-
119
- ```typescript
120
- import { render } from 'pretext-pdf'
121
- import { writeFileSync } from 'fs'
122
-
123
- const pdf = await render({
124
- pageSize: 'A4',
125
- margins: { top: 40, bottom: 40, left: 50, right: 50 },
126
- metadata: { title: 'My Invoice', author: 'Acme Corp' },
127
- content: [
128
- { type: 'heading', level: 1, text: 'Invoice #12345' },
129
- { type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
130
- {
131
- type: 'table',
132
- columns: [
133
- { name: 'Item', width: 200 },
134
- { name: 'Qty', width: 50, align: 'right' },
135
- { name: 'Price', width: 100, align: 'right' },
136
- ],
137
- rows: [
138
- { Item: 'Professional Services', Qty: '10', Price: '$1,000' },
139
- { Item: 'Hosting (annual)', Qty: '1', Price: '$500' },
140
- ],
141
- },
142
- { type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
143
- ],
144
- })
145
-
146
- writeFileSync('invoice.pdf', pdf)
147
- ```
148
-
149
- ### Builder API
150
-
151
- ```typescript
152
- import { createPdf } from 'pretext-pdf'
153
-
154
- const pdf = await createPdf({ pageSize: 'A4' })
155
- .addHeading('My Report', 1)
156
- .addText('Fluent chainable API.')
157
- .addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
158
- .build()
159
- ```
160
-
161
- ---
162
-
163
- ## Agent / AI Integration
164
-
165
- pretext-pdf works great as a tool for AI agents generating PDFs on demand.
166
-
167
- ### MCP Server (Claude Desktop, Cursor, Windsurf)
168
-
169
- Use [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp) to call pretext-pdf directly from any AI agent:
170
-
171
- ```json
172
- {
173
- "mcpServers": {
174
- "pretext-pdf": {
175
- "command": "npx",
176
- "args": ["-y", "pretext-pdf-mcp"]
177
- }
178
- }
179
- }
180
- ```
181
-
182
- Tools available: `generate_pdf`, `generate_invoice`, `generate_report`, `generate_from_markdown`, `list_element_types`
183
-
184
- ### Quick pattern for LLMs
185
-
186
- ```typescript
187
- import { render } from 'pretext-pdf'
188
-
189
- // Every PdfDocument is a plain JSON object — perfect for AI generation
190
- const pdf = await render({
191
- metadata: { title: 'AI-Generated Report' },
192
- content: [
193
- { type: 'heading', level: 1, text: 'Summary' },
194
- { type: 'paragraph', text: 'Generated content here.' },
195
- // ... AI fills this array
196
- ]
197
- })
198
- ```
199
-
200
- ### Key facts for AI agents
201
-
202
- - `content` is an array of typed elements — each has a `type` field
203
- - All fields are optional except `type` and element-specific required fields (e.g. `text`, `level`)
204
- - Errors are typed: `err.code` tells you exactly what went wrong
205
- - `render()` is fully async, safe to `await` in any context
206
- - Works in Node.js 18+ and modern browsers (with `@napi-rs/canvas` for SVG)
207
-
208
- ### Element type reference (quick)
209
-
210
- ```
211
- paragraph heading(1-4) spacer hr page-break
212
- table image svg list code
213
- blockquote rich-paragraph callout comment form-field
214
- toc qr-code barcode chart
215
- ```
216
-
217
- ---
218
-
219
- ## India / GST Invoicing
220
-
221
- pretext-pdf has built-in support for Indian invoice requirements:
222
-
223
- - **₹ symbol** renders correctly (bundled Inter font includes the Rupee glyph)
224
- - **Indian number formatting** — helper for 1,00,000 notation (not 100,000)
225
- - **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) table layouts
226
- - **Amount in words** — Indian numbering system (Lakh/Crore)
227
- - **SAC/HSN codes** — column support in line-item tables
228
-
229
- Use the `createGstInvoice` template for a complete GST-compliant invoice in one function call:
230
-
231
- ```typescript
232
- import { createGstInvoice } from 'pretext-pdf/templates'
233
- import { render } from 'pretext-pdf'
234
-
235
- const content = createGstInvoice({
236
- supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
237
- buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
238
- invoiceNumber: 'INV/2026-27/001',
239
- invoiceDate: '20 Apr 2026',
240
- placeOfSupply: 'Maharashtra (27)',
241
- items: [
242
- { description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
243
- ],
244
- isInterState: true, // auto-detected from state fields if omitted
245
- qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
246
- bankName: 'HDFC Bank', accountNumber: '501001234567', ifscCode: 'HDFC0001234',
247
- })
248
- const pdf = await render({ content })
249
- ```
250
-
251
- See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for the raw element approach.
252
-
253
- ---
254
-
255
- ## Markdown PDF (`pretext-pdf/markdown`)
256
-
257
- Convert any Markdown string to a `pretext-pdf` document in one call. Requires `marked` peer dep.
258
-
259
- ```typescript
260
- import { markdownToContent } from 'pretext-pdf/markdown'
261
- import { render } from 'pretext-pdf'
262
- import { writeFileSync } from 'fs'
263
-
264
- const md = `
265
- # Q1 2026 Report
266
-
267
- Revenue grew **18%** year-over-year, driven by:
268
-
269
- - Cloud services (+32%)
270
- - Enterprise licenses (+12%)
271
-
272
- > All figures are in USD millions.
273
- `
274
-
275
- const content = await markdownToContent(md, {
276
- codeFontFamily: 'Courier New', // enables fenced code block rendering
277
- })
278
- const pdf = await render({ content })
279
- writeFileSync('report.pdf', pdf)
280
- ```
281
-
282
- Supported Markdown: headings h1–h4, bold, italic, strikethrough, inline code, links, ordered/unordered lists (2 levels), fenced code blocks, blockquotes, horizontal rules.
283
-
284
- ---
285
-
286
- ## Invoice & Report Templates (`pretext-pdf/templates`)
287
-
288
- Pre-built zero-dependency template functions that generate `ContentElement[]` arrays:
289
-
290
- ```typescript
291
- import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
292
- import { render } from 'pretext-pdf'
293
-
294
- // Generic invoice (any currency)
295
- const invoiceContent = createInvoice({
296
- from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
297
- to: { name: 'Client Ltd', address: '456 Oak Ave' },
298
- invoiceNumber: 'INV-2026-001', date: '2026-04-20',
299
- items: [{ description: 'Consulting', quantity: 10, unitPrice: 150 }],
300
- currency: '$', taxRate: 10, taxLabel: 'GST',
301
- qrData: 'upi://pay?pa=acme@bank&am=1650',
302
- })
303
-
304
- // Research report with optional TOC
305
- const reportContent = createReport({
306
- title: 'Annual Performance Report',
307
- author: 'Finance Team', date: 'April 2026',
308
- abstract: 'Revenue grew 18% YoY across all segments.',
309
- includeTableOfContents: true,
310
- sections: [
311
- { title: 'Revenue', paragraphs: ['Cloud +32%, Enterprise +12%.'], bullets: ['SaaS: $2.8M', 'Services: $1.1M'] },
312
- ],
313
- })
314
-
315
- const pdf = await render({ content: reportContent })
316
- ```
317
-
318
- ---
319
-
320
- ## Features
321
-
322
- ### Security & Reliability
323
-
324
- - **Type-safe architecture** strict TypeScript inference, documented casts for pdf-lib internals
325
- - **Cryptographically signed PDFs** PKCS#7 signing support (Phase 3)
326
- - ✅ **Path traversal protection** — Secure file operations with validated paths
327
- - ✅ **Error sanitization** — No sensitive data in error messages
328
- - ✅ **Async-safe I/O** — Non-blocking file operations throughout
329
- - **Comprehensive test coverage** — 188+ tests with 100% pass rate
330
- - ✅ **No hardcoded secrets** — Environment-based configuration
331
-
332
- ### Element Types
333
-
334
- | Element | What it does |
335
- | --- | --- |
336
- | `paragraph` | Text block — font, size, color, align, background, letterSpacing, smallCaps, tabularNumbers, multi-column (`columns` + `columnGap`), RTL (`dir`) |
337
- | `heading` | H1–H4 with bookmarks, URL links, internal anchors, tabularNumbers, RTL (`dir`) |
338
- | `table` | Fixed/proportional columns, colspan, rowspan, repeating headers across page breaks |
339
- | `image` | PNG/JPG/WebP with sizing, alignment, float left/right with `floatText` or rich `floatSpans` (mixed-format caption) |
340
- | `list` | Ordered/unordered, 2-level nesting, `nestedNumberingStyle: 'restart' \| 'continue'` |
341
- | `code` | Monospace block with background and padding |
342
- | `blockquote` | Left border + background |
343
- | `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
344
- | `svg` | Embedded SVG graphics with auto-sizing from viewBox |
345
- | `toc` | Auto-generated table of contents with accurate page numbers (two-pass) |
346
- | `qr-code` | Scannable QR code — UPI payment links, URLs, vCards. `data`, `size`, `errorCorrectionLevel`, `foreground`/`background` color. Requires `qrcode` peer dep. |
347
- | `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix, and more via `symbology` field. Requires `bwip-js` peer dep. |
348
- | `chart` | Vega-Lite data visualisation pass any valid Vega-Lite spec to `spec`. Rendered as vector SVG. Requires `vega` + `vega-lite` peer deps. |
349
- | `comment` | PDF sticky-note annotation (visible in Acrobat/Preview sidebar) |
350
- | `hr` | Horizontal rule |
351
- | `spacer` | Fixed-height gap |
352
- | `page-break` | Force new page |
353
-
354
- ### Document Features
355
-
356
- | Feature | Config key | Notes |
357
- | --- | --- | --- |
358
- | Watermarks | `doc.watermark` | Text or image, opacity, rotation |
359
- | Encryption | `doc.encryption` | Password + granular permissions |
360
- | PDF Bookmarks | `doc.bookmarks` | Auto-generated from headings |
361
- | Hyphenation | `doc.hyphenation` | Liang's algorithm, `language: 'en-us'` |
362
- | Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}`, `{{author}}` tokens |
363
- | Per-section overrides | `doc.sections` | Different header/footer/margins per page range |
364
- | Metadata | `doc.metadata` | Title, author, subject, keywords, `language` (PDF /Lang), `producer` |
365
-
366
- ### Phase 8 Features
367
-
368
- | Feature | API |
369
- | --- | --- |
370
- | **Hyperlinks** | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` |
371
- | **Inline formatting** | `span.verticalAlign: 'superscript'\|'subscript'`, `paragraph.letterSpacing`, `heading.smallCaps` |
372
- | **Sticky notes** | `{ type: 'comment', contents: '...' }`, `paragraph.annotation` |
373
- | **Document assembly** | `merge(pdfs)`, `assemble(parts)` |
374
- | **Interactive forms** | `{ type: 'form-field', fieldType: 'text'\|'checkbox'\|'radio'\|'dropdown'\|'button' }`, `doc.flattenForms` |
375
- | **Signature placeholder** | `doc.signature: { signerName, reason, location, x, y, page }` |
376
- | **Callout boxes** | `{ type: 'callout', content, style: 'info'\|'warning'\|'tip'\|'note', title }` |
377
- | **Form error handling** | `doc.onFormFieldError: (name, err) => 'skip' \| 'throw'` |
378
- | **Image error handling** | `doc.onImageLoadError: (src, err) => 'skip' \| 'throw'` |
379
-
380
- ### Type Safety (v0.4.6+)
381
-
382
- pretext-pdf is built with **strict TypeScript**. Remaining `as any` casts are limited to pdf-lib internal APIs with no public type surface, each documented with a comment explaining why:
383
-
384
- - **Full type inference** — No need to cast document configs or response types
385
- - **Element validation** — TypeScript catches invalid element types at compile time
386
- - **API contract testing** — Every API boundary has comprehensive type tests
387
- - **Error types** — `PretextPdfError` with typed code field for safe error handling
388
- - **Module typing** Complete type definitions for all exports and configurations
389
-
390
- ---
391
-
392
- ## Security Audit (April 2026)
393
-
394
- Comprehensive security and quality audit completed. **41 issues identified and fixed across 5 phases:**
395
-
396
- | Phase | Focus | Issues | Status |
397
- | --- | --- | --- | --- |
398
- | 0 | Core rendering | Footnote truncation | ✅ Fixed |
399
- | 1 | Security hardening | Path validation, async I/O, error handling | ✅ Fixed |
400
- | 2 | Type safety | Any-cast elimination, module typing | Fixed |
401
- | 3 | Test coverage | False-positives, boundary cases, crypto signing | ✅ Fixed |
402
- | 4 | Code quality | Silent failures → explicit errors, decoupling | ✅ Fixed |
403
-
404
- **Audit results:**
405
-
406
- - Zero path traversal vulnerabilities
407
- - All error messages sanitized (no data leaks)
408
- - Async file I/O throughout (non-blocking)
409
- - No hardcoded secrets or credentials
410
- - 188+ tests, 100% pass rate
411
- - Production-ready reliability
412
-
413
- See [SECURITY.md](SECURITY.md) for detailed security policies.
414
-
415
- ---
416
-
417
- ## Examples
418
-
419
- Run working examples from the `examples/` directory:
420
-
421
- ```bash
422
- # v0.8.0 new element examples (install optional deps first)
423
- # npm install qrcode bwip-js vega vega-lite marked
424
-
425
- # QR code in a document:
426
- # content: [{ type: 'qr-code', data: 'upi://pay?pa=merchant@upi&am=1000', size: 80, align: 'center' }]
427
-
428
- # Barcode:
429
- # content: [{ type: 'barcode', symbology: 'ean13', data: '5901234123457', width: 200, height: 80 }]
430
-
431
- # Vega-Lite chart:
432
- # content: [{ type: 'chart', spec: { data: { values: [...] }, mark: 'bar', encoding: { x: ..., y: ... } } }]
433
-
434
- # Phase 7 examples
435
- npm run example # Basic invoice
436
- npm run example:watermark # Text/image watermarks
437
- npm run example:bookmarks # PDF outline/bookmarks
438
- npm run example:toc # Auto table of contents
439
- npm run example:rtl # Arabic/Hebrew RTL text
440
- npm run example:encryption # Password-protected PDF
441
-
442
- # Phase 8 examples
443
- npm run example:hyperlinks # External links, email links, internal anchors
444
- npm run example:annotations # Sticky notes on elements
445
- npm run example:assembly # Merge and assemble multiple PDFs
446
- npm run example:inline # Superscript, subscript, letter-spacing, small-caps
447
- npm run example:forms # Interactive form fields (text, checkbox, radio, dropdown)
448
- npm run example:callout # Callout boxes (info, warning, tip, note presets)
449
- ```
450
-
451
- All examples write output to `output/*.pdf`.
452
-
453
- ---
454
-
455
- ## API Reference
456
-
457
- ### `render(doc): Promise<Uint8Array>`
458
-
459
- ```typescript
460
- import { render } from 'pretext-pdf'
461
-
462
- const pdf = await render({
463
- pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | [w, h]
464
- margins: { top: 72, bottom: 72, left: 72, right: 72 },
465
- defaultFont: 'Inter', // Inter 400 bundled; load others via doc.fonts
466
- defaultFontSize: 12,
467
- metadata: {
468
- title: 'Document Title',
469
- author: 'Author Name',
470
- subject: 'Description',
471
- keywords: ['pdf', 'report'],
472
- },
473
- watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
474
- encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
475
- bookmarks: { minLevel: 1, maxLevel: 3 },
476
- hyphenation: { language: 'en-us', minWordLength: 6 }, // ⚠️ Use lowercase: 'en-us' not 'en-US' — matches the npm package name hyphenation.en-us
477
- header: { text: 'My Document — {{pageNumber}} of {{totalPages}}', align: 'right' },
478
- footer: { text: 'Confidential', align: 'center', color: '#999999' },
479
- content: [ /* ContentElement[] */ ],
480
- })
481
- ```
482
-
483
- ### `merge(pdfs): Promise<Uint8Array>`
484
-
485
- Combine pre-rendered PDFs:
486
-
487
- ```typescript
488
- import { merge } from 'pretext-pdf'
489
-
490
- const combined = await merge([coverPdf, bodyPdf, appendixPdf])
491
- ```
492
-
493
- ### `assemble(parts): Promise<Uint8Array>`
494
-
495
- Mix new document configs with existing PDFs:
496
-
497
- ```typescript
498
- import { assemble } from 'pretext-pdf'
499
-
500
- const report = await assemble([
501
- { pdf: existingCoverPdf },
502
- { doc: { content: [...] } }, // rendered fresh
503
- { pdf: standardTermsPdf },
504
- ])
505
- ```
506
-
507
- ---
508
-
509
- ## Error Handling
510
-
511
- Every error throws `PretextPdfError` with a typed code:
512
-
513
- ```typescript
514
- import { render, PretextPdfError } from 'pretext-pdf'
515
-
516
- try {
517
- const pdf = await render(config)
518
- } catch (err) {
519
- if (err instanceof PretextPdfError) {
520
- switch (err.code) {
521
- case 'VALIDATION_ERROR': // Invalid config
522
- case 'FONT_LOAD_FAILED': // Font file not found
523
- case 'IMAGE_TOO_TALL': // Image doesn't fit on page
524
- case 'ASSEMBLY_EMPTY': // merge/assemble called with empty array
525
- // ... see CHANGELOG.md for full list
526
- }
527
- }
528
- }
529
- ```
530
-
531
- ---
532
-
533
- ## Troubleshooting
534
-
535
- ### Hyphenation language not found
536
-
537
- ```
538
- UNSUPPORTED_LANGUAGE: Language 'en-US' not supported
539
- ```
540
-
541
- Use **lowercase** language codes that match the npm package name:
542
-
543
- ```typescript
544
- // Wrong — 'en-US' fails on Linux (case-sensitive filesystem)
545
- hyphenation: { language: 'en-US' }
546
-
547
- // Correct — matches 'hyphenation.en-us' package name
548
- hyphenation: { language: 'en-us' }
549
- ```
550
-
551
- ### Encryption
552
-
553
- Encryption is built-in since v0.4.0. Add `encryption` to your document config:
554
-
555
- ```typescript
556
- const pdf = await render({
557
- encryption: {
558
- userPassword: 'open123',
559
- ownerPassword: 'admin456',
560
- permissions: { printing: true, copying: false, modifying: false }
561
- },
562
- content: [...]
563
- })
564
- ```
565
-
566
- ### SVG rendering requires optional dependency
567
-
568
- Install `@napi-rs/canvas` for SVG support:
569
-
570
- ```bash
571
- npm install @napi-rs/canvas
572
- ```
573
-
574
- ### PDF is blank or too small
575
-
576
- Check margins if left+right margins exceed page width, content width becomes negative:
577
-
578
- ```typescript
579
- // For narrow pages, reduce margins:
580
- margins: { top: 36, bottom: 36, left: 36, right: 36 }
581
- ```
582
-
583
- ### Form fields not interactive after flattenForms
584
-
585
- `flattenForms: true` bakes fields into static content — by design. Remove it to keep interactive.
586
-
587
- ---
588
-
589
- ## Test Coverage
590
-
591
- 598+ tests across all phases with 100% pass rate:
592
-
593
- ```bash
594
- npm test # Full suite (unit + e2e + all phases including v0.8.0)
595
- npm run test:unit # Validation, builder, rich-text unit tests
596
- npm run test:e2e # End-to-end render tests
597
- npm run test:10a # QR code + barcode tests
598
- npm run test:10b # Vega-Lite chart tests
599
- npm run test:10c # Markdown converter tests
600
- npm run test:10d # Template function tests
601
- npm run test:phases # All phase tests (7–11, performance, signatures)
602
- ```
603
-
604
- **Coverage**: Type safety, path validation, error handling, boundary cases, crypto signing, document assembly, all content elements, optional-dep error codes, MCP tool validation.
605
-
606
- ---
607
-
608
- ## Custom Fonts
609
-
610
- ```typescript
611
- const pdf = await render({
612
- fonts: [
613
- { family: 'Roboto', weight: 400, src: '/path/to/Roboto-Regular.ttf' },
614
- { family: 'Roboto', weight: 700, src: '/path/to/Roboto-Bold.ttf' },
615
- { family: 'Roboto', style: 'italic', src: '/path/to/Roboto-Italic.ttf' },
616
- ],
617
- defaultFont: 'Roboto',
618
- content: [
619
- { type: 'paragraph', text: 'Uses Roboto font' },
620
- { type: 'paragraph', text: 'Bold text', fontWeight: 700 },
621
- ],
622
- })
623
- ```
624
-
625
- ---
626
-
627
- ## Rich Text
628
-
629
- ```typescript
630
- {
631
- type: 'rich-paragraph',
632
- fontSize: 13,
633
- spans: [
634
- { text: 'Normal ' },
635
- { text: 'bold', fontWeight: 700 },
636
- { text: ' and ', fontStyle: 'italic' },
637
- { text: 'colored', color: '#e63946' },
638
- { text: ' and ' },
639
- { text: 'linked', href: 'https://example.com', underline: true, color: '#0070f3' },
640
- { text: '. Also: E=mc' },
641
- { text: '2', verticalAlign: 'superscript' },
642
- { text: ' and H' },
643
- { text: '2', verticalAlign: 'subscript' },
644
- { text: 'O.' },
645
- ],
646
- }
647
- ```
648
-
649
- ---
650
-
651
- ## Footnotes
652
-
653
- Use `createFootnoteSet()` to generate matched reference/definition pairs with guaranteed unique IDs:
654
-
655
- ```typescript
656
- import { render, createFootnoteSet } from 'pretext-pdf'
657
-
658
- const notes = createFootnoteSet([
659
- { text: 'Smith, J. (2022). Typography in PDFs.' },
660
- { text: 'Ibid., p. 42.' },
661
- ])
662
-
663
- await render({
664
- content: [
665
- {
666
- type: 'rich-paragraph',
667
- spans: [
668
- { text: 'See the original research' },
669
- { text: '¹', verticalAlign: 'superscript', footnoteRef: notes[0]!.id },
670
- { text: ' for details.' },
671
- ],
672
- },
673
- ...notes.map(n => n.def), // footnote-def elements go at end of document
674
- ],
675
- })
676
- ```
677
-
678
- ---
679
-
680
- ## Roadmap
681
-
682
- | Phase | Feature | Status |
683
- |-------|---------|--------|
684
- | 1–4 | Core engine, pagination, typography | ✅ |
685
- | 5 | Rich text / builder API | ✅ |
686
- | 6 | Headers/footers, columns, decoration | ✅ |
687
- | 7A | PDF Bookmarks / Outline | ✅ |
688
- | 7B | Watermarks | ✅ |
689
- | 7C | Hyphenation | ✅ |
690
- | 7D | Table of Contents | ✅ |
691
- | 7E | SVG support | ✅ |
692
- | 7F | RTL text (Arabic/Hebrew) | ✅ |
693
- | 7G | Encryption | ✅ |
694
- | 8A | Sticky note annotations | ✅ |
695
- | 8B | Interactive forms (text/checkbox/radio/dropdown/button) | ✅ |
696
- | 8C | Document assembly (merge + assemble) | ✅ |
697
- | 8D | Callout boxes (info/warning/tip/note) | ✅ |
698
- | 8E | Signature placeholder | ✅ |
699
- | 8F | Document metadata (language, producer) | ✅ |
700
- | 8G | Hyperlinks | ✅ |
701
- | 8H | Inline formatting (super/subscript, letterSpacing, smallCaps) | ✅ |
702
- | 9A | Digital signatures (cryptographic, PKCS#7) | 🔜 |
703
- | 9B | Image floats (text flowing around images) | 🔜 |
704
- | 9C | Font subsetting pre-computation | 🔜 |
705
-
706
- ---
707
-
708
- ## Performance
709
-
710
- Benchmarked on Windows 11 / Node 22 / Intel i7-12th Gen. Numbers are averages over 10 runs, excluding the first cold JIT run.
711
-
712
- | Document | Render time | PDF size |
713
- | --- | --- | --- |
714
- | 1 page (heading + paragraph + list) | ~220 ms | ~45 KB |
715
- | 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
716
- | Mixed (heading + paragraph + 20-row table + list + hr) | ~290 ms | ~60 KB |
717
-
718
- **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.
719
-
720
- For large documents (10,000+ elements), set `NODE_OPTIONS=--max-old-space-size=4096` to prevent GC pressure.
721
-
722
- ---
723
-
724
- ## Migration from pdfmake
725
-
726
- Coming from pdfmake? See the **[Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md)** for a complete cheat sheet covering every common pdfmake pattern and its pretext-pdf equivalent.
727
-
728
- ---
729
-
730
- ## Contributing
731
-
732
- See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
733
-
734
- ---
735
-
736
- ## License
737
-
738
- [MIT](LICENSE)
739
-
740
- ---
741
-
742
- ## Credits
743
-
744
- Built by [Himanshu Jain](https://github.com/Himaan1998Y) on top of:
745
- - **[pretext](https://github.com/chenglou/pretext)** — Text layout engine (Cheng Lou)
746
- - **[pdf-lib](https://github.com/Hopding/pdf-lib)** — PDF manipulation
747
- - **[@napi-rs/canvas](https://github.com/napi-rs/canvas)** — Server-side Canvas API for Node.js
748
-
749
- Questions? [Open an issue](https://github.com/Himaan1998Y/pretext-pdf/issues)
1
+ # pretext-pdf
2
+
3
+ > **The PDF library AI agents speak natively.**
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.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
8
+ [![npm downloads](https://img.shields.io/npm/dw/pretext-pdf)](https://www.npmjs.com/package/pretext-pdf)
9
+ [![CI](https://github.com/Himaan1998Y/pretext-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/Himaan1998Y/pretext-pdf/actions)
10
+ [![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
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
13
+
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.
17
+
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
+
20
+ ---
21
+
22
+ ## Why pretext-pdf
23
+
24
+ There are three established camps in JS PDF generation, and one gap. pretext-pdf lives in the gap.
25
+
26
+ | | pdfmake / jsPDF / pdfkit | Puppeteer / Playwright | LaTeX / WeasyPrint | **pretext-pdf** |
27
+ |---|---|---|---|---|
28
+ | Lightweight (no Chromium) | | ~300 MB | native binaries | ✅ |
29
+ | Pure ESM, runs in serverless | | ⚠️ painful in Lambda | | ✅ |
30
+ | Professional typography (kerning, hyphenation, RTL/CJK) | ❌ | ✅ | ✅ | ✅ |
31
+ | 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'
62
+
63
+ // Whatever produced this JSON Claude, GPT, a workflow node, a form submission — works 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.
80
+
81
+ ---
82
+
83
+ ## Install
84
+
85
+ ```bash
86
+ npm install pretext-pdf
87
+ ```
88
+
89
+ > **ESM only** — use `import`, not `require`. Requires Node.js 18.
90
+
91
+ Optional peer dependencies — install only what you need:
92
+
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
+ ```
101
+
102
+ > **Encryption is built-in** since v0.4.0 no extra install needed. Just add `encryption` to your document config.
103
+
104
+ ---
105
+
106
+ ## Quick start
107
+
108
+ ```typescript
109
+ import { render } from 'pretext-pdf'
110
+ import { writeFileSync } from 'fs'
111
+
112
+ const pdf = await render({
113
+ pageSize: 'A4',
114
+ margins: { top: 40, bottom: 40, left: 50, right: 50 },
115
+ metadata: { title: 'My Invoice', author: 'Acme Corp' },
116
+ content: [
117
+ { type: 'heading', level: 1, text: 'Invoice #12345' },
118
+ { type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
119
+ {
120
+ type: 'table',
121
+ columns: [
122
+ { name: 'Item', width: 200 },
123
+ { name: 'Qty', width: 50, align: 'right' },
124
+ { name: 'Price', width: 100, align: 'right' },
125
+ ],
126
+ rows: [
127
+ { Item: 'Professional Services', Qty: '10', Price: '$1,000' },
128
+ { Item: 'Hosting (annual)', Qty: '1', Price: '$500' },
129
+ ],
130
+ },
131
+ { type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
132
+ ],
133
+ })
134
+
135
+ writeFileSync('invoice.pdf', pdf)
136
+ ```
137
+
138
+ ### Builder API (fluent style)
139
+
140
+ ```typescript
141
+ import { createPdf } from 'pretext-pdf'
142
+
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()
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Output samples
153
+
154
+ Real documents generated with pretext-pdf:
155
+
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) |
160
+
161
+ ---
162
+
163
+ ## What's in v0.8.0
164
+
165
+ Five new capabilities, all behind optional peer dependencies (zero extra weight if unused):
166
+
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`.
172
+
173
+ See [CHANGELOG.md](CHANGELOG.md) for the full history.
174
+
175
+ ---
176
+
177
+ ## India / GST invoicing
178
+
179
+ pretext-pdf has built-in support for Indian invoice requirements:
180
+
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
186
+
187
+ Use the `createGstInvoice` template for a complete GST-compliant invoice in one call:
188
+
189
+ ```typescript
190
+ import { createGstInvoice } from 'pretext-pdf/templates'
191
+ import { render } from 'pretext-pdf'
192
+
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',
205
+ })
206
+ const pdf = await render({ content })
207
+ ```
208
+
209
+ See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for the raw element approach.
210
+
211
+ ---
212
+
213
+ ## Markdown → PDF (`pretext-pdf/markdown`)
214
+
215
+ Convert any Markdown string to a `pretext-pdf` document in one call. Requires `marked` peer dep.
216
+
217
+ ```typescript
218
+ import { markdownToContent } from 'pretext-pdf/markdown'
219
+ import { render } from 'pretext-pdf'
220
+ import { writeFileSync } from 'fs'
221
+
222
+ const md = `
223
+ # Q1 2026 Report
224
+
225
+ Revenue grew **18%** year-over-year, driven by:
226
+
227
+ - Cloud services (+32%)
228
+ - Enterprise licenses (+12%)
229
+
230
+ > All figures are in USD millions.
231
+ `
232
+
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)
238
+ ```
239
+
240
+ Supported Markdown: headings h1–h4, bold, italic, strikethrough, inline code, links, ordered/unordered lists (2 levels), fenced code blocks, blockquotes, horizontal rules.
241
+
242
+ ---
243
+
244
+ ## Invoice & report templates (`pretext-pdf/templates`)
245
+
246
+ Pre-built zero-dependency template functions that generate `ContentElement[]` arrays:
247
+
248
+ ```typescript
249
+ import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
250
+ import { render } from 'pretext-pdf'
251
+
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'] },
270
+ ],
271
+ })
272
+
273
+ const pdf = await render({ content: reportContent })
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Element type reference
279
+
280
+ ```
281
+ paragraph heading(1-4) spacer hr page-break
282
+ table image svg list code
283
+ blockquote rich-paragraph callout comment form-field
284
+ toc qr-code barcode chart footnote-def
285
+ ```
286
+
287
+ | Element | What it does |
288
+ | --- | --- |
289
+ | `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'` |
294
+ | `code` | Monospace block with background and padding |
295
+ | `blockquote` | Left border + background |
296
+ | `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
297
+ | `svg` | Embedded SVG graphics with auto-sizing from viewBox |
298
+ | `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. |
302
+ | `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 |
306
+
307
+ ### Document-level features
308
+
309
+ | Feature | Config key | Notes |
310
+ | --- | --- | --- |
311
+ | Watermarks | `doc.watermark` | Text or image, opacity, rotation |
312
+ | Encryption | `doc.encryption` | Password + granular permissions |
313
+ | 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` |
318
+ | 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
+ | 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 }` | |
326
+
327
+ ---
328
+
329
+ ## API reference
330
+
331
+ ### `render(doc): Promise<Uint8Array>`
332
+
333
+ ```typescript
334
+ import { render } from 'pretext-pdf'
335
+
336
+ const pdf = await render({
337
+ pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | [w, h]
338
+ margins: { top: 72, bottom: 72, left: 72, right: 72 },
339
+ defaultFont: 'Inter', // Inter 400 bundled; load others via doc.fonts
340
+ defaultFontSize: 12,
341
+ metadata: {
342
+ title: 'Document Title',
343
+ author: 'Author Name',
344
+ subject: 'Description',
345
+ keywords: ['pdf', 'report'],
346
+ },
347
+ watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
348
+ encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
349
+ 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' },
353
+ content: [ /* ContentElement[] */ ],
354
+ })
355
+ ```
356
+
357
+ ### `merge(pdfs): Promise<Uint8Array>`
358
+
359
+ Combine pre-rendered PDFs:
360
+
361
+ ```typescript
362
+ import { merge } from 'pretext-pdf'
363
+
364
+ const combined = await merge([coverPdf, bodyPdf, appendixPdf])
365
+ ```
366
+
367
+ ### `assemble(parts): Promise<Uint8Array>`
368
+
369
+ Mix new document configs with existing PDFs:
370
+
371
+ ```typescript
372
+ import { assemble } from 'pretext-pdf'
373
+
374
+ const report = await assemble([
375
+ { pdf: existingCoverPdf },
376
+ { doc: { content: [...] } }, // rendered fresh
377
+ { pdf: standardTermsPdf },
378
+ ])
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Custom fonts
384
+
385
+ ```typescript
386
+ const pdf = await render({
387
+ fonts: [
388
+ { family: 'Roboto', weight: 400, src: '/path/to/Roboto-Regular.ttf' },
389
+ { family: 'Roboto', weight: 700, src: '/path/to/Roboto-Bold.ttf' },
390
+ { family: 'Roboto', style: 'italic', src: '/path/to/Roboto-Italic.ttf' },
391
+ ],
392
+ defaultFont: 'Roboto',
393
+ content: [
394
+ { type: 'paragraph', text: 'Uses Roboto font' },
395
+ { type: 'paragraph', text: 'Bold text', fontWeight: 700 },
396
+ ],
397
+ })
398
+ ```
399
+
400
+ > **Avoid `system-ui`** as a font name on macOS — it triggers a known layout-measurement inaccuracy in Pretext. Always name fonts explicitly.
401
+
402
+ ---
403
+
404
+ ## Rich text
405
+
406
+ ```typescript
407
+ {
408
+ type: 'rich-paragraph',
409
+ fontSize: 13,
410
+ spans: [
411
+ { text: 'Normal ' },
412
+ { text: 'bold', fontWeight: 700 },
413
+ { text: ' and ', fontStyle: 'italic' },
414
+ { text: 'colored', color: '#e63946' },
415
+ { text: ' and ' },
416
+ { text: 'linked', href: 'https://example.com', underline: true, color: '#0070f3' },
417
+ { text: '. Also: E=mc' },
418
+ { text: '2', verticalAlign: 'superscript' },
419
+ { text: ' and H' },
420
+ { text: '2', verticalAlign: 'subscript' },
421
+ { text: 'O.' },
422
+ ],
423
+ }
424
+ ```
425
+
426
+ ---
427
+
428
+ ## Footnotes
429
+
430
+ Use `createFootnoteSet()` to generate matched reference/definition pairs with guaranteed unique IDs:
431
+
432
+ ```typescript
433
+ import { render, createFootnoteSet } from 'pretext-pdf'
434
+
435
+ const notes = createFootnoteSet([
436
+ { text: 'Smith, J. (2022). Typography in PDFs.' },
437
+ { text: 'Ibid., p. 42.' },
438
+ ])
439
+
440
+ await render({
441
+ content: [
442
+ {
443
+ type: 'rich-paragraph',
444
+ spans: [
445
+ { text: 'See the original research' },
446
+ { text: '¹', verticalAlign: 'superscript', footnoteRef: notes[0]!.id },
447
+ { text: ' for details.' },
448
+ ],
449
+ },
450
+ ...notes.map(n => n.def), // footnote-def elements go at end of document
451
+ ],
452
+ })
453
+ ```
454
+
455
+ ---
456
+
457
+ ## Examples
458
+
459
+ Run working examples from the `examples/` directory:
460
+
461
+ ```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
+ npm run example # Basic invoice
467
+ npm run example:watermark # Text/image watermarks
468
+ npm run example:bookmarks # PDF outline/bookmarks
469
+ npm run example:toc # Auto table of contents
470
+ npm run example:rtl # Arabic/Hebrew RTL text
471
+ 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
478
+ 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
481
+ ```
482
+
483
+ All examples write output to `output/*.pdf`.
484
+
485
+ ---
486
+
487
+ ## Error handling
488
+
489
+ Every error throws `PretextPdfError` with a typed `code` — designed so an LLM (or a human) can self-correct:
490
+
491
+ ```typescript
492
+ import { render, PretextPdfError } from 'pretext-pdf'
493
+
494
+ try {
495
+ const pdf = await render(config)
496
+ } catch (err) {
497
+ if (err instanceof PretextPdfError) {
498
+ switch (err.code) {
499
+ case 'VALIDATION_ERROR': // Invalid config
500
+ case 'FONT_LOAD_FAILED': // Font file not found
501
+ 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
504
+ }
505
+ }
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Troubleshooting
512
+
513
+ ### Hyphenation language not found
514
+
515
+ ```
516
+ UNSUPPORTED_LANGUAGE: Language 'en-US' not supported
517
+ ```
518
+
519
+ Use **lowercase** language codes that match the npm package name:
520
+
521
+ ```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' }
527
+ ```
528
+
529
+ ### SVG rendering requires optional dependency
530
+
531
+ Install `@napi-rs/canvas` for SVG / chart / qr-code / barcode support:
532
+
533
+ ```bash
534
+ npm install @napi-rs/canvas
535
+ ```
536
+
537
+ ### PDF is blank or too small
538
+
539
+ Check margins — if left+right margins exceed page width, content width becomes negative:
540
+
541
+ ```typescript
542
+ // For narrow pages, reduce margins:
543
+ margins: { top: 36, bottom: 36, left: 36, right: 36 }
544
+ ```
545
+
546
+ ### Form fields not interactive after `flattenForms`
547
+
548
+ `flattenForms: true` bakes fields into static content — by design. Remove it to keep them interactive.
549
+
550
+ ---
551
+
552
+ ## Non-goals
553
+
554
+ What pretext-pdf is **not** trying to be — pick a different tool for these:
555
+
556
+ - **Editing or parsing existing PDFs** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdf-parse`](https://www.npmjs.com/package/pdf-parse)
557
+ - **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
559
+ - **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
561
+
562
+ ---
563
+
564
+ ## Runtime requirements
565
+
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
571
+
572
+ ---
573
+
574
+ ## Performance
575
+
576
+ Benchmarked on Windows 11 / Node 22 / Intel i7-12th Gen. Numbers are averages over 10 runs, excluding the first cold JIT run.
577
+
578
+ | Document | Render time | PDF size |
579
+ | --- | --- | --- |
580
+ | 1 page (heading + paragraph + list) | ~220 ms | ~45 KB |
581
+ | 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
582
+ | Mixed (heading + paragraph + 20-row table + list + hr) | ~290 ms | ~60 KB |
583
+
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.
585
+
586
+ For large documents (10,000+ elements), set `NODE_OPTIONS=--max-old-space-size=4096` to prevent GC pressure.
587
+
588
+ ---
589
+
590
+ ## Test coverage
591
+
592
+ 598+ tests across all phases with 100% pass rate:
593
+
594
+ ```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)
603
+ ```
604
+
605
+ **Coverage**: type safety, path validation, error handling, boundary cases, crypto signing, document assembly, all content elements, optional-dep error codes, MCP tool validation.
606
+
607
+ ---
608
+
609
+ ## Security
610
+
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.
612
+
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
616
+ - Async file I/O throughout (non-blocking)
617
+ - Strict TypeScript with documented `any`-casts only at pdf-lib internal boundaries
618
+
619
+ ---
620
+
621
+ ## Roadmap
622
+
623
+ | Phase | Feature | Status |
624
+ |-------|---------|--------|
625
+ | 1–6 | Core engine, pagination, typography, rich text, builder, columns | ✅ |
626
+ | 7A–G | Bookmarks, watermarks, hyphenation, TOC, SVG, RTL, encryption | ✅ |
627
+ | 8A–H | Annotations, forms, assembly, callouts, signatures, metadata, hyperlinks, inline formatting | ✅ |
628
+ | 9A–C | Cryptographic signatures (PKCS#7), image floats, font subsetting | ✅ |
629
+ | 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
637
+
638
+ Coming from pdfmake? See the **[Migration Guide](docs/MIGRATION_FROM_PDFMAKE.md)** — every common pdfmake pattern mapped to its pretext-pdf equivalent.
639
+
640
+ ---
641
+
642
+ ## Contributing
643
+
644
+ See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
645
+
646
+ ---
647
+
648
+ ## License
649
+
650
+ [MIT](LICENSE)
651
+
652
+ ---
653
+
654
+ ## Credits
655
+
656
+ Built by [Himanshu Jain](https://github.com/Himaan1998Y) on the shoulders of [pretext](https://github.com/chenglou/pretext), [pdf-lib](https://github.com/Hopding/pdf-lib), and [@napi-rs/canvas](https://github.com/napi-rs/canvas).
657
+
658
+ Questions? [Open an issue](https://github.com/Himaan1998Y/pretext-pdf/issues) — or try it live at the [demo](https://himaan1998y.github.io/pretext-pdf/).