pdfnative 1.0.3 → 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 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
 
@@ -944,6 +946,18 @@ When `tagged` is set, the output includes:
944
946
 
945
947
  The `tagged` option is backward-compatible — omitting it or setting `false` produces the same output as before.
946
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
+
947
961
  ### PDF Encryption — Implemented ✅
948
962
 
949
963
  AES-128 and AES-256 encryption with owner/user passwords and granular permissions:
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 buildXMPMetadata(title, createDate, pdfaPart = 2, pdfaConformance = "B") {
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
- return [
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
- " <dc:creator><rdf:Seq><rdf:li>pdfnative</rdf:li></rdf:Seq></dc:creator>",
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
- ].join("\n");
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 ${buildIdArray(encState.docId)} >>
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 now = /* @__PURE__ */ new Date();
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) {
@@ -9703,10 +9726,7 @@ function buildDocumentPDF(params, layoutOptions) {
9703
9726
  }
9704
9727
  const baseObjCount = enc.isUnicode ? 4 + fontEntries.length * 5 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs : 4 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs;
9705
9728
  const infoObjNum = baseObjCount + 1;
9706
- const now = /* @__PURE__ */ new Date();
9707
- const pad2 = (n) => String(n).padStart(2, "0");
9708
- const pdfDate = `D:${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
9709
- 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();
9710
9730
  const infoTitle = params.title ?? "";
9711
9731
  const metaParts = [`/Title ${encodePdfTextString(infoTitle)}`, "/Producer (pdfnative)", `/CreationDate (${pdfDate})`];
9712
9732
  if (params.metadata?.author) {
@@ -9734,7 +9754,7 @@ function buildDocumentPDF(params, layoutOptions) {
9734
9754
  structTreeRootObjNum = tree.structTreeRootObjNum;
9735
9755
  totalObjs = treeStart + tree.totalObjects - 1;
9736
9756
  xmpObjNum = totalObjs + 1;
9737
- const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance);
9757
+ const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance, params.metadata?.author);
9738
9758
  emitStreamObj(
9739
9759
  xmpObjNum,
9740
9760
  `<< /Type /Metadata /Subtype /XML /Length ${xmpContent.length}`,
@@ -9840,7 +9860,7 @@ endobj
9840
9860
  }
9841
9861
  }
9842
9862
  const writer = { emit, emitObj, emitStreamObj, offset: getOffset, adjustOffset, objOffsets, parts };
9843
- writeXrefTrailer(writer, totalObjs, infoObjNum, encState);
9863
+ writeXrefTrailer(writer, totalObjs, infoObjNum, encState, `${infoTitle}|${pdfDate}`);
9844
9864
  return parts.join("");
9845
9865
  }
9846
9866
  function buildDocumentPDFBytes(params, layoutOptions) {