pdfnative 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/index.cjs +71 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +71 -22
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +232 -12
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +232 -12
- package/dist/worker/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ Pure native PDF generation library — zero vendor dependencies. ISO 32000-1 (PD
|
|
|
42
42
|
- **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations
|
|
43
43
|
- **95%+ test coverage** — 1588+ tests across 40 files, fuzz suite, performance benchmarks
|
|
44
44
|
- **NPM provenance** — signed builds via GitHub Actions OIDC
|
|
45
|
+
- **On-device generation** — runs in Node, browsers, Workers, Deno, Bun. No SaaS round-trip; documents never leave the calling process unless your application explicitly sends them
|
|
46
|
+
- **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home
|
|
45
47
|
|
|
46
48
|
## Installation
|
|
47
49
|
|
|
@@ -51,6 +53,17 @@ npm install pdfnative
|
|
|
51
53
|
|
|
52
54
|
**Requirements:** Node.js >= 22 | Modern browsers | Deno | Bun
|
|
53
55
|
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
- 🌐 **Website:** [pdfnative.dev](https://pdfnative.dev) — landing page, live in-browser demo with 10 examples, comparisons, benchmarks.
|
|
59
|
+
- 📘 **Quick Start:** [docs/guides/quickstart.md](docs/guides/quickstart.md) — Node.js, browser, Web Worker, streaming.
|
|
60
|
+
- 🏛️ **Architecture:** [docs/guides/architecture.md](docs/guides/architecture.md) — modules, builders, generation pipeline.
|
|
61
|
+
- ♿ **Accessibility:** [docs/guides/accessibility.md](docs/guides/accessibility.md) — tagged PDF, PDF/UA, PDF/A.
|
|
62
|
+
- ❓ **FAQ:** [docs/guides/faq.md](docs/guides/faq.md) — fonts, encryption, signatures, comparisons.
|
|
63
|
+
- 🛠️ **Troubleshooting:** [docs/guides/troubleshooting.md](docs/guides/troubleshooting.md) — common pitfalls.
|
|
64
|
+
- 🎮 **Playgrounds:** [docs/playgrounds/extreme-scripts.html](docs/playgrounds/extreme-scripts.html) (live BiDi/Indic stress tests) and [docs/playgrounds/medical-800.html](docs/playgrounds/medical-800.html) (800-page Web Worker showcase).
|
|
65
|
+
- 🧪 **Sample PDFs:** [scripts/generators/](scripts/generators/) — ~140 sample PDFs across 23 categories (see [Sample PDFs](#sample-pdfs) below).
|
|
66
|
+
|
|
54
67
|
## Why pdfnative?
|
|
55
68
|
|
|
56
69
|
pdfnative was designed for teams that need **ISO-compliant, production-grade PDF generation** with zero supply-chain risk. Here is how it compares to other popular JavaScript PDF libraries:
|
|
@@ -344,6 +357,8 @@ The tool extracts cmap, widths, metrics, GSUB, GPOS, and embeds the raw TTF as b
|
|
|
344
357
|
|
|
345
358
|
## Visual PDF Inspection
|
|
346
359
|
|
|
360
|
+
<a id="sample-pdfs"></a>
|
|
361
|
+
|
|
347
362
|
Generate sample PDFs for all supported languages to visually verify output:
|
|
348
363
|
|
|
349
364
|
```bash
|
|
@@ -466,6 +481,9 @@ See [scripts/README.md](scripts/README.md) for the modular generator architectur
|
|
|
466
481
|
| `doc-arabic.pdf` | Arabic RTL document (headings, lists, table, BiDi) |
|
|
467
482
|
| `doc-hebrew.pdf` | Hebrew RTL document (headings, lists, table, BiDi) |
|
|
468
483
|
| `doc-thai.pdf` | Thai user manual (GSUB+GPOS shaping, pricing table) |
|
|
484
|
+
| `doc-bengali.pdf` | Bengali document (GSUB conjuncts + GPOS marks) |
|
|
485
|
+
| `doc-tamil.pdf` | Tamil document (GSUB substitution + split vowels) |
|
|
486
|
+
| `doc-devanagari.pdf` | Hindi (Devanagari) document — GSUB conjuncts, reph reordering, matra reordering, split vowels |
|
|
469
487
|
| `doc-chinese-catalog.pdf` | Chinese product catalog (tables, ordering info) |
|
|
470
488
|
| `doc-multi-language.pdf` | Multi-language: EN + Arabic + Japanese in one PDF |
|
|
471
489
|
| `doc-invoice.pdf` | Invoice template (line items, totals, payment link) |
|
|
@@ -928,6 +946,18 @@ When `tagged` is set, the output includes:
|
|
|
928
946
|
|
|
929
947
|
The `tagged` option is backward-compatible — omitting it or setting `false` produces the same output as before.
|
|
930
948
|
|
|
949
|
+
> **PDF/A status (v1.0.4).** As of v1.0.4 every PDF emits a trailer
|
|
950
|
+
> `/ID` and the `/Info CreationDate` is byte-equivalent to the
|
|
951
|
+
> `xmp:CreateDate` (with timezone offset) — closing two veraPDF
|
|
952
|
+
> reference-validator findings. **Latin font embedding** is **not yet
|
|
953
|
+
> implemented**: standard 14 Helvetica is still emitted as an
|
|
954
|
+
> unembedded reference, which veraPDF flags under ISO 19005-1 §6.3.4.
|
|
955
|
+
> Treat the `pdfaid:part` claim in XMP as aspirational until **v1.0.5**
|
|
956
|
+
> lands. See [docs/guides/pdfa.html](docs/guides/pdfa.html) and the
|
|
957
|
+
> tracking issue [release-notes/draft-issue-v1.0.5-latin-embedding.md](release-notes/draft-issue-v1.0.5-latin-embedding.md).
|
|
958
|
+
> Run `npm run validate:pdfa` locally (with veraPDF installed) to
|
|
959
|
+
> verify against the reference validator.
|
|
960
|
+
|
|
931
961
|
### PDF Encryption — Implemented ✅
|
|
932
962
|
|
|
933
963
|
AES-128 and AES-256 encryption with owner/user passwords and granular permissions:
|
|
@@ -1077,6 +1107,19 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
|
|
1077
1107
|
- Code style requirements (strict TypeScript, pure functions, ESM-first)
|
|
1078
1108
|
- Branch strategy and PR process
|
|
1079
1109
|
|
|
1110
|
+
## Citing pdfnative
|
|
1111
|
+
|
|
1112
|
+
If you use pdfnative in academic, governmental, or compliance work, please cite it. Citation metadata is available in [CITATION.cff](CITATION.cff).
|
|
1113
|
+
|
|
1114
|
+
```bibtex
|
|
1115
|
+
@software{pdfnative,
|
|
1116
|
+
author = {Nizoka},
|
|
1117
|
+
title = {pdfnative: Zero-dependency, ISO 32000-1 compliant PDF generation for TypeScript},
|
|
1118
|
+
url = {https://github.com/Nizoka/pdfnative},
|
|
1119
|
+
year = {2026}
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1080
1123
|
## License
|
|
1081
1124
|
|
|
1082
1125
|
MIT — see [LICENSE](LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -4623,9 +4623,26 @@ function buildStructureTree(root, startObjNum, pageObjToStructParents) {
|
|
|
4623
4623
|
totalObjects: nextObj - startObjNum
|
|
4624
4624
|
};
|
|
4625
4625
|
}
|
|
4626
|
-
function
|
|
4626
|
+
function buildPdfMetadata(now = /* @__PURE__ */ new Date()) {
|
|
4627
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
4628
|
+
const yyyy = now.getFullYear();
|
|
4629
|
+
const mm = pad2(now.getMonth() + 1);
|
|
4630
|
+
const dd = pad2(now.getDate());
|
|
4631
|
+
const hh = pad2(now.getHours());
|
|
4632
|
+
const mi = pad2(now.getMinutes());
|
|
4633
|
+
const ss = pad2(now.getSeconds());
|
|
4634
|
+
const tzMinutes = -now.getTimezoneOffset();
|
|
4635
|
+
const tzSign = tzMinutes >= 0 ? "+" : "-";
|
|
4636
|
+
const tzAbs = Math.abs(tzMinutes);
|
|
4637
|
+
const tzH = pad2(Math.floor(tzAbs / 60));
|
|
4638
|
+
const tzM = pad2(tzAbs % 60);
|
|
4639
|
+
const pdfDate = `D:${yyyy}${mm}${dd}${hh}${mi}${ss}${tzSign}${tzH}'${tzM}'`;
|
|
4640
|
+
const xmpDate = `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}${tzSign}${tzH}:${tzM}`;
|
|
4641
|
+
return { pdfDate, xmpDate };
|
|
4642
|
+
}
|
|
4643
|
+
function buildXMPMetadata(title, createDate, pdfaPart = 2, pdfaConformance = "B", author) {
|
|
4627
4644
|
const escapedTitle = escapeXml(title);
|
|
4628
|
-
|
|
4645
|
+
const lines = [
|
|
4629
4646
|
'<?xpacket begin="\xEF\xBB\xBF" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
|
4630
4647
|
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
|
|
4631
4648
|
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">',
|
|
@@ -4634,17 +4651,24 @@ function buildXMPMetadata(title, createDate, pdfaPart = 2, pdfaConformance = "B"
|
|
|
4634
4651
|
' xmlns:pdf="http://ns.adobe.com/pdf/1.3/"',
|
|
4635
4652
|
' xmlns:xmp="http://ns.adobe.com/xap/1.0/"',
|
|
4636
4653
|
' xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">',
|
|
4637
|
-
` <dc:title><rdf:Alt><rdf:li xml:lang="x-default">${escapedTitle}</rdf:li></rdf:Alt></dc:title
|
|
4638
|
-
|
|
4654
|
+
` <dc:title><rdf:Alt><rdf:li xml:lang="x-default">${escapedTitle}</rdf:li></rdf:Alt></dc:title>`
|
|
4655
|
+
];
|
|
4656
|
+
if (author !== void 0 && author !== "") {
|
|
4657
|
+
lines.push(` <dc:creator><rdf:Seq><rdf:li>${escapeXml(author)}</rdf:li></rdf:Seq></dc:creator>`);
|
|
4658
|
+
}
|
|
4659
|
+
lines.push(
|
|
4639
4660
|
" <pdf:Producer>pdfnative</pdf:Producer>",
|
|
4640
4661
|
` <xmp:CreateDate>${createDate}</xmp:CreateDate>`,
|
|
4662
|
+
` <xmp:ModifyDate>${createDate}</xmp:ModifyDate>`,
|
|
4663
|
+
` <xmp:MetadataDate>${createDate}</xmp:MetadataDate>`,
|
|
4641
4664
|
` <pdfaid:part>${pdfaPart}</pdfaid:part>`,
|
|
4642
4665
|
` <pdfaid:conformance>${pdfaConformance}</pdfaid:conformance>`,
|
|
4643
4666
|
" </rdf:Description>",
|
|
4644
4667
|
" </rdf:RDF>",
|
|
4645
4668
|
"</x:xmpmeta>",
|
|
4646
4669
|
'<?xpacket end="w"?>'
|
|
4647
|
-
|
|
4670
|
+
);
|
|
4671
|
+
return lines.join("\n");
|
|
4648
4672
|
}
|
|
4649
4673
|
function buildOutputIntentDict(iccStreamObjNum, subtype = "GTS_PDFA1") {
|
|
4650
4674
|
return `<< /Type /OutputIntent /S /${subtype} /OutputConditionIdentifier (sRGB IEC61966-2.1) /RegistryName (http://www.color.org) /DestOutputProfile ${iccStreamObjNum} 0 R >>`;
|
|
@@ -5256,7 +5280,7 @@ endstream`);
|
|
|
5256
5280
|
_offset += d;
|
|
5257
5281
|
}, objOffsets, parts };
|
|
5258
5282
|
}
|
|
5259
|
-
function writeXrefTrailer(w, totalObjs, infoObjNum, encState) {
|
|
5283
|
+
function writeXrefTrailer(w, totalObjs, infoObjNum, encState, idSeed = "") {
|
|
5260
5284
|
let encryptObjNum = 0;
|
|
5261
5285
|
if (encState) {
|
|
5262
5286
|
encryptObjNum = totalObjs + 1;
|
|
@@ -5274,11 +5298,13 @@ function writeXrefTrailer(w, totalObjs, infoObjNum, encState) {
|
|
|
5274
5298
|
`);
|
|
5275
5299
|
}
|
|
5276
5300
|
w.emit("trailer\n");
|
|
5301
|
+
const docId = encState ? encState.docId : md5(new TextEncoder().encode(`pdfnative|${idSeed}|${totalObjs}`));
|
|
5302
|
+
const idArray = buildIdArray(docId);
|
|
5277
5303
|
if (encState) {
|
|
5278
|
-
w.emit(`<< /Size ${totalObjs + 1} /Root 1 0 R /Info ${infoObjNum} 0 R /Encrypt ${encryptObjNum} 0 R /ID ${
|
|
5304
|
+
w.emit(`<< /Size ${totalObjs + 1} /Root 1 0 R /Info ${infoObjNum} 0 R /Encrypt ${encryptObjNum} 0 R /ID ${idArray} >>
|
|
5279
5305
|
`);
|
|
5280
5306
|
} else {
|
|
5281
|
-
w.emit(`<< /Size ${totalObjs + 1} /Root 1 0 R /Info ${infoObjNum} 0 R >>
|
|
5307
|
+
w.emit(`<< /Size ${totalObjs + 1} /Root 1 0 R /Info ${infoObjNum} 0 R /ID ${idArray} >>
|
|
5282
5308
|
`);
|
|
5283
5309
|
}
|
|
5284
5310
|
w.emit("startxref\n");
|
|
@@ -6014,10 +6040,7 @@ function buildPDF(params, layoutOptions) {
|
|
|
6014
6040
|
}
|
|
6015
6041
|
const baseObjCount = enc.isUnicode ? 4 + fontEntries.length * 5 + wmExtraObjs + totalPages * 2 : 4 + wmExtraObjs + totalPages * 2;
|
|
6016
6042
|
const infoObjNum = baseObjCount + 1;
|
|
6017
|
-
const
|
|
6018
|
-
const pad2 = (n) => String(n).padStart(2, "0");
|
|
6019
|
-
const pdfDate = `D:${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
6020
|
-
const isoDate = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}T${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
6043
|
+
const { pdfDate, xmpDate: isoDate } = buildPdfMetadata();
|
|
6021
6044
|
const infoTitle = params.docTitle || title || "";
|
|
6022
6045
|
emitObj(
|
|
6023
6046
|
infoObjNum,
|
|
@@ -6097,7 +6120,7 @@ endobj
|
|
|
6097
6120
|
}
|
|
6098
6121
|
}
|
|
6099
6122
|
const writer = { emit, emitObj, emitStreamObj, offset: getOffset, adjustOffset, objOffsets, parts };
|
|
6100
|
-
writeXrefTrailer(writer, totalObjs, infoObjNum, encState);
|
|
6123
|
+
writeXrefTrailer(writer, totalObjs, infoObjNum, encState, `${infoTitle}|${pdfDate}`);
|
|
6101
6124
|
return parts.join("");
|
|
6102
6125
|
}
|
|
6103
6126
|
function buildPDFBytes(params, layoutOptions) {
|
|
@@ -8444,6 +8467,22 @@ function tokenizeForWrap(text) {
|
|
|
8444
8467
|
if (buf) segments.push(buf);
|
|
8445
8468
|
return segments;
|
|
8446
8469
|
}
|
|
8470
|
+
function hardBreakSegment(seg, maxWidth, fontSize, enc) {
|
|
8471
|
+
const pieces = [];
|
|
8472
|
+
let buf = "";
|
|
8473
|
+
for (const ch of seg) {
|
|
8474
|
+
const candidate = buf + ch;
|
|
8475
|
+
const w = measureText(candidate, fontSize, enc);
|
|
8476
|
+
if (w <= maxWidth || buf === "") {
|
|
8477
|
+
buf = candidate;
|
|
8478
|
+
} else {
|
|
8479
|
+
pieces.push(buf);
|
|
8480
|
+
buf = ch;
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
if (buf) pieces.push(buf);
|
|
8484
|
+
return pieces.length > 0 ? pieces : [seg];
|
|
8485
|
+
}
|
|
8447
8486
|
function wrapText(text, maxWidth, fontSize, enc) {
|
|
8448
8487
|
if (!text) return [""];
|
|
8449
8488
|
if (maxWidth <= 0) return [text];
|
|
@@ -8454,12 +8493,25 @@ function wrapText(text, maxWidth, fontSize, enc) {
|
|
|
8454
8493
|
for (const seg of segments) {
|
|
8455
8494
|
const candidate = currentLine + seg;
|
|
8456
8495
|
const w = measureText(candidate, fontSize, enc);
|
|
8457
|
-
if (w <= maxWidth
|
|
8496
|
+
if (w <= maxWidth) {
|
|
8458
8497
|
currentLine = candidate;
|
|
8459
|
-
|
|
8498
|
+
continue;
|
|
8499
|
+
}
|
|
8500
|
+
if (currentLine !== "") {
|
|
8460
8501
|
lines.push(currentLine.trimEnd());
|
|
8461
|
-
currentLine =
|
|
8502
|
+
currentLine = "";
|
|
8462
8503
|
}
|
|
8504
|
+
const segTrim = seg.trimStart();
|
|
8505
|
+
const segW = measureText(segTrim, fontSize, enc);
|
|
8506
|
+
if (segW <= maxWidth) {
|
|
8507
|
+
currentLine = segTrim;
|
|
8508
|
+
continue;
|
|
8509
|
+
}
|
|
8510
|
+
const pieces = hardBreakSegment(segTrim, maxWidth, fontSize, enc);
|
|
8511
|
+
for (let pi = 0; pi < pieces.length - 1; pi++) {
|
|
8512
|
+
lines.push(pieces[pi].trimEnd());
|
|
8513
|
+
}
|
|
8514
|
+
currentLine = pieces[pieces.length - 1];
|
|
8463
8515
|
}
|
|
8464
8516
|
if (currentLine) lines.push(currentLine.trimEnd());
|
|
8465
8517
|
return lines;
|
|
@@ -9674,10 +9726,7 @@ function buildDocumentPDF(params, layoutOptions) {
|
|
|
9674
9726
|
}
|
|
9675
9727
|
const baseObjCount = enc.isUnicode ? 4 + fontEntries.length * 5 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs : 4 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs;
|
|
9676
9728
|
const infoObjNum = baseObjCount + 1;
|
|
9677
|
-
const
|
|
9678
|
-
const pad2 = (n) => String(n).padStart(2, "0");
|
|
9679
|
-
const pdfDate = `D:${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
9680
|
-
const isoDate = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}T${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
9729
|
+
const { pdfDate, xmpDate: isoDate } = buildPdfMetadata();
|
|
9681
9730
|
const infoTitle = params.title ?? "";
|
|
9682
9731
|
const metaParts = [`/Title ${encodePdfTextString(infoTitle)}`, "/Producer (pdfnative)", `/CreationDate (${pdfDate})`];
|
|
9683
9732
|
if (params.metadata?.author) {
|
|
@@ -9705,7 +9754,7 @@ function buildDocumentPDF(params, layoutOptions) {
|
|
|
9705
9754
|
structTreeRootObjNum = tree.structTreeRootObjNum;
|
|
9706
9755
|
totalObjs = treeStart + tree.totalObjects - 1;
|
|
9707
9756
|
xmpObjNum = totalObjs + 1;
|
|
9708
|
-
const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance);
|
|
9757
|
+
const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance, params.metadata?.author);
|
|
9709
9758
|
emitStreamObj(
|
|
9710
9759
|
xmpObjNum,
|
|
9711
9760
|
`<< /Type /Metadata /Subtype /XML /Length ${xmpContent.length}`,
|
|
@@ -9811,7 +9860,7 @@ endobj
|
|
|
9811
9860
|
}
|
|
9812
9861
|
}
|
|
9813
9862
|
const writer = { emit, emitObj, emitStreamObj, offset: getOffset, adjustOffset, objOffsets, parts };
|
|
9814
|
-
writeXrefTrailer(writer, totalObjs, infoObjNum, encState);
|
|
9863
|
+
writeXrefTrailer(writer, totalObjs, infoObjNum, encState, `${infoTitle}|${pdfDate}`);
|
|
9815
9864
|
return parts.join("");
|
|
9816
9865
|
}
|
|
9817
9866
|
function buildDocumentPDFBytes(params, layoutOptions) {
|