pdfnative 1.0.3 → 1.0.5

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
@@ -10,6 +10,8 @@
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
11
  [![npm provenance](https://img.shields.io/badge/provenance-signed-blueviolet)](https://docs.npmjs.com/generating-provenance-statements)
12
12
  [![website](https://img.shields.io/badge/pdfnative.dev-0066FF?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxyZWN0IHg9IjMiIHk9IjIiIHdpZHRoPSIxNCIgaGVpZ2h0PSIxOCIgcng9IjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTcgN2g2TTcgMTFoOE03IDE1aDQiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=)](https://pdfnative.dev)
13
+ [![pdfnative-mcp](https://img.shields.io/npm/v/pdfnative-mcp?label=pdfnative-mcp&color=6366f1)](https://www.npmjs.com/package/pdfnative-mcp)
14
+ [![pdfnative-cli](https://img.shields.io/npm/v/pdfnative-cli?label=pdfnative-cli&color=0e7490)](https://www.npmjs.com/package/pdfnative-cli)
13
15
 
14
16
  Pure native PDF generation library — zero vendor dependencies. ISO 32000-1 (PDF 1.7) compliant.
15
17
 
@@ -42,6 +44,10 @@ Pure native PDF generation library — zero vendor dependencies. ISO 32000-1 (PD
42
44
  - **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations
43
45
  - **95%+ test coverage** — 1588+ tests across 40 files, fuzz suite, performance benchmarks
44
46
  - **NPM provenance** — signed builds via GitHub Actions OIDC
47
+ - **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
48
+ - **No telemetry, no network calls** — verifiable in source. The library never opens a socket, fetches remote fonts, or phones home
49
+ - **AI client integration** — use pdfnative from Claude Desktop, Cursor, Continue, and Zed via [`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp)
50
+ - **Command-line interface** — render, sign, and inspect PDFs from the shell with [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) — zero-config, scriptable, ideal for CI/CD pipelines
45
51
 
46
52
  ## Installation
47
53
 
@@ -802,6 +808,66 @@ const pdf = buildPDFBytes(params, { compress: true });
802
808
  | `PAGE_SIZES` | Preset page dimensions (A4, Letter, Legal, A3, Tabloid) |
803
809
  | `resolveTemplate(tpl, page, pages, title, date)` | Resolve header/footer template placeholders |
804
810
 
811
+ ## Ecosystem
812
+
813
+ pdfnative ships as a library, but two official companion packages cover the most common non-library use cases. Both live in separate repositories and depend on `pdfnative` only through the public API.
814
+
815
+ ### pdfnative-cli — command-line interface
816
+
817
+ [`pdfnative-cli`](https://github.com/Nizoka/pdfnative-cli) is the **official CLI**. It exposes three commands — `render`, `sign`, `inspect` — for use in shell scripts, Makefiles, GitHub Actions, and Docker images. Zero extra runtime dependencies, npm-provenance-signed.
818
+
819
+ ```bash
820
+ # render a JSON document spec to PDF
821
+ npx pdfnative-cli render document.json --output report.pdf
822
+
823
+ # sign an existing PDF (RSA or ECDSA, CMS/PKCS#7)
824
+ npx pdfnative-cli sign report.pdf --cert cert.pem --key key.pem --output signed.pdf
825
+
826
+ # inspect a PDF (page count, metadata, fonts, signatures)
827
+ npx pdfnative-cli inspect signed.pdf
828
+ ```
829
+
830
+ See the [CLI Guide](https://pdfnative.dev/guides/cli.html) for full reference, security model, and pipeline examples.
831
+
832
+ ### pdfnative-mcp — Model Context Protocol server
833
+
834
+ [`pdfnative-mcp`](https://github.com/Nizoka/pdfnative-mcp) is a **Model Context Protocol server** that bridges pdfnative to any MCP-compatible AI client. Once configured, your AI assistant can generate PDFs, embed barcodes, create forms, sign documents, and render international text — all without writing code.
835
+
836
+ ```bash
837
+ npx -y pdfnative-mcp
838
+ ```
839
+
840
+ ### Available tools
841
+
842
+ | Tool | Purpose |
843
+ |------|---------|
844
+ | `generate_basic_pdf` | Multi-page documents from structured blocks (headings, paragraphs, lists) |
845
+ | `add_table` | Tabular reports from column headers and data rows |
846
+ | `add_barcode` | QR Code, Code 128, EAN-13, Data Matrix, PDF417 |
847
+ | `add_international_text` | 16 non-Latin scripts with BiDi & OpenType shaping |
848
+ | `add_form` | Interactive AcroForm PDFs (text, checkbox, radio, dropdown) |
849
+ | `embed_image` | Embed a JPEG or PNG image (base64) |
850
+ | `prepare_signature_placeholder` | PDF with a `/Sig` field ready to be signed |
851
+ | `sign_pdf` | CMS/PKCS#7 digital signatures (RSA-SHA256 / ECDSA-SHA256) |
852
+
853
+ ### Claude Desktop configuration
854
+
855
+ ```json
856
+ {
857
+ "mcpServers": {
858
+ "pdfnative": {
859
+ "command": "npx",
860
+ "args": ["-y", "pdfnative-mcp"],
861
+ "env": {
862
+ "PDFNATIVE_MPC_OUTPUT_DIR": "/Users/you/Documents/mcp-pdfs"
863
+ }
864
+ }
865
+ }
866
+ }
867
+ ```
868
+
869
+ See the [MCP Integration Guide](https://pdfnative.dev/guides/mcp.html) and the [pdfnative-mcp repository](https://github.com/Nizoka/pdfnative-mcp) for configuration on Cursor, Continue, Zed, and more.
870
+
805
871
  ## Architecture
806
872
 
807
873
  ```
@@ -944,6 +1010,18 @@ When `tagged` is set, the output includes:
944
1010
 
945
1011
  The `tagged` option is backward-compatible — omitting it or setting `false` produces the same output as before.
946
1012
 
1013
+ > **PDF/A status (v1.0.4).** As of v1.0.4 every PDF emits a trailer
1014
+ > `/ID` and the `/Info CreationDate` is byte-equivalent to the
1015
+ > `xmp:CreateDate` (with timezone offset) — closing two veraPDF
1016
+ > reference-validator findings. **Latin font embedding** is **not yet
1017
+ > implemented**: standard 14 Helvetica is still emitted as an
1018
+ > unembedded reference, which veraPDF flags under ISO 19005-1 §6.3.4.
1019
+ > Treat the `pdfaid:part` claim in XMP as aspirational until **v1.0.5**
1020
+ > lands. See [docs/guides/pdfa.html](docs/guides/pdfa.html) and the
1021
+ > tracking issue [release-notes/draft-issue-v1.0.5-latin-embedding.md](release-notes/draft-issue-v1.0.5-latin-embedding.md).
1022
+ > Run `npm run validate:pdfa` locally (with veraPDF installed) to
1023
+ > verify against the reference validator.
1024
+
947
1025
  ### PDF Encryption — Implemented ✅
948
1026
 
949
1027
  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");
@@ -5475,6 +5501,7 @@ var DEFAULT_TEXT_COLOR = "0.75 0.75 0.75";
5475
5501
  var DEFAULT_TEXT_OPACITY = 0.15;
5476
5502
  var DEFAULT_TEXT_ANGLE = -45;
5477
5503
  var DEFAULT_IMAGE_OPACITY = 0.1;
5504
+ var DEFAULT_CAP_HEIGHT_RATIO = 0.718;
5478
5505
  function validateWatermark(watermark, pdfaLevel) {
5479
5506
  if (pdfaLevel === "pdfa1b") {
5480
5507
  const textOpacity = watermark.text?.opacity ?? DEFAULT_TEXT_OPACITY;
@@ -5529,10 +5556,12 @@ function _buildTextWatermarkOps(wm, pgW, pgH, enc, gsName) {
5529
5556
  const cy = pgH / 2;
5530
5557
  const textWidth = enc.tw(wm.text, sz);
5531
5558
  const offsetX = -textWidth / 2;
5532
- const offsetY = -sz / 2;
5559
+ const fd = enc.fontData;
5560
+ const capHeightRatio = fd ? fd.metrics.capHeight / fd.metrics.unitsPerEm : DEFAULT_CAP_HEIGHT_RATIO;
5561
+ const offsetY = -sz * capHeightRatio / 2;
5533
5562
  const tx = cx + offsetX * cos - offsetY * sin;
5534
5563
  const ty = cy + offsetX * sin + offsetY * cos;
5535
- const escapedText = pdfString(wm.text);
5564
+ const escapedText = enc.ps(wm.text);
5536
5565
  const ops = [
5537
5566
  "q",
5538
5567
  `${gsName} gs`,
@@ -6014,10 +6043,7 @@ function buildPDF(params, layoutOptions) {
6014
6043
  }
6015
6044
  const baseObjCount = enc.isUnicode ? 4 + fontEntries.length * 5 + wmExtraObjs + totalPages * 2 : 4 + wmExtraObjs + totalPages * 2;
6016
6045
  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())}`;
6046
+ const { pdfDate, xmpDate: isoDate } = buildPdfMetadata();
6021
6047
  const infoTitle = params.docTitle || title || "";
6022
6048
  emitObj(
6023
6049
  infoObjNum,
@@ -6097,7 +6123,7 @@ endobj
6097
6123
  }
6098
6124
  }
6099
6125
  const writer = { emit, emitObj, emitStreamObj, offset: getOffset, adjustOffset, objOffsets, parts };
6100
- writeXrefTrailer(writer, totalObjs, infoObjNum, encState);
6126
+ writeXrefTrailer(writer, totalObjs, infoObjNum, encState, `${infoTitle}|${pdfDate}`);
6101
6127
  return parts.join("");
6102
6128
  }
6103
6129
  function buildPDFBytes(params, layoutOptions) {
@@ -9703,10 +9729,7 @@ function buildDocumentPDF(params, layoutOptions) {
9703
9729
  }
9704
9730
  const baseObjCount = enc.isUnicode ? 4 + fontEntries.length * 5 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs : 4 + imageCount + wmExtraObjs + totalPages * 2 + totalAnnots + totalFormObjs + formFontObjs;
9705
9731
  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())}`;
9732
+ const { pdfDate, xmpDate: isoDate } = buildPdfMetadata();
9710
9733
  const infoTitle = params.title ?? "";
9711
9734
  const metaParts = [`/Title ${encodePdfTextString(infoTitle)}`, "/Producer (pdfnative)", `/CreationDate (${pdfDate})`];
9712
9735
  if (params.metadata?.author) {
@@ -9734,7 +9757,7 @@ function buildDocumentPDF(params, layoutOptions) {
9734
9757
  structTreeRootObjNum = tree.structTreeRootObjNum;
9735
9758
  totalObjs = treeStart + tree.totalObjects - 1;
9736
9759
  xmpObjNum = totalObjs + 1;
9737
- const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance);
9760
+ const xmpContent = buildXMPMetadata(infoTitle, isoDate, pdfaConfig.pdfaPart, pdfaConfig.pdfaConformance, params.metadata?.author);
9738
9761
  emitStreamObj(
9739
9762
  xmpObjNum,
9740
9763
  `<< /Type /Metadata /Subtype /XML /Length ${xmpContent.length}`,
@@ -9840,7 +9863,7 @@ endobj
9840
9863
  }
9841
9864
  }
9842
9865
  const writer = { emit, emitObj, emitStreamObj, offset: getOffset, adjustOffset, objOffsets, parts };
9843
- writeXrefTrailer(writer, totalObjs, infoObjNum, encState);
9866
+ writeXrefTrailer(writer, totalObjs, infoObjNum, encState, `${infoTitle}|${pdfDate}`);
9844
9867
  return parts.join("");
9845
9868
  }
9846
9869
  function buildDocumentPDFBytes(params, layoutOptions) {