pretext-pdf 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +476 -462
- package/README.md +658 -749
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js +21 -8
- package/dist/fonts.js.map +1 -1
- package/package.json +175 -175
package/README.md
CHANGED
|
@@ -1,749 +1,658 @@
|
|
|
1
|
-
# pretext-pdf
|
|
2
|
-
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/pretext-pdf)
|
|
8
|
-
[](https://www.npmjs.com/package/pretext-pdf)
|
|
9
|
-
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
|
|
10
|
-
[](https://www.typescriptlang.org/)
|
|
11
|
-
[![
|
|
12
|
-
[![
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
**[
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
npm install
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
##
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
npm
|
|
596
|
-
npm run test:
|
|
597
|
-
npm run test:
|
|
598
|
-
npm run test:
|
|
599
|
-
npm run test:
|
|
600
|
-
npm run test:
|
|
601
|
-
npm run test:
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/pretext-pdf)
|
|
8
|
+
[](https://www.npmjs.com/package/pretext-pdf)
|
|
9
|
+
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](#test-coverage)
|
|
12
|
+
[](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
|
+
| [](examples/showcase-invoice.ts) | [](examples/showcase-report.ts) | [](examples/showcase-resume.ts) |
|
|
159
|
+
| [View source](examples/showcase-invoice.ts) | [View source](examples/showcase-report.ts) | [View source](examples/showcase-resume.ts) |
|
|
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/).
|