pretext-pdf 0.5.3 → 0.8.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +462 -361
  2. package/README.md +749 -568
  3. package/dist/assets.d.ts +5 -0
  4. package/dist/assets.d.ts.map +1 -1
  5. package/dist/assets.js +248 -43
  6. package/dist/assets.js.map +1 -1
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js.map +1 -1
  10. package/dist/fonts.d.ts.map +1 -1
  11. package/dist/fonts.js +67 -8
  12. package/dist/fonts.js.map +1 -1
  13. package/dist/index.d.ts +29 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +35 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/markdown.d.ts +28 -0
  18. package/dist/markdown.d.ts.map +1 -0
  19. package/dist/markdown.js +222 -0
  20. package/dist/markdown.js.map +1 -0
  21. package/dist/measure-blocks.d.ts.map +1 -1
  22. package/dist/measure-blocks.js +347 -62
  23. package/dist/measure-blocks.js.map +1 -1
  24. package/dist/measure-text.d.ts.map +1 -1
  25. package/dist/measure-text.js +1 -8
  26. package/dist/measure-text.js.map +1 -1
  27. package/dist/measure.d.ts.map +1 -1
  28. package/dist/measure.js +13 -21
  29. package/dist/measure.js.map +1 -1
  30. package/dist/render-blocks.d.ts +4 -1
  31. package/dist/render-blocks.d.ts.map +1 -1
  32. package/dist/render-blocks.js +227 -105
  33. package/dist/render-blocks.js.map +1 -1
  34. package/dist/render-extras.d.ts.map +1 -1
  35. package/dist/render-extras.js +72 -71
  36. package/dist/render-extras.js.map +1 -1
  37. package/dist/render-utils.d.ts +9 -2
  38. package/dist/render-utils.d.ts.map +1 -1
  39. package/dist/render-utils.js +24 -13
  40. package/dist/render-utils.js.map +1 -1
  41. package/dist/render.d.ts.map +1 -1
  42. package/dist/render.js +27 -3
  43. package/dist/render.js.map +1 -1
  44. package/dist/rich-text.d.ts +0 -4
  45. package/dist/rich-text.d.ts.map +1 -1
  46. package/dist/rich-text.js +15 -9
  47. package/dist/rich-text.js.map +1 -1
  48. package/dist/templates.d.ts +79 -0
  49. package/dist/templates.d.ts.map +1 -0
  50. package/dist/templates.js +201 -0
  51. package/dist/templates.js.map +1 -0
  52. package/dist/types.d.ts +139 -5
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/validate.d.ts.map +1 -1
  55. package/dist/validate.js +241 -28
  56. package/dist/validate.js.map +1 -1
  57. package/package.json +175 -130
package/README.md CHANGED
@@ -1,568 +1,749 @@
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-0%20any%20casts-blueviolet)](#type-safety)
12
- [![Tests](https://img.shields.io/badge/tests-188%2B-brightgreen)](#test-coverage)
13
- [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
-
15
- ---
16
-
17
- ## v0.4.6Security & Quality Hardening
18
-
19
- All 41 issues from comprehensive April 2026 security audit resolved:
20
-
21
- - **Phase 0**: Footnote text truncation fixed
22
- - **Phase 1**: Security hardening (path traversal protection, async file I/O, explicit error handling)
23
- - **Phase 2**: Type safety (eliminated any-casts, proper module typing, strict inference)
24
- - **Phase 3**: Test coverage (false-positive fixes, boundary case validation)
25
- - **Phase 4**: Code quality (silent failures explicit errors, improved decoupling)
26
-
27
- **Result**: 188+ comprehensive tests, 100% pass rate, production-ready reliability.
28
-
29
- ---
30
-
31
- ## Why pretext-pdf?
32
-
33
- | | pdfmake | Puppeteer | **pretext-pdf** |
34
- |---|---|---|---|
35
- | Easy declarative API | ✅ | ❌ | ✅ |
36
- | Professional typography | | ✅ | ✅ |
37
- | Lightweight (no browser) | | ❌ | ✅ |
38
- | International text (RTL/CJK) | ❌ | ✅ | ✅ |
39
- | Pure Node.js | ✅ | ❌ | ✅ |
40
- | Hyperlinks + annotations | ❌ | ✅ | ✅ |
41
- | Document assembly | | | |
42
-
43
- ### Powered by [pretext](https://github.com/chenglou/pretext)
44
-
45
- Pretext is a precision text layout engine by [Cheng Lou](https://github.com/chenglou) (React core team, Midjourney).
46
-
47
- ```
48
- JSON descriptor → pretext layout → pdf-lib renderer → PDF bytes
49
- (kerning, (annotations,
50
- hyphenation, encryption,
51
- RTL, CJK) hyperlinks)
52
- ```
53
-
54
- ---
55
-
56
- ## Output Samples
57
-
58
- Real documents generated with pretext-pdf:
59
-
60
- | Invoice | Market Report | Resume / CV |
61
- |---------|--------------|-------------|
62
- | [![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) |
63
- | [View source](examples/showcase-invoice.ts) | [View source](examples/showcase-report.ts) | [View source](examples/showcase-resume.ts) |
64
-
65
- ---
66
-
67
- ## Install
68
-
69
- ```bash
70
- npm install pretext-pdf@^0.4.6
71
- ```
72
-
73
- Optional peer dependency (for SVG support):
74
- ```bash
75
- npm install @napi-rs/canvas # Required for SVG elements (auto-installed in most setups)
76
- ```
77
-
78
- Optional peer dependency (for cryptographic signing):
79
- ```bash
80
- npm install @signpdf/signpdf # Required for PKCS#7 document signing
81
- ```
82
-
83
- > **Encryption is built-in since v0.4.0** — no extra install needed. Just add `encryption` to your document config.
84
- > **Type safety certified v0.4.6** — 100% strict TypeScript, zero any-casts in critical paths.
85
-
86
- ---
87
-
88
- ## Quick Start
89
-
90
- ```typescript
91
- import { render } from 'pretext-pdf'
92
- import { writeFileSync } from 'fs'
93
-
94
- const pdf = await render({
95
- pageSize: 'A4',
96
- margins: { top: 40, bottom: 40, left: 50, right: 50 },
97
- metadata: { title: 'My Invoice', author: 'Acme Corp' },
98
- content: [
99
- { type: 'heading', level: 1, text: 'Invoice #12345' },
100
- { type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
101
- {
102
- type: 'table',
103
- columns: [
104
- { name: 'Item', width: 200 },
105
- { name: 'Qty', width: 50, align: 'right' },
106
- { name: 'Price', width: 100, align: 'right' },
107
- ],
108
- rows: [
109
- { Item: 'Professional Services', Qty: '10', Price: '$1,000' },
110
- { Item: 'Hosting (annual)', Qty: '1', Price: '$500' },
111
- ],
112
- },
113
- { type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
114
- ],
115
- })
116
-
117
- writeFileSync('invoice.pdf', pdf)
118
- ```
119
-
120
- ### Builder API
121
-
122
- ```typescript
123
- import { createPdf } from 'pretext-pdf'
124
-
125
- const pdf = await createPdf({ pageSize: 'A4' })
126
- .addHeading('My Report', 1)
127
- .addText('Fluent chainable API.')
128
- .addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
129
- .build()
130
- ```
131
-
132
- ---
133
-
134
- ## Agent / AI Integration
135
-
136
- pretext-pdf works great as a tool for AI agents generating PDFs on demand.
137
-
138
- ### MCP Server (Claude Desktop, Cursor, Windsurf)
139
-
140
- Use [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp) to call pretext-pdf directly from any AI agent:
141
-
142
- ```json
143
- {
144
- "mcpServers": {
145
- "pretext-pdf": {
146
- "command": "npx",
147
- "args": ["-y", "pretext-pdf-mcp"]
148
- }
149
- }
150
- }
151
- ```
152
-
153
- Tools available: `generate_pdf`, `generate_invoice`, `generate_report`, `list_element_types`
154
-
155
- ### Quick pattern for LLMs
156
-
157
- ```typescript
158
- import { render } from 'pretext-pdf'
159
-
160
- // Every PdfDocument is a plain JSON object — perfect for AI generation
161
- const pdf = await render({
162
- metadata: { title: 'AI-Generated Report' },
163
- content: [
164
- { type: 'heading', level: 1, text: 'Summary' },
165
- { type: 'paragraph', text: 'Generated content here.' },
166
- // ... AI fills this array
167
- ]
168
- })
169
- ```
170
-
171
- ### Key facts for AI agents
172
-
173
- - `content` is an array of typed elements — each has a `type` field
174
- - All fields are optional except `type` and element-specific required fields (e.g. `text`, `level`)
175
- - Errors are typed: `err.code` tells you exactly what went wrong
176
- - `render()` is fully async, safe to `await` in any context
177
- - Works in Node.js 18+ and modern browsers (with `@napi-rs/canvas` for SVG)
178
-
179
- ### Element type reference (quick)
180
-
181
- ```
182
- paragraph heading(1-4) spacer hr page-break
183
- table image svg list code
184
- blockquote rich-paragraph callout comment form-field
185
- toc
186
- ```
187
-
188
- ---
189
-
190
- ## India / GST Invoicing
191
-
192
- pretext-pdf has built-in support for Indian invoice requirements:
193
-
194
- - **₹ symbol** renders correctly (bundled Inter font includes the Rupee glyph)
195
- - **Indian number formatting** helper for 1,00,000 notation (not 100,000)
196
- - **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) table layouts
197
- - **Amount in words** — Indian numbering system (Lakh/Crore)
198
- - **SAC/HSN codes** — column support in line-item tables
199
-
200
- See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for a complete GST-compliant invoice template.
201
-
202
- ---
203
-
204
- ## Features
205
-
206
- ### Security & Reliability
207
-
208
- - **Type-safe architecture** — 0 any-casts in critical path, strict TypeScript inference
209
- - ✅ **Cryptographically signed PDFs** — PKCS#7 signing support (Phase 3)
210
- - ✅ **Path traversal protection** — Secure file operations with validated paths
211
- - ✅ **Error sanitization** — No sensitive data in error messages
212
- - ✅ **Async-safe I/O** — Non-blocking file operations throughout
213
- - ✅ **Comprehensive test coverage** — 188+ tests with 100% pass rate
214
- - ✅ **No hardcoded secrets** — Environment-based configuration
215
-
216
- ### Element Types
217
-
218
- | Element | What it does |
219
- | --- | --- |
220
- | `paragraph` | Text block — font, size, color, align, background, letterSpacing, smallCaps |
221
- | `heading` | H1–H4 with bookmarks, URL links, internal anchors |
222
- | `table` | Fixed/proportional columns, colspan, repeating headers across page breaks |
223
- | `image` | PNG/JPG/WebP with sizing, alignment, auto-format detection |
224
- | `list` | Ordered/unordered, nested, custom markers |
225
- | `code` | Monospace block with background and padding |
226
- | `blockquote` | Left border + background |
227
- | `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
228
- | `svg` | Embedded SVG graphics with auto-sizing from viewBox |
229
- | `toc` | Auto-generated table of contents with accurate page numbers (two-pass) |
230
- | `comment` | PDF sticky-note annotation (visible in Acrobat/Preview sidebar) |
231
- | `hr` | Horizontal rule |
232
- | `spacer` | Fixed-height gap |
233
- | `page-break` | Force new page |
234
-
235
- ### Document Features
236
-
237
- | Feature | Config key | Notes |
238
- | --- | --- | --- |
239
- | Watermarks | `doc.watermark` | Text or image, opacity, rotation |
240
- | Encryption | `doc.encryption` | Password + granular permissions |
241
- | PDF Bookmarks | `doc.bookmarks` | Auto-generated from headings |
242
- | Hyphenation | `doc.hyphenation` | Liang's algorithm, `language: 'en-us'` |
243
- | Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}` / `{{totalPages}}` tokens |
244
- | Metadata | `doc.metadata` | Title, author, subject, keywords, `language` (PDF /Lang), `producer` |
245
-
246
- ### Phase 8 Features
247
-
248
- | Feature | API |
249
- | --- | --- |
250
- | **Hyperlinks** | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` |
251
- | **Inline formatting** | `span.verticalAlign: 'superscript'\|'subscript'`, `paragraph.letterSpacing`, `heading.smallCaps` |
252
- | **Sticky notes** | `{ type: 'comment', contents: '...' }`, `paragraph.annotation` |
253
- | **Document assembly** | `merge(pdfs)`, `assemble(parts)` |
254
- | **Interactive forms** | `{ type: 'form-field', fieldType: 'text'\|'checkbox'\|'radio'\|'dropdown'\|'button' }`, `doc.flattenForms` |
255
- | **Signature placeholder** | `doc.signature: { signerName, reason, location, x, y, page }` |
256
- | **Callout boxes** | `{ type: 'callout', content, style: 'info'\|'warning'\|'tip'\|'note', title }` |
257
-
258
- ### Type Safety (v0.4.6+)
259
-
260
- pretext-pdf is built with **strict TypeScript** and **zero any-casts** in critical paths:
261
-
262
- - **Full type inference** No need to cast document configs or response types
263
- - **Element validation** — TypeScript catches invalid element types at compile time
264
- - **API contract testing** — Every API boundary has comprehensive type tests
265
- - **Error types** — `PretextPdfError` with typed code field for safe error handling
266
- - **Module typing** — Complete type definitions for all exports and configurations
267
-
268
- ---
269
-
270
- ## Security Audit (April 2026)
271
-
272
- Comprehensive security and quality audit completed. **41 issues identified and fixed across 5 phases:**
273
-
274
- | Phase | Focus | Issues | Status |
275
- | --- | --- | --- | --- |
276
- | 0 | Core rendering | Footnote truncation | ✅ Fixed |
277
- | 1 | Security hardening | Path validation, async I/O, error handling | ✅ Fixed |
278
- | 2 | Type safety | Any-cast elimination, module typing | ✅ Fixed |
279
- | 3 | Test coverage | False-positives, boundary cases, crypto signing | ✅ Fixed |
280
- | 4 | Code quality | Silent failures → explicit errors, decoupling | ✅ Fixed |
281
-
282
- **Audit results:**
283
-
284
- - Zero path traversal vulnerabilities
285
- - All error messages sanitized (no data leaks)
286
- - Async file I/O throughout (non-blocking)
287
- - No hardcoded secrets or credentials
288
- - 188+ tests, 100% pass rate
289
- - Production-ready reliability
290
-
291
- See [SECURITY.md](SECURITY.md) for detailed security policies.
292
-
293
- ---
294
-
295
- ## Examples
296
-
297
- Run working examples from the `examples/` directory:
298
-
299
- ```bash
300
- # Phase 7 examples
301
- npm run example # Basic invoice
302
- npm run example:watermark # Text/image watermarks
303
- npm run example:bookmarks # PDF outline/bookmarks
304
- npm run example:toc # Auto table of contents
305
- npm run example:rtl # Arabic/Hebrew RTL text
306
- npm run example:encryption # Password-protected PDF
307
-
308
- # Phase 8 examples
309
- npm run example:hyperlinks # External links, email links, internal anchors
310
- npm run example:annotations # Sticky notes on elements
311
- npm run example:assembly # Merge and assemble multiple PDFs
312
- npm run example:inline # Superscript, subscript, letter-spacing, small-caps
313
- npm run example:forms # Interactive form fields (text, checkbox, radio, dropdown)
314
- npm run example:callout # Callout boxes (info, warning, tip, note presets)
315
- ```
316
-
317
- All examples write output to `output/*.pdf`.
318
-
319
- ---
320
-
321
- ## API Reference
322
-
323
- ### `render(doc): Promise<Uint8Array>`
324
-
325
- ```typescript
326
- import { render } from 'pretext-pdf'
327
-
328
- const pdf = await render({
329
- pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | [w, h]
330
- margins: { top: 72, bottom: 72, left: 72, right: 72 },
331
- defaultFont: 'Inter', // Inter 400 bundled; load others via doc.fonts
332
- defaultFontSize: 12,
333
- metadata: {
334
- title: 'Document Title',
335
- author: 'Author Name',
336
- subject: 'Description',
337
- keywords: ['pdf', 'report'],
338
- },
339
- watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
340
- encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
341
- bookmarks: { minLevel: 1, maxLevel: 3 },
342
- hyphenation: { language: 'en-us', minWordLength: 6 }, // ⚠️ Use lowercase: 'en-us' not 'en-US' — matches the npm package name hyphenation.en-us
343
- header: { text: 'My Document {{pageNumber}} of {{totalPages}}', align: 'right' },
344
- footer: { text: 'Confidential', align: 'center', color: '#999999' },
345
- content: [ /* ContentElement[] */ ],
346
- })
347
- ```
348
-
349
- ### `merge(pdfs): Promise<Uint8Array>`
350
-
351
- Combine pre-rendered PDFs:
352
-
353
- ```typescript
354
- import { merge } from 'pretext-pdf'
355
-
356
- const combined = await merge([coverPdf, bodyPdf, appendixPdf])
357
- ```
358
-
359
- ### `assemble(parts): Promise<Uint8Array>`
360
-
361
- Mix new document configs with existing PDFs:
362
-
363
- ```typescript
364
- import { assemble } from 'pretext-pdf'
365
-
366
- const report = await assemble([
367
- { pdf: existingCoverPdf },
368
- { doc: { content: [...] } }, // rendered fresh
369
- { pdf: standardTermsPdf },
370
- ])
371
- ```
372
-
373
- ---
374
-
375
- ## Error Handling
376
-
377
- Every error throws `PretextPdfError` with a typed code:
378
-
379
- ```typescript
380
- import { render, PretextPdfError } from 'pretext-pdf'
381
-
382
- try {
383
- const pdf = await render(config)
384
- } catch (err) {
385
- if (err instanceof PretextPdfError) {
386
- switch (err.code) {
387
- case 'VALIDATION_ERROR': // Invalid config
388
- case 'FONT_LOAD_FAILED': // Font file not found
389
- case 'IMAGE_TOO_TALL': // Image doesn't fit on page
390
- case 'ASSEMBLY_EMPTY': // merge/assemble called with empty array
391
- // ... see CHANGELOG.md for full list
392
- }
393
- }
394
- }
395
- ```
396
-
397
- ---
398
-
399
- ## Troubleshooting
400
-
401
- ### Hyphenation language not found
402
-
403
- ```
404
- UNSUPPORTED_LANGUAGE: Language 'en-US' not supported
405
- ```
406
-
407
- Use **lowercase** language codes that match the npm package name:
408
-
409
- ```typescript
410
- // Wrong — 'en-US' fails on Linux (case-sensitive filesystem)
411
- hyphenation: { language: 'en-US' }
412
-
413
- // Correct matches 'hyphenation.en-us' package name
414
- hyphenation: { language: 'en-us' }
415
- ```
416
-
417
- ### Encryption
418
-
419
- Encryption is built-in since v0.4.0. Add `encryption` to your document config:
420
-
421
- ```typescript
422
- const pdf = await render({
423
- encryption: {
424
- userPassword: 'open123',
425
- ownerPassword: 'admin456',
426
- permissions: { printing: true, copying: false, modifying: false }
427
- },
428
- content: [...]
429
- })
430
- ```
431
-
432
- ### SVG rendering requires optional dependency
433
-
434
- Install `@napi-rs/canvas` for SVG support:
435
-
436
- ```bash
437
- npm install @napi-rs/canvas
438
- ```
439
-
440
- ### PDF is blank or too small
441
-
442
- Check margins if left+right margins exceed page width, content width becomes negative:
443
-
444
- ```typescript
445
- // For narrow pages, reduce margins:
446
- margins: { top: 36, bottom: 36, left: 36, right: 36 }
447
- ```
448
-
449
- ### Form fields not interactive after flattenForms
450
-
451
- `flattenForms: true` bakes fields into static content — by design. Remove it to keep interactive.
452
-
453
- ---
454
-
455
- ## Test Coverage
456
-
457
- 437+ tests across all phases with 100% pass rate:
458
-
459
- ```bash
460
- npm test # Full suite (unit + e2e + all phases)
461
- npm run test:unit # Validation, builder, rich-text unit tests
462
- npm run test:e2e # End-to-end render tests
463
- npm run test:phase-7 # Phase 7A-7G feature tests
464
- npm run test:phase-8 # Phase 8A-8H feature tests
465
- npm run test:phases # All phase tests (7–9, performance, signatures)
466
- ```
467
-
468
- **Coverage**: Type safety, path validation, error handling, boundary cases, crypto signing, document assembly, and all content elements.
469
-
470
- ---
471
-
472
- ## Custom Fonts
473
-
474
- ```typescript
475
- const pdf = await render({
476
- fonts: [
477
- { family: 'Roboto', weight: 400, src: '/path/to/Roboto-Regular.ttf' },
478
- { family: 'Roboto', weight: 700, src: '/path/to/Roboto-Bold.ttf' },
479
- { family: 'Roboto', style: 'italic', src: '/path/to/Roboto-Italic.ttf' },
480
- ],
481
- defaultFont: 'Roboto',
482
- content: [
483
- { type: 'paragraph', text: 'Uses Roboto font' },
484
- { type: 'paragraph', text: 'Bold text', fontWeight: 700 },
485
- ],
486
- })
487
- ```
488
-
489
- ---
490
-
491
- ## Rich Text
492
-
493
- ```typescript
494
- {
495
- type: 'rich-paragraph',
496
- fontSize: 13,
497
- spans: [
498
- { text: 'Normal ' },
499
- { text: 'bold', fontWeight: 700 },
500
- { text: ' and ', fontStyle: 'italic' },
501
- { text: 'colored', color: '#e63946' },
502
- { text: ' and ' },
503
- { text: 'linked', href: 'https://example.com', underline: true, color: '#0070f3' },
504
- { text: '. Also: E=mc' },
505
- { text: '2', verticalAlign: 'superscript' },
506
- { text: ' and H' },
507
- { text: '2', verticalAlign: 'subscript' },
508
- { text: 'O.' },
509
- ],
510
- }
511
- ```
512
-
513
- ---
514
-
515
- ## Roadmap
516
-
517
- | Phase | Feature | Status |
518
- |-------|---------|--------|
519
- | 1–4 | Core engine, pagination, typography | ✅ |
520
- | 5 | Rich text / builder API | ✅ |
521
- | 6 | Headers/footers, columns, decoration | ✅ |
522
- | 7A | PDF Bookmarks / Outline | ✅ |
523
- | 7B | Watermarks | |
524
- | 7C | Hyphenation | |
525
- | 7D | Table of Contents | ✅ |
526
- | 7E | SVG support | ✅ |
527
- | 7F | RTL text (Arabic/Hebrew) | ✅ |
528
- | 7G | Encryption | ✅ |
529
- | 8A | Sticky note annotations | ✅ |
530
- | 8B | Interactive forms (text/checkbox/radio/dropdown/button) | ✅ |
531
- | 8C | Document assembly (merge + assemble) | ✅ |
532
- | 8D | Callout boxes (info/warning/tip/note) | ✅ |
533
- | 8E | Signature placeholder | ✅ |
534
- | 8F | Document metadata (language, producer) | ✅ |
535
- | 8G | Hyperlinks | ✅ |
536
- | 8H | Inline formatting (super/subscript, letterSpacing, smallCaps) | ✅ |
537
- | 9A | Digital signatures (cryptographic, PKCS#7) | 🔜 |
538
- | 9B | Image floats (text flowing around images) | 🔜 |
539
- | 9C | Font subsetting pre-computation | 🔜 |
540
-
541
- ---
542
-
543
- ## Migration from pdfmake
544
-
545
- 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.
546
-
547
- ---
548
-
549
- ## Contributing
550
-
551
- See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
552
-
553
- ---
554
-
555
- ## License
556
-
557
- [MIT](LICENSE)
558
-
559
- ---
560
-
561
- ## Credits
562
-
563
- Built by [Himanshu Jain](https://github.com/Himaan1998Y) on top of:
564
- - **[pretext](https://github.com/chenglou/pretext)** — Text layout engine (Cheng Lou)
565
- - **[pdf-lib](https://github.com/Hopding/pdf-lib)** — PDF manipulation
566
- - **[@napi-rs/canvas](https://github.com/napi-rs/canvas)** Server-side Canvas API for Node.js
567
-
568
- Questions? [Open an issue](https://github.com/Himaan1998Y/pretext-pdf/issues)
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)