pretext-pdf 0.9.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +874 -795
- package/dist/assets.d.ts +4 -2
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +22 -1
- package/dist/assets.js.map +1 -1
- package/dist/benchmarks/corpora.d.ts +11 -0
- package/dist/benchmarks/corpora.d.ts.map +1 -0
- package/dist/benchmarks/corpora.js +228 -0
- package/dist/benchmarks/corpora.js.map +1 -0
- package/dist/builder.d.ts +10 -2
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +12 -4
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +19 -19
- package/dist/element-types.d.ts +16 -0
- package/dist/element-types.d.ts.map +1 -0
- package/dist/element-types.js +16 -0
- package/dist/element-types.js.map +1 -0
- package/dist/errors.d.ts +8 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/fonts.d.ts +2 -1
- package/dist/fonts.d.ts.map +1 -1
- package/dist/fonts.js.map +1 -1
- package/dist/index.d.ts +21 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -309
- package/dist/index.js.map +1 -1
- package/dist/layout-state.d.ts +39 -0
- package/dist/layout-state.d.ts.map +1 -0
- package/dist/layout-state.js +46 -0
- package/dist/layout-state.js.map +1 -0
- package/dist/measure-blocks.d.ts +2 -1
- package/dist/measure-blocks.d.ts.map +1 -1
- package/dist/measure-blocks.js.map +1 -1
- package/dist/measure.d.ts +4 -2
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +15 -5
- package/dist/measure.js.map +1 -1
- package/dist/page-sizes.d.ts +1 -0
- package/dist/page-sizes.d.ts.map +1 -1
- package/dist/page-sizes.js.map +1 -1
- package/dist/paginate.d.ts +1 -1
- package/dist/paginate.d.ts.map +1 -1
- package/dist/paginate.js +11 -2
- package/dist/paginate.js.map +1 -1
- package/dist/pipeline-footnotes.d.ts +18 -0
- package/dist/pipeline-footnotes.d.ts.map +1 -0
- package/dist/pipeline-footnotes.js +98 -0
- package/dist/pipeline-footnotes.js.map +1 -0
- package/dist/pipeline-toc.d.ts +11 -0
- package/dist/pipeline-toc.d.ts.map +1 -0
- package/dist/pipeline-toc.js +28 -0
- package/dist/pipeline-toc.js.map +1 -0
- package/dist/pipeline.d.ts +24 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +116 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/plugin-registry.d.ts +40 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +104 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/plugin-types.d.ts +185 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +27 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/post-process.d.ts +16 -0
- package/dist/post-process.d.ts.map +1 -0
- package/dist/post-process.js +80 -0
- package/dist/post-process.js.map +1 -0
- package/dist/render-blocks.d.ts +2 -1
- package/dist/render-blocks.d.ts.map +1 -1
- package/dist/render-blocks.js.map +1 -1
- package/dist/render-extras.d.ts +2 -2
- package/dist/render-extras.d.ts.map +1 -1
- package/dist/render-extras.js.map +1 -1
- package/dist/render.d.ts +4 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +23 -4
- package/dist/render.js.map +1 -1
- package/dist/types-internal.d.ts +302 -0
- package/dist/types-internal.d.ts.map +1 -0
- package/dist/types-internal.js +9 -0
- package/dist/types-internal.js.map +1 -0
- package/dist/types-public.d.ts +1031 -0
- package/dist/types-public.d.ts.map +1 -0
- package/dist/types-public.js +2 -0
- package/dist/types-public.js.map +1 -0
- package/dist/types.d.ts +6 -1224
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +6 -4
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +35 -4
- package/dist/validate.js.map +1 -1
- package/package.json +192 -184
- package/docs/screenshots/showcase-invoice.png +0 -0
- package/docs/screenshots/showcase-report.png +0 -0
- package/docs/screenshots/showcase-resume.png +0 -0
package/README.md
CHANGED
|
@@ -1,795 +1,874 @@
|
|
|
1
|
-
# pretext-pdf
|
|
2
|
-
|
|
3
|
-
> **The PDF library AI agents speak natively — and humans love writing.**
|
|
4
|
-
>
|
|
5
|
-
> A `PdfDocument` is plain JSON. LLMs emit it in one shot — no codegen, no headless browser, no `eval`.
|
|
6
|
-
> Humans get a strict-typed declarative API for invoices, reports, resumes, and templates.
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/pretext-pdf)
|
|
9
|
-
[](https://www.npmjs.com/package/pretext-pdf)
|
|
10
|
-
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
|
|
11
|
-
[](https://www.typescriptlang.org/)
|
|
12
|
-
[](LICENSE)
|
|
14
|
-
[](#runtime-footprint)
|
|
15
|
-
|
|
16
|
-
**[Live demo](https://himaan1998y.github.io/pretext-pdf/)** · **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** (MCP server) · **[Migrating from pdfmake?](#migrating-from-pdfmake)**
|
|
17
|
-
|
|
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
|
-
## Table of contents
|
|
23
|
-
|
|
24
|
-
- [Why pretext-pdf](#why-pretext-pdf)
|
|
25
|
-
- [Install](#install)
|
|
26
|
-
- [Quick start](#quick-start)
|
|
27
|
-
- [Library API](#library-api)
|
|
28
|
-
- [CLI](#cli)
|
|
29
|
-
- [Markdown](#markdown)
|
|
30
|
-
- [Templates](#templates)
|
|
31
|
-
- [pdfmake migration](#migrating-from-pdfmake)
|
|
32
|
-
- [MCP server (Claude / Cursor / Windsurf)](#mcp-server-claude--cursor--windsurf)
|
|
33
|
-
- [Built for AI agents](#built-for-ai-agents)
|
|
34
|
-
- [Element catalog](#element-catalog)
|
|
35
|
-
- [Document features](#document-level-features)
|
|
36
|
-
- [API reference](#api-reference)
|
|
37
|
-
- [Strict validation](#strict-validation)
|
|
38
|
-
- [India / GST invoicing](#india--gst-invoicing)
|
|
39
|
-
- [Custom fonts](#custom-fonts)
|
|
40
|
-
- [Rich text](#rich-text)
|
|
41
|
-
- [Footnotes](#footnotes)
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{ width:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{ cells: [{ text: '
|
|
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
|
-
{ type: '
|
|
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
|
-
{ pdf:
|
|
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
|
-
{ family: 'Roboto',
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
{ text: '
|
|
541
|
-
{ text: '
|
|
542
|
-
{ text: ' and ' },
|
|
543
|
-
{ text: '
|
|
544
|
-
{ text: '
|
|
545
|
-
{ text: '
|
|
546
|
-
{ text: '
|
|
547
|
-
{ text: '2', verticalAlign: '
|
|
548
|
-
{ text: '
|
|
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
|
-
{ text: '
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
```bash
|
|
649
|
-
npm
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
- **
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1
|
+
# pretext-pdf
|
|
2
|
+
|
|
3
|
+
> **The PDF library AI agents speak natively — and humans love writing.**
|
|
4
|
+
>
|
|
5
|
+
> A `PdfDocument` is plain JSON. LLMs emit it in one shot — no codegen, no headless browser, no `eval`.
|
|
6
|
+
> Humans get a strict-typed declarative API for invoices, reports, resumes, and templates.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/pretext-pdf)
|
|
9
|
+
[](https://www.npmjs.com/package/pretext-pdf)
|
|
10
|
+
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](#tests)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](#runtime-footprint)
|
|
15
|
+
|
|
16
|
+
**[Live demo](https://himaan1998y.github.io/pretext-pdf/)** · **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** (MCP server) · **[Migrating from pdfmake?](#migrating-from-pdfmake)**
|
|
17
|
+
|
|
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
|
+
## Table of contents
|
|
23
|
+
|
|
24
|
+
- [Why pretext-pdf](#why-pretext-pdf)
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [Library API](#library-api)
|
|
28
|
+
- [CLI](#cli)
|
|
29
|
+
- [Markdown](#markdown)
|
|
30
|
+
- [Templates](#templates)
|
|
31
|
+
- [pdfmake migration](#migrating-from-pdfmake)
|
|
32
|
+
- [MCP server (Claude / Cursor / Windsurf)](#mcp-server-claude--cursor--windsurf)
|
|
33
|
+
- [Built for AI agents](#built-for-ai-agents)
|
|
34
|
+
- [Element catalog](#element-catalog)
|
|
35
|
+
- [Document features](#document-level-features)
|
|
36
|
+
- [API reference](#api-reference)
|
|
37
|
+
- [Strict validation](#strict-validation)
|
|
38
|
+
- [India / GST invoicing](#india--gst-invoicing)
|
|
39
|
+
- [Custom fonts](#custom-fonts)
|
|
40
|
+
- [Rich text](#rich-text)
|
|
41
|
+
- [Footnotes](#footnotes)
|
|
42
|
+
- [Custom element types (plugins)](#custom-element-types-plugins)
|
|
43
|
+
- [Examples](#examples)
|
|
44
|
+
- [Error handling](#error-handling)
|
|
45
|
+
- [Troubleshooting](#troubleshooting)
|
|
46
|
+
- [Non-goals](#non-goals)
|
|
47
|
+
- [Runtime footprint](#runtime-footprint)
|
|
48
|
+
- [Compatibility matrix](#compatibility-matrix)
|
|
49
|
+
- [Performance](#performance)
|
|
50
|
+
- [Tests](#tests)
|
|
51
|
+
- [Security](#security)
|
|
52
|
+
- [Roadmap](#roadmap)
|
|
53
|
+
- [Contributing](#contributing)
|
|
54
|
+
- [Credits](#credits)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why pretext-pdf
|
|
59
|
+
|
|
60
|
+
Three established camps in JS PDF generation, and one gap. pretext-pdf lives in the gap.
|
|
61
|
+
|
|
62
|
+
| | pdfmake / jsPDF / pdfkit | Puppeteer / Playwright | LaTeX / WeasyPrint | **pretext-pdf** |
|
|
63
|
+
|---|---|---|---|---|
|
|
64
|
+
| Lightweight (no Chromium) | ✅ | ❌ ~300 MB | ❌ native binaries | ✅ |
|
|
65
|
+
| Pure ESM, runs in serverless | ✅ | ⚠️ painful in Lambda | ❌ | ✅ |
|
|
66
|
+
| Professional typography (kerning, hyphenation, RTL/CJK) | ❌ | ✅ | ✅ | ✅ |
|
|
67
|
+
| Declarative — describe the document, don't draw it | ⚠️ partial | ❌ | ❌ | ✅ |
|
|
68
|
+
| **LLM emits a working document in one shot** | ❌ requires codegen loop | ❌ requires HTML+CSS knowledge | ❌ requires LaTeX knowledge | ✅ pure JSON |
|
|
69
|
+
| MCP server out of the box | ❌ | ❌ | ❌ | ✅ |
|
|
70
|
+
| Drop-in CLI for shell pipelines | ❌ | ⚠️ wrap with code | ⚠️ separate binary | ✅ `pretext-pdf in.json out.pdf` |
|
|
71
|
+
| pdfmake migration shim | — | ❌ | ❌ | ✅ `fromPdfmake()` |
|
|
72
|
+
|
|
73
|
+
**The headline:** every other JS PDF library asks an LLM (or you) to *write code*. pretext-pdf asks for a JSON object. That difference is what makes agent-generated PDFs reliable — and the same shape happens to be a clean declarative API for humans too.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install pretext-pdf
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> **ESM only** — use `import`, not `require`. Requires Node.js ≥ 18.
|
|
84
|
+
|
|
85
|
+
Optional peer dependencies — install only what you use:
|
|
86
|
+
|
|
87
|
+
| Peer | When you need it |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `@napi-rs/canvas` | SVG / qr-code / barcode / chart elements (Node only — browser uses `OffscreenCanvas`) |
|
|
90
|
+
| `qrcode` | `qr-code` element |
|
|
91
|
+
| `bwip-js` | `barcode` element (100+ symbologies) |
|
|
92
|
+
| `vega` + `vega-lite` | `chart` element |
|
|
93
|
+
| `marked` | `pretext-pdf/markdown` entry point and `--markdown` CLI flag |
|
|
94
|
+
| `@signpdf/signpdf` | PKCS#7 cryptographic signing |
|
|
95
|
+
|
|
96
|
+
> **Encryption is built-in** since v0.4.0 — no extra install.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Quick start
|
|
101
|
+
|
|
102
|
+
### Library API
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { render } from 'pretext-pdf'
|
|
106
|
+
import { writeFileSync } from 'fs'
|
|
107
|
+
|
|
108
|
+
const pdf = await render({
|
|
109
|
+
pageSize: 'A4',
|
|
110
|
+
margins: { top: 40, bottom: 40, left: 50, right: 50 },
|
|
111
|
+
metadata: { title: 'Invoice #001', author: 'Acme Corp' },
|
|
112
|
+
content: [
|
|
113
|
+
{ type: 'heading', level: 1, text: 'Invoice #12345' },
|
|
114
|
+
{ type: 'paragraph', text: 'Thank you for your business.', fontSize: 12 },
|
|
115
|
+
{
|
|
116
|
+
type: 'table',
|
|
117
|
+
columns: [
|
|
118
|
+
{ width: 200 },
|
|
119
|
+
{ width: 50, align: 'right' },
|
|
120
|
+
{ width: 100, align: 'right' },
|
|
121
|
+
],
|
|
122
|
+
rows: [
|
|
123
|
+
{ isHeader: true, cells: [{ text: 'Item', fontWeight: 700 }, { text: 'Qty', fontWeight: 700 }, { text: 'Price', fontWeight: 700 }] },
|
|
124
|
+
{ cells: [{ text: 'Professional Services' }, { text: '10' }, { text: '$1,000' }] },
|
|
125
|
+
{ cells: [{ text: 'Hosting (annual)' }, { text: '1' }, { text: '$500' }] },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{ type: 'paragraph', text: 'Total: $1,500', align: 'right', fontWeight: 700 },
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
writeFileSync('invoice.pdf', pdf)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### CLI
|
|
136
|
+
|
|
137
|
+
`pretext-pdf` ships with a binary that turns a JSON or Markdown file into a PDF — no Node code required.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# JSON in, PDF out
|
|
141
|
+
pretext-pdf doc.json invoice.pdf
|
|
142
|
+
|
|
143
|
+
# Stdin → stdout (pipe-friendly)
|
|
144
|
+
echo '{"content":[{"type":"heading","level":1,"text":"Hi"}]}' | pretext-pdf > out.pdf
|
|
145
|
+
|
|
146
|
+
# Markdown straight to PDF
|
|
147
|
+
pretext-pdf --markdown --code-font 'Courier New' README.md docs.pdf
|
|
148
|
+
|
|
149
|
+
# Help / version
|
|
150
|
+
pretext-pdf --help
|
|
151
|
+
pretext-pdf --version
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Flag | Meaning |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `-i, --input <path>` | Read input from file (default: first positional, or stdin) |
|
|
157
|
+
| `-o, --output <path>` | Write PDF to file (default: second positional, or stdout) |
|
|
158
|
+
| `--markdown` | Treat input as Markdown — converts via `pretext-pdf/markdown` |
|
|
159
|
+
| `--code-font <name>` | With `--markdown`, font family for fenced code blocks |
|
|
160
|
+
| `-v, --version` | Print version |
|
|
161
|
+
| `-h, --help` | Print help |
|
|
162
|
+
|
|
163
|
+
Exit codes: `0` success, `1` user error (bad args, invalid JSON), `2` render error.
|
|
164
|
+
|
|
165
|
+
### Markdown
|
|
166
|
+
|
|
167
|
+
Convert any Markdown string to `ContentElement[]` in one call. Requires `marked` peer dep.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { markdownToContent } from 'pretext-pdf/markdown'
|
|
171
|
+
import { render } from 'pretext-pdf'
|
|
172
|
+
|
|
173
|
+
const md = `
|
|
174
|
+
# Q1 2026 Report
|
|
175
|
+
|
|
176
|
+
Revenue grew **18%** year-over-year.
|
|
177
|
+
|
|
178
|
+
| Metric | Q4 2025 | Q1 2026 | Change |
|
|
179
|
+
|--------|--------:|--------:|:------:|
|
|
180
|
+
| Revenue | $45M | $60M | +33% |
|
|
181
|
+
| Margin | 62% | 68% | +6pp |
|
|
182
|
+
|
|
183
|
+
- [x] Cloud expansion launched
|
|
184
|
+
- [x] Enterprise pipeline doubled
|
|
185
|
+
- [ ] APAC region opening Q2
|
|
186
|
+
|
|
187
|
+
> All figures in USD millions.
|
|
188
|
+
`
|
|
189
|
+
|
|
190
|
+
const content = await markdownToContent(md, { codeFontFamily: 'Courier New' })
|
|
191
|
+
const pdf = await render({ content })
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Supported: headings h1–h4, bold, italic, strikethrough, inline code, links, ordered/unordered lists (recursive nesting), **GFM tables (with column alignment)**, **GFM task lists** (☑/☐), fenced code blocks, blockquotes, horizontal rules.
|
|
195
|
+
|
|
196
|
+
### Templates
|
|
197
|
+
|
|
198
|
+
Pre-built zero-dependency template functions:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
|
|
202
|
+
import { render } from 'pretext-pdf'
|
|
203
|
+
|
|
204
|
+
const content = createInvoice({
|
|
205
|
+
from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
|
|
206
|
+
to: { name: 'Client Ltd', address: '456 Oak Ave' },
|
|
207
|
+
invoiceNumber: 'INV-2026-001',
|
|
208
|
+
date: '2026-04-20',
|
|
209
|
+
items: [{ description: 'Consulting', quantity: 10, unitPrice: 150 }],
|
|
210
|
+
currency: '$', taxRate: 10, taxLabel: 'GST',
|
|
211
|
+
qrData: 'upi://pay?pa=acme@bank&am=1650',
|
|
212
|
+
})
|
|
213
|
+
const pdf = await render({ content })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Available: `createInvoice` (any currency), `createGstInvoice` (India GST/IGST/CGST+SGST + UPI QR + amount-in-words), `createReport` (with optional TOC).
|
|
217
|
+
|
|
218
|
+
### Migrating from pdfmake
|
|
219
|
+
|
|
220
|
+
`pretext-pdf/compat` translates pdfmake document descriptors into a `PdfDocument` — most common patterns work without code changes.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { fromPdfmake } from 'pretext-pdf/compat'
|
|
224
|
+
import { render } from 'pretext-pdf'
|
|
225
|
+
|
|
226
|
+
// Existing pdfmake document, unchanged
|
|
227
|
+
const pdfmakeDoc = {
|
|
228
|
+
pageSize: 'LETTER',
|
|
229
|
+
pageMargins: [40, 60, 40, 60],
|
|
230
|
+
defaultStyle: { fontSize: 11 },
|
|
231
|
+
styles: {
|
|
232
|
+
header: { fontSize: 22, bold: true },
|
|
233
|
+
subheader: { fontSize: 16 },
|
|
234
|
+
},
|
|
235
|
+
content: [
|
|
236
|
+
{ text: 'Invoice #001', style: 'header' },
|
|
237
|
+
{ text: 'Acme Corp', style: 'subheader' },
|
|
238
|
+
'Thanks for your business.',
|
|
239
|
+
{
|
|
240
|
+
table: {
|
|
241
|
+
widths: ['*', 'auto', 80],
|
|
242
|
+
headerRows: 1,
|
|
243
|
+
body: [
|
|
244
|
+
['Item', 'Qty', 'Price'],
|
|
245
|
+
['Widget', '3', '$30'],
|
|
246
|
+
['Sprocket', '5', '$50'],
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{ ul: ['Net 30 terms', 'Late fee: 1.5%/mo'] },
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const pdf = await render(fromPdfmake(pdfmakeDoc))
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| pdfmake feature | Compat support |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `string` content | ✅ → paragraph |
|
|
260
|
+
| `{ text, bold, italics, color, fontSize, alignment, font }` | ✅ → paragraph or rich-paragraph |
|
|
261
|
+
| `{ text, style: 'header' }` (style lookup) | ✅ — `header`/`h1`/`title` map to heading 1, `subheader`/`h2` to 2, etc. |
|
|
262
|
+
| `{ ul }` / `{ ol }` (recursive) | ✅ → list |
|
|
263
|
+
| `{ table: { body, widths, headerRows } }` | ✅ → table |
|
|
264
|
+
| `{ image, width, height }` | ✅ → image |
|
|
265
|
+
| `{ qr, fit }` | ✅ → qr-code |
|
|
266
|
+
| `{ pageBreak: 'before' \| 'after' }` | ✅ → page-break |
|
|
267
|
+
| `{ stack }` | ✅ → flattened inline |
|
|
268
|
+
| `{ link }` on inline text | ✅ → span.href |
|
|
269
|
+
| `pageSize`, `pageOrientation`, `pageMargins` | ✅ |
|
|
270
|
+
| `info` (title/author/subject/keywords) | ✅ → metadata |
|
|
271
|
+
| `header`, `footer` (string form) | ✅ |
|
|
272
|
+
| `{ columns }` | ⚠️ flattened with a warning |
|
|
273
|
+
| `{ canvas }` | ❌ unsupported (drawing primitives) |
|
|
274
|
+
| Function-style `header`/`footer` | ❌ pass a string |
|
|
275
|
+
|
|
276
|
+
Override the heading-name mapping via `fromPdfmake(doc, { headingMap: { ... } })`.
|
|
277
|
+
|
|
278
|
+
### MCP server (Claude / Cursor / Windsurf)
|
|
279
|
+
|
|
280
|
+
Drop into any MCP-aware AI agent in 60 seconds:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"mcpServers": {
|
|
285
|
+
"pretext-pdf": {
|
|
286
|
+
"command": "npx",
|
|
287
|
+
"args": ["-y", "pretext-pdf-mcp"]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Exposes: `generate_pdf`, `generate_invoice`, `generate_report`, `generate_from_markdown`, `list_element_types`. Versioned alongside this library — see [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Built for AI agents
|
|
298
|
+
|
|
299
|
+
A `PdfDocument` is a plain JSON object. No functions are required. Every field is optional except `type` and a few element-specific essentials. That shape is exactly what an LLM can produce reliably with no tool-use loop.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { render } from 'pretext-pdf'
|
|
303
|
+
|
|
304
|
+
// Whatever produced this JSON — Claude, GPT, a workflow node, a form submission — works the same
|
|
305
|
+
const pdf = await render({
|
|
306
|
+
metadata: { title: 'AI-generated quarterly report' },
|
|
307
|
+
content: [
|
|
308
|
+
{ type: 'heading', level: 1, text: 'Q1 2026 Summary' },
|
|
309
|
+
{ type: 'paragraph', text: 'Revenue grew 18% YoY.' },
|
|
310
|
+
{ type: 'table', columns: [/* ... */], rows: [/* ... */] },
|
|
311
|
+
],
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Why JSON-first matters for agents
|
|
316
|
+
|
|
317
|
+
- **No code execution loop.** Model returns JSON; you call `render()`. No sandbox, no `vm`, no Vercel Sandbox roundtrip.
|
|
318
|
+
- **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).
|
|
319
|
+
- **Self-correcting errors.** Every failure throws `PretextPdfError` with a typed `code`. Feed it back to the model and it fixes itself.
|
|
320
|
+
- **Progressive disclosure.** Optional peer deps mean agents only ask for QR codes, charts, or markdown when needed — token-efficient prompts.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Element catalog
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
paragraph heading(1-4) spacer hr page-break
|
|
328
|
+
table image svg list code
|
|
329
|
+
blockquote rich-paragraph callout comment form-field
|
|
330
|
+
toc qr-code barcode chart footnote-def
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
| Element | What it does |
|
|
334
|
+
| --- | --- |
|
|
335
|
+
| `paragraph` | Text block — font, size, color, align, background, letterSpacing, smallCaps, tabularNumbers, multi-column (`columns` + `columnGap`), RTL (`dir`) |
|
|
336
|
+
| `heading` | H1–H4 with bookmarks, URL links, internal anchors, tabularNumbers, RTL |
|
|
337
|
+
| `table` | Fixed/proportional/auto columns, colspan, rowspan, repeating headers across page breaks |
|
|
338
|
+
| `image` | PNG/JPG/WebP with sizing, alignment, float left/right with `floatText` or rich `floatSpans` |
|
|
339
|
+
| `list` | Ordered/unordered, recursive nesting, `nestedNumberingStyle: 'restart' \| 'continue'` |
|
|
340
|
+
| `code` | Monospace block with background and padding |
|
|
341
|
+
| `blockquote` | Left border + background |
|
|
342
|
+
| `rich-paragraph` | Mixed bold/italic/color/size/super/subscript spans with inline hyperlinks |
|
|
343
|
+
| `svg` | Embedded SVG graphics with auto-sizing from viewBox |
|
|
344
|
+
| `toc` | Auto-generated table of contents with accurate page numbers (two-pass) |
|
|
345
|
+
| `qr-code` | Scannable QR code — UPI, URLs, vCards. Requires `qrcode` peer dep. |
|
|
346
|
+
| `barcode` | 100+ symbologies — EAN-13, Code128, PDF417, DataMatrix, etc. Requires `bwip-js`. |
|
|
347
|
+
| `chart` | Vega-Lite data visualisation as vector SVG. Requires `vega` + `vega-lite`. |
|
|
348
|
+
| `comment` | PDF sticky-note annotation (visible in Acrobat/Preview sidebar) |
|
|
349
|
+
| `form-field` | Interactive text/checkbox/radio/dropdown/button (with `flattenForms` to bake) |
|
|
350
|
+
| `callout` | Info / warning / tip / note callout boxes |
|
|
351
|
+
| `footnote-def` | Paired with `span.footnoteRef` for proper footnote numbering + zone reservation |
|
|
352
|
+
| `hr` / `spacer` / `page-break` | Layout primitives |
|
|
353
|
+
|
|
354
|
+
### Document-level features
|
|
355
|
+
|
|
356
|
+
| Feature | Config key | Notes |
|
|
357
|
+
| --- | --- | --- |
|
|
358
|
+
| Watermarks | `doc.watermark` | Text or image, opacity, rotation |
|
|
359
|
+
| Encryption | `doc.encryption` | Password + granular permissions, built-in |
|
|
360
|
+
| Cryptographic signing | `doc.signature: { p12, passphrase, ... }` | PKCS#7, optional `@signpdf/signpdf` |
|
|
361
|
+
| PDF Bookmarks | `doc.bookmarks` | Auto-generated from headings |
|
|
362
|
+
| Hyphenation | `doc.hyphenation` | Liang's algorithm, e.g. `language: 'en-us'` |
|
|
363
|
+
| Headers/Footers | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}` tokens |
|
|
364
|
+
| Per-section overrides | `doc.sections` | Different header/footer per page range |
|
|
365
|
+
| Metadata | `doc.metadata` | Title, author, subject, keywords, language, producer |
|
|
366
|
+
| Hyperlinks | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` | External, mailto, internal anchors |
|
|
367
|
+
| Document assembly | `merge(pdfs)`, `assemble(parts)` | Combine pre-rendered + freshly rendered |
|
|
368
|
+
| Path-traversal lockdown | `doc.allowedFileDirs` | Restrict file-source reads to listed dirs |
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## API reference
|
|
373
|
+
|
|
374
|
+
### `render(doc): Promise<Uint8Array>`
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { render } from 'pretext-pdf'
|
|
378
|
+
|
|
379
|
+
const pdf = await render({
|
|
380
|
+
pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | 'Tabloid' | [w, h]
|
|
381
|
+
margins: { top: 72, bottom: 72, left: 72, right: 72 },
|
|
382
|
+
defaultFont: 'Inter', // Inter 400/700 bundled
|
|
383
|
+
defaultFontSize: 12,
|
|
384
|
+
metadata: { title: '...', author: '...', keywords: ['pdf'] },
|
|
385
|
+
watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
|
|
386
|
+
encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
|
|
387
|
+
bookmarks: { minLevel: 1, maxLevel: 3 },
|
|
388
|
+
hyphenation: { language: 'en-us', minWordLength: 6 },
|
|
389
|
+
header: { text: '{{pageNumber}} of {{totalPages}}', align: 'right' },
|
|
390
|
+
footer: { text: 'Confidential', align: 'center', color: '#999' },
|
|
391
|
+
content: [ /* ContentElement[] */ ],
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### `merge(pdfs): Promise<Uint8Array>`
|
|
396
|
+
|
|
397
|
+
Combine pre-rendered PDFs:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import { merge } from 'pretext-pdf'
|
|
401
|
+
const combined = await merge([coverPdf, bodyPdf, appendixPdf])
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### `assemble(parts): Promise<Uint8Array>`
|
|
405
|
+
|
|
406
|
+
Mix new docs with existing PDFs:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { assemble } from 'pretext-pdf'
|
|
410
|
+
|
|
411
|
+
const report = await assemble([
|
|
412
|
+
{ pdf: existingCoverPdf },
|
|
413
|
+
{ doc: { content: [/* fresh */] } },
|
|
414
|
+
{ pdf: standardTermsPdf },
|
|
415
|
+
])
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### `createPdf(opts): PdfBuilder` (fluent builder)
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { createPdf } from 'pretext-pdf'
|
|
422
|
+
|
|
423
|
+
const pdf = await createPdf({ pageSize: 'A4' })
|
|
424
|
+
.addHeading('My Report', 1)
|
|
425
|
+
.addText('Fluent chainable API.')
|
|
426
|
+
.addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
|
|
427
|
+
.build()
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### `markdownToContent(md, opts?)` *(from `pretext-pdf/markdown`)*
|
|
431
|
+
|
|
432
|
+
### `createInvoice / createGstInvoice / createReport` *(from `pretext-pdf/templates`)*
|
|
433
|
+
|
|
434
|
+
### `fromPdfmake(doc, opts?)` *(from `pretext-pdf/compat`)*
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Strict validation
|
|
439
|
+
|
|
440
|
+
By default, `render()` uses permissive validation — unknown properties are silently ignored. Enable strict mode to catch typos and ensure property names match the schema exactly:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { render } from 'pretext-pdf'
|
|
444
|
+
|
|
445
|
+
const pdf = await render(doc, { strict: true })
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
In strict mode:
|
|
449
|
+
|
|
450
|
+
- **Unknown properties are rejected** with a `VALIDATION_ERROR` that includes:
|
|
451
|
+
- Property name and location (JSONPath-like: `content[3].table.rows[0].cells[1].align`)
|
|
452
|
+
- Typo suggestions via Levenshtein distance (edit distance ≤2)
|
|
453
|
+
- All violations collected before throwing, with a 20-error cap + overflow indicator
|
|
454
|
+
|
|
455
|
+
Example error:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
VALIDATION_ERROR:
|
|
459
|
+
unknown property 'fontSizee' at content[0].fontSizee (did you mean fontsize, fontSize?)
|
|
460
|
+
unknown property 'colorr' at content[1].inline.colorr (did you mean color?)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Strict validation is useful for:
|
|
464
|
+
- **AI agent self-correction**: LLMs can parse error messages and fix typos
|
|
465
|
+
- **Template development**: catch copy-paste errors in large documents
|
|
466
|
+
- **Type safety**: ensure your generator is emitting well-formed documents
|
|
467
|
+
|
|
468
|
+
You can also call `validate()` standalone for testing:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { validate } from 'pretext-pdf'
|
|
472
|
+
|
|
473
|
+
// Throws PretextPdfError('VALIDATION_ERROR', ...) if strict check fails
|
|
474
|
+
validate(doc, { strict: true })
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## India / GST invoicing
|
|
480
|
+
|
|
481
|
+
Built-in support for Indian invoice requirements:
|
|
482
|
+
|
|
483
|
+
- **₹ symbol** renders correctly (bundled Inter includes the Rupee glyph)
|
|
484
|
+
- **Indian number formatting** (`1,00,000` not `100,000`)
|
|
485
|
+
- **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) layouts (auto-detected from state fields)
|
|
486
|
+
- **Amount in words** — Indian numbering system (Lakh/Crore), with correct sub-rupee handling
|
|
487
|
+
- **SAC/HSN codes** — column support in line-item tables
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import { createGstInvoice } from 'pretext-pdf/templates'
|
|
491
|
+
import { render } from 'pretext-pdf'
|
|
492
|
+
|
|
493
|
+
const content = createGstInvoice({
|
|
494
|
+
supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
|
|
495
|
+
buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
|
|
496
|
+
invoiceNumber: 'INV/2026-27/001',
|
|
497
|
+
invoiceDate: '20 Apr 2026',
|
|
498
|
+
placeOfSupply: 'Maharashtra (27)',
|
|
499
|
+
items: [
|
|
500
|
+
{ description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
|
|
501
|
+
],
|
|
502
|
+
qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
|
|
503
|
+
bankName: 'HDFC Bank', accountNumber: '501001234567', ifscCode: 'HDFC0001234',
|
|
504
|
+
})
|
|
505
|
+
const pdf = await render({ content })
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for a fully wired example.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Custom fonts
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const pdf = await render({
|
|
516
|
+
fonts: [
|
|
517
|
+
{ family: 'Roboto', weight: 400, src: '/path/to/Roboto-Regular.ttf' },
|
|
518
|
+
{ family: 'Roboto', weight: 700, src: '/path/to/Roboto-Bold.ttf' },
|
|
519
|
+
{ family: 'Roboto', style: 'italic', src: '/path/to/Roboto-Italic.ttf' },
|
|
520
|
+
],
|
|
521
|
+
defaultFont: 'Roboto',
|
|
522
|
+
content: [
|
|
523
|
+
{ type: 'paragraph', text: 'Uses Roboto' },
|
|
524
|
+
{ type: 'paragraph', text: 'Bold', fontWeight: 700 },
|
|
525
|
+
],
|
|
526
|
+
})
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
> **Avoid `system-ui`** — known Pretext layout-measurement inaccuracy on macOS. Always name fonts explicitly.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Rich text
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
{
|
|
537
|
+
type: 'rich-paragraph',
|
|
538
|
+
fontSize: 13,
|
|
539
|
+
spans: [
|
|
540
|
+
{ text: 'Normal ' },
|
|
541
|
+
{ text: 'bold', fontWeight: 700 },
|
|
542
|
+
{ text: ' and ', fontStyle: 'italic' },
|
|
543
|
+
{ text: 'colored', color: '#e63946' },
|
|
544
|
+
{ text: ' and ' },
|
|
545
|
+
{ text: 'linked', href: 'https://example.com', underline: true, color: '#0070f3' },
|
|
546
|
+
{ text: '. Also: E=mc' },
|
|
547
|
+
{ text: '2', verticalAlign: 'superscript' },
|
|
548
|
+
{ text: ' and H' },
|
|
549
|
+
{ text: '2', verticalAlign: 'subscript' },
|
|
550
|
+
{ text: 'O.' },
|
|
551
|
+
],
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## Footnotes
|
|
558
|
+
|
|
559
|
+
`createFootnoteSet()` produces matched reference/definition pairs with guaranteed unique IDs:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { render, createFootnoteSet } from 'pretext-pdf'
|
|
563
|
+
|
|
564
|
+
const notes = createFootnoteSet([
|
|
565
|
+
{ text: 'Smith, J. (2022). Typography in PDFs.' },
|
|
566
|
+
{ text: 'Ibid., p. 42.' },
|
|
567
|
+
])
|
|
568
|
+
|
|
569
|
+
await render({
|
|
570
|
+
content: [
|
|
571
|
+
{
|
|
572
|
+
type: 'rich-paragraph',
|
|
573
|
+
spans: [
|
|
574
|
+
{ text: 'See the original research' },
|
|
575
|
+
{ text: '¹', verticalAlign: 'superscript', footnoteRef: notes[0]!.id },
|
|
576
|
+
{ text: ' for details.' },
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
...notes.map(n => n.def), // footnote-def elements go at end of document
|
|
580
|
+
],
|
|
581
|
+
})
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Custom element types (plugins)
|
|
587
|
+
|
|
588
|
+
The plugin API lets you register new element types without forking the library.
|
|
589
|
+
Each plugin definition handles one `type` string and participates in the standard
|
|
590
|
+
validate → measure → render pipeline.
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import { render } from 'pretext-pdf'
|
|
594
|
+
import type { PluginDefinition } from 'pretext-pdf'
|
|
595
|
+
import { rgb } from '@cantoo/pdf-lib'
|
|
596
|
+
|
|
597
|
+
const highlightBoxPlugin: PluginDefinition = {
|
|
598
|
+
type: 'highlight-box',
|
|
599
|
+
|
|
600
|
+
// Optional: reject bad elements early
|
|
601
|
+
validate(element) {
|
|
602
|
+
if (typeof element['label'] !== 'string') return '"label" must be a string'
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Required: return block height for layout/pagination
|
|
606
|
+
async measure(element) {
|
|
607
|
+
return { height: 48, spaceBefore: 8, spaceAfter: 8 }
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
// Required: draw onto the pdf-lib page
|
|
611
|
+
render({ element, pdfPage, x, y, width, height }) {
|
|
612
|
+
pdfPage.drawRectangle({ x, y: y - height, width, height, color: rgb(1, 0.93, 0.73) })
|
|
613
|
+
pdfPage.drawText(element['label'] as string, { x: x + 16, y: y - 30, size: 13 })
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Pass plugins via render() options or createPdf() options
|
|
618
|
+
const pdf = await render(doc, { plugins: [highlightBoxPlugin] })
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**How it works:**
|
|
622
|
+
|
|
623
|
+
| Hook | Stage | Required | Purpose |
|
|
624
|
+
| ---- | ----- | -------- | ------- |
|
|
625
|
+
| `validate` | 1 | No | Reject malformed custom elements; return error string or void |
|
|
626
|
+
| `loadAsset` | 2b | No | Embed a `PDFImage` (passed back as `context.pdfImage` in render) |
|
|
627
|
+
| `measure` | 3 | **Yes** | Return `height`, optional `spaceBefore`/`spaceAfter`, optional `pluginData` |
|
|
628
|
+
| `render` | 5 | **Yes** | Draw onto `context.pdfPage` using pdf-lib's drawing API |
|
|
629
|
+
|
|
630
|
+
**Y-coordinate note:** pdf-lib uses a bottom-left origin. `context.y` is the **top** edge of your block.
|
|
631
|
+
To fill the block: `drawRectangle({ x, y: y - height, width, height })`.
|
|
632
|
+
To draw the first line of text: `drawText(line, { x, y: y - fontSize })`.
|
|
633
|
+
|
|
634
|
+
**Constraints:** Plugin elements can only appear at the top level of `doc.content`.
|
|
635
|
+
They cannot be nested inside callout, blockquote, or float-group children (those
|
|
636
|
+
have hardcoded child type whitelists). Use top-level layout with spacers for positioning.
|
|
637
|
+
|
|
638
|
+
See `examples/plugin-custom-element.ts` for a full runnable example:
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
npm run example:plugin
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Examples
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
npm run example # Basic invoice
|
|
650
|
+
npm run example:gst # India GST invoice
|
|
651
|
+
npm run example:watermark # Text/image watermarks
|
|
652
|
+
npm run example:bookmarks # PDF outline/bookmarks
|
|
653
|
+
npm run example:toc # Auto table of contents
|
|
654
|
+
npm run example:rtl # Arabic/Hebrew RTL text
|
|
655
|
+
npm run example:encryption # Password-protected PDF
|
|
656
|
+
npm run example:hyperlinks # External + email + internal anchors
|
|
657
|
+
npm run example:annotations # Sticky notes
|
|
658
|
+
npm run example:assembly # Merge + assemble multiple PDFs
|
|
659
|
+
npm run example:inline # Super/subscript, letterSpacing, smallCaps
|
|
660
|
+
npm run example:forms # Interactive form fields
|
|
661
|
+
npm run example:callout # Callout boxes
|
|
662
|
+
npm run example:plugin # Custom element types (plugin API)
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
All write to `output/*.pdf`.
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Error handling
|
|
670
|
+
|
|
671
|
+
Every error throws `PretextPdfError` with a typed `code`:
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
import { render, PretextPdfError } from 'pretext-pdf'
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const pdf = await render(config)
|
|
678
|
+
} catch (err) {
|
|
679
|
+
if (err instanceof PretextPdfError) {
|
|
680
|
+
switch (err.code) {
|
|
681
|
+
case 'VALIDATION_ERROR': // Invalid config
|
|
682
|
+
case 'FONT_LOAD_FAILED': // Font file not found
|
|
683
|
+
case 'IMAGE_TOO_TALL': // Image doesn't fit on page
|
|
684
|
+
case 'IMAGE_LOAD_FAILED': // URL fetch / safety check failed
|
|
685
|
+
case 'ASSEMBLY_EMPTY': // merge / assemble called with empty array
|
|
686
|
+
// ... see CHANGELOG.md for the full list
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
This shape is also designed for AI self-correction loops — the typed `code` is enough context for an LLM to fix its own output.
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Troubleshooting
|
|
697
|
+
|
|
698
|
+
### Hyphenation language not found
|
|
699
|
+
|
|
700
|
+
Use **lowercase** language codes that match the npm package name:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
hyphenation: { language: 'en-us' } // ✅
|
|
704
|
+
hyphenation: { language: 'en-US' } // ❌ fails on Linux (case-sensitive FS)
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### SVG / chart / qr-code / barcode rendering
|
|
708
|
+
|
|
709
|
+
Install `@napi-rs/canvas` (Node only — browsers use native `OffscreenCanvas`):
|
|
710
|
+
|
|
711
|
+
```bash
|
|
712
|
+
npm install @napi-rs/canvas
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### PDF is blank or too small
|
|
716
|
+
|
|
717
|
+
Check margins. If `left + right` exceeds page width, content width becomes negative:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
margins: { top: 36, bottom: 36, left: 36, right: 36 }
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Form fields not interactive
|
|
724
|
+
|
|
725
|
+
`flattenForms: true` bakes fields into static content — by design. Remove the flag to keep them interactive.
|
|
726
|
+
|
|
727
|
+
### Browser usage
|
|
728
|
+
|
|
729
|
+
Supply font bytes via `doc.fonts: [{ family: 'Inter', weight: 400, src: <Uint8Array> }]` — the bundled Inter loader is Node-only. Also register the same font with `document.fonts.add(new FontFace(...))` so pretext's measurement matches pdf-lib's drawing.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Non-goals
|
|
734
|
+
|
|
735
|
+
What pretext-pdf is **not** trying to be — pick a different tool for these:
|
|
736
|
+
|
|
737
|
+
- **Editing or parsing existing PDFs** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdf-parse`](https://www.npmjs.com/package/pdf-parse)
|
|
738
|
+
- **Filling existing PDF form templates** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdftk`](https://www.pdflabs.com/tools/pdftk-server/)
|
|
739
|
+
- **Heavily art-directed pages** with CSS grids, SVG illustrations, floats, background images → headless Chrome (Puppeteer)
|
|
740
|
+
- **PDF/A archival, PDF/UA accessibility tagging** → not yet
|
|
741
|
+
- **Print-shop kerning pairs, OpenType ligatures, variable-font axes beyond weight** → upstream Pretext doesn't model these
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## Runtime footprint
|
|
746
|
+
|
|
747
|
+
Mandatory runtime dependencies:
|
|
748
|
+
|
|
749
|
+
- `@cantoo/pdf-lib` — PDF assembly
|
|
750
|
+
- `@chenglou/pretext` — text-layout engine
|
|
751
|
+
- `@fontsource/inter` + `@pdf-lib/fontkit` — bundled Inter + font subsetting
|
|
752
|
+
- `bidi-js` — bidirectional text resolution
|
|
753
|
+
- `hypher` + `hyphenation.en-us` — hyphenation
|
|
754
|
+
|
|
755
|
+
All other capabilities (SVG, charts, QR, barcodes, markdown, signing) are optional peer deps — install only what you use.
|
|
756
|
+
|
|
757
|
+
**Browser:** the library imports cleanly from any non-`file://` URL (esm.sh, Vite dev server, browser bundles) since v0.8.1. Bring your own Inter font via `doc.fonts` and register it with `document.fonts.add(...)` for accurate measurement.
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Compatibility matrix
|
|
762
|
+
|
|
763
|
+
| Environment | Status | Notes |
|
|
764
|
+
| ----------- | ------ | ----- |
|
|
765
|
+
| **Node.js 18 / 20 / 22** | ✅ Confirmed | CI tests all three. Requires `@napi-rs/canvas` peer dep for SVG / chart / QR elements. |
|
|
766
|
+
| **Browser (Vite, webpack, esm.sh)** | ✅ Confirmed | Uses native `OffscreenCanvas`. No canvas peer dep needed. Bring your own font bytes via `doc.fonts` — the bundled Inter loader is Node-only. |
|
|
767
|
+
| **Bun** | ⚠️ Untested | Bun has Node.js compat mode. `@napi-rs/canvas` provides Bun builds but is untested end-to-end. |
|
|
768
|
+
| **Deno** | ⚠️ Untested | Deno's Node compat layer may work. `@napi-rs/canvas` native bindings are the unknown variable. |
|
|
769
|
+
| **AWS Lambda / serverless (Node runtime)** | ⚠️ Likely works | Node.js runtime, ESM supported. Cold-start impact from `@napi-rs/canvas` native addon if used. Elements that don't need canvas (paragraph, heading, table, list) have no native dep. |
|
|
770
|
+
| **Cloudflare Workers** | ❌ Not supported | No Node.js runtime, no native addons, no `OffscreenCanvas`. Neither the Node polyfill nor the browser path can run. |
|
|
771
|
+
| **Next.js (server components / API routes)** | ✅ Confirmed (Node path) | Runs on Node.js server side. Client-side rendering follows the browser path above. |
|
|
772
|
+
|
|
773
|
+
**Legend:** ✅ Confirmed in CI or end-to-end testing · ⚠️ Untested / likely works · ❌ Known not supported
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## Performance
|
|
778
|
+
|
|
779
|
+
Benchmarked on Windows 11 / Node 22 / Intel i7-12th Gen. Averages over 10 runs, excluding the first cold JIT.
|
|
780
|
+
|
|
781
|
+
| Document | Render time | PDF size |
|
|
782
|
+
| --- | --- | --- |
|
|
783
|
+
| 1 page (heading + paragraph + list) | ~220 ms | ~45 KB |
|
|
784
|
+
| Mixed (heading + paragraph + 20-row table + list + hr) | ~290 ms | ~60 KB |
|
|
785
|
+
| 10 pages (40 sections, mixed elements) | ~1,100 ms | ~180 KB |
|
|
786
|
+
|
|
787
|
+
**Font subsetting** is automatic for TTF/OTF fonts. Only used glyphs are embedded — typically 40–60% smaller than full-font embedding. Single-font invoices render under 65 KB.
|
|
788
|
+
|
|
789
|
+
For documents with 10,000+ elements, set `NODE_OPTIONS=--max-old-space-size=4096`.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## Tests
|
|
794
|
+
|
|
795
|
+
600+ tests with 100% pass rate:
|
|
796
|
+
|
|
797
|
+
```bash
|
|
798
|
+
npm test # Full suite (contract + unit + e2e + phases + 2f stress)
|
|
799
|
+
npm run test:unit # Validation, builder, rich-text
|
|
800
|
+
npm run test:e2e # End-to-end render
|
|
801
|
+
npm run test:phases # All phase tests including v0.8/v0.9 features
|
|
802
|
+
npm run test:rich # Rich-paragraph compositor (incl. v0.8.2 whitespace regressions)
|
|
803
|
+
npm run test:contract # Public API surface contracts
|
|
804
|
+
npm run test:visual # Pixel-diff visual regressions
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Coverage**: type safety, path validation, SSRF, error handling, boundary cases, crypto signing, document assembly, every content element, optional-dep error codes, MCP tool validation, browser import simulation.
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Security
|
|
812
|
+
|
|
813
|
+
A comprehensive April 2026 audit fixed 41 issues across path-traversal protection, async I/O, error sanitization, type safety, and explicit failure modes. Subsequent fixes:
|
|
814
|
+
|
|
815
|
+
- **v0.8.3** — IPv4-mapped IPv6 SSRF bypass closed; `fetch` redirects now revalidated per hop.
|
|
816
|
+
- **v0.8.1** — Browser module-init crashes fixed (Node-only APIs gated behind `IS_NODE` checks).
|
|
817
|
+
|
|
818
|
+
Highlights of the current security posture:
|
|
819
|
+
|
|
820
|
+
- Opt-in `allowedFileDirs` lockdown for user-controlled file inputs
|
|
821
|
+
- All error messages sanitized (no filesystem paths or secrets leak)
|
|
822
|
+
- Async file I/O throughout (non-blocking)
|
|
823
|
+
- Strict TypeScript with documented `any`-casts only at pdf-lib internal boundaries
|
|
824
|
+
- HTTPS-only fetch with private-IP / SSRF guard, including IPv6
|
|
825
|
+
- HTTP redirect chain re-validated against the same SSRF guard
|
|
826
|
+
|
|
827
|
+
See [SECURITY.md](SECURITY.md) for disclosure policy.
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
## Roadmap
|
|
832
|
+
|
|
833
|
+
| Phase | Feature | Status |
|
|
834
|
+
|-------|---------|--------|
|
|
835
|
+
| 1–6 | Core engine, pagination, typography, rich text, builder, columns | ✅ |
|
|
836
|
+
| 7A–G | Bookmarks, watermarks, hyphenation, TOC, SVG, RTL, encryption | ✅ |
|
|
837
|
+
| 8A–H | Annotations, forms, assembly, callouts, signatures, metadata, hyperlinks, inline formatting | ✅ |
|
|
838
|
+
| 9A–C | Cryptographic signatures (PKCS#7), image floats, font subsetting | ✅ |
|
|
839
|
+
| 10A–D | QR codes, barcodes, Vega-Lite charts, Markdown, templates | ✅ |
|
|
840
|
+
| 11+ | Performance enhancements, hardening | ✅ |
|
|
841
|
+
| **0.9.0** | **CLI, pdfmake compat shim, GFM tables + task lists** | ✅ |
|
|
842
|
+
| Future | Variable fonts, OpenType features, PDF/A, PDF/UA accessibility | 🔜 |
|
|
843
|
+
|
|
844
|
+
See [docs/ROADMAP.md](docs/ROADMAP.md).
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
848
|
+
## Contributing
|
|
849
|
+
|
|
850
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). TDD approach — write tests first.
|
|
851
|
+
|
|
852
|
+
Useful commands:
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
npm install # one-time setup
|
|
856
|
+
npm run build # tsc → dist/
|
|
857
|
+
npm run typecheck # tsc --noEmit
|
|
858
|
+
npm test # full suite
|
|
859
|
+
npm run example # run a sample render
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## License
|
|
865
|
+
|
|
866
|
+
[MIT](LICENSE)
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## Credits
|
|
871
|
+
|
|
872
|
+
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).
|
|
873
|
+
|
|
874
|
+
Questions? [Open an issue](https://github.com/Himaan1998Y/pretext-pdf/issues) — or try it live at the [demo](https://himaan1998y.github.io/pretext-pdf/).
|