modern-pdf-lib 0.40.0 → 0.40.2

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.
@@ -1,7 +1,7 @@
1
- const require_pdfDocument = require("./pdfDocument-D5qz7duP.cjs");
1
+ const require_pdfDocument = require("./pdfDocument-CnVPQ_Cm.cjs");
2
2
  const require_pdfObjects = require("./pdfObjects-BcPlSI0a.cjs");
3
3
  const require_streamDecode = require("./streamDecode-kQ-REV7v.cjs");
4
- const require_pdfForm = require("./pdfForm-BVS_do95.cjs");
4
+ const require_pdfForm = require("./pdfForm-Dym3PFT3.cjs");
5
5
  const require_compressionAnalysis = require("./compressionAnalysis-DwxVCe4E.cjs");
6
6
  const require_bridge = require("./bridge-ByvzPu5h.cjs");
7
7
  const require_loader = require("./loader-DR7H0XOf.cjs");
@@ -277,7 +277,7 @@ function compressStream(stream, level) {
277
277
  * @returns The incremental save result.
278
278
  */
279
279
  async function saveDocumentIncremental(originalBytes, doc, options) {
280
- const { buildDocumentStructure } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.pdfCatalog_exports);
280
+ const { buildDocumentStructure } = await Promise.resolve().then(() => require("./pdfDocument-CnVPQ_Cm.cjs")).then((n) => n.pdfCatalog_exports);
281
281
  const registry = doc.getRegistry();
282
282
  const structure = buildDocumentStructure(doc.getInternalPages().map((p) => p.finalize()), {
283
283
  producer: doc.getProducer(),
@@ -28156,16 +28156,40 @@ const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
28156
28156
  "pl",
28157
28157
  "php"
28158
28158
  ]);
28159
- /** Read a `PdfName` value (without the leading `/`) from a dict entry. */
28160
- function nameValue$1(obj) {
28161
- if (obj !== void 0 && obj.kind === "name") {
28162
- const v = obj.value;
28159
+ /**
28160
+ * Resolve a {@link PdfObject} that may be an indirect reference to its
28161
+ * underlying value using the registry. Non-refs are returned as-is; an
28162
+ * unresolvable ref yields `undefined`. (Mirrors `resolve()` in
28163
+ * `./sanitize.ts`.)
28164
+ *
28165
+ * This is security-critical: a hostile PDF can write a dangerous value
28166
+ * indirectly — e.g. `/S 99 0 R` where object 99 is the name `/JavaScript` — and
28167
+ * a conforming viewer treats that exactly like `/S /JavaScript`. Classifying
28168
+ * `/S`, `/Type`, `/Subtype`, `/UF`, `/F`, and `/URI` without resolving the ref
28169
+ * first would let that one indirection evade detection entirely.
28170
+ */
28171
+ function resolveRef(obj, registry) {
28172
+ if (obj instanceof require_pdfObjects.PdfRef) return registry.resolve(obj);
28173
+ return obj;
28174
+ }
28175
+ /**
28176
+ * Read a `PdfName` value (without the leading `/`) from a dict entry, resolving
28177
+ * an indirect reference through the registry first.
28178
+ */
28179
+ function nameValue$1(obj, registry) {
28180
+ const resolved = resolveRef(obj, registry);
28181
+ if (resolved !== void 0 && resolved.kind === "name") {
28182
+ const v = resolved.value;
28163
28183
  return v.startsWith("/") ? v.slice(1) : v;
28164
28184
  }
28165
28185
  }
28166
- /** Read a `PdfString` value from a dict entry. */
28167
- function stringValue(obj) {
28168
- if (obj !== void 0 && obj.kind === "string") return obj.value;
28186
+ /**
28187
+ * Read a `PdfString` value from a dict entry, resolving an indirect reference
28188
+ * through the registry first.
28189
+ */
28190
+ function stringValue(obj, registry) {
28191
+ const resolved = resolveRef(obj, registry);
28192
+ if (resolved !== void 0 && resolved.kind === "string") return resolved.value;
28169
28193
  }
28170
28194
  /** Format a `PdfRef` as the canonical `"N G R"` string, if available. */
28171
28195
  function refString(ref) {
@@ -28220,7 +28244,7 @@ function scanObjectGraph(registry, findings) {
28220
28244
  if (obj.kind === "dict") dict = obj;
28221
28245
  else if (obj.kind === "stream") dict = obj.dict;
28222
28246
  if (dict === void 0) continue;
28223
- inspectDict(dict, refStr, findings);
28247
+ inspectDict(dict, refStr, findings, registry);
28224
28248
  }
28225
28249
  }
28226
28250
  /**
@@ -28229,8 +28253,10 @@ function scanObjectGraph(registry, findings) {
28229
28253
  * @param dict The dictionary (or stream dictionary).
28230
28254
  * @param refStr The owning object's `"N G R"` string, if known.
28231
28255
  * @param findings Accumulator.
28256
+ * @param registry Object registry, used to resolve indirectly-referenced
28257
+ * name/string values (e.g. an `/S` that is a `PdfRef`).
28232
28258
  */
28233
- function inspectDict(dict, refStr, findings) {
28259
+ function inspectDict(dict, refStr, findings, registry) {
28234
28260
  if (dict.has("/OpenAction")) findings.push({
28235
28261
  category: "OpenAction",
28236
28262
  severity: "medium",
@@ -28249,15 +28275,15 @@ function inspectDict(dict, refStr, findings) {
28249
28275
  detail: "A /JavaScript name tree (catalog /Names → /JavaScript) is present; these scripts execute automatically on open (ISO 32000 §12.6.4.16, §7.7.4).",
28250
28276
  objectRef: refStr
28251
28277
  });
28252
- const subtype = nameValue$1(dict.get("/S"));
28253
- if (subtype !== void 0) inspectActionSubtype(subtype, dict, refStr, findings);
28278
+ const subtype = nameValue$1(dict.get("/S"), registry);
28279
+ if (subtype !== void 0) inspectActionSubtype(subtype, dict, refStr, findings, registry);
28254
28280
  if (dict.has("/XFA")) findings.push({
28255
28281
  category: "XFA",
28256
28282
  severity: "low",
28257
28283
  detail: "Document contains an XFA form (/AcroForm → /XFA, ISO 32000 §12.7.8); XFA carries its own scripting and data-binding model.",
28258
28284
  objectRef: refStr
28259
28285
  });
28260
- const annotSubtype = nameValue$1(dict.get("/Subtype"));
28286
+ const annotSubtype = nameValue$1(dict.get("/Subtype"), registry);
28261
28287
  if (annotSubtype === "RichMedia") findings.push({
28262
28288
  category: "RichMedia",
28263
28289
  severity: "low",
@@ -28294,8 +28320,8 @@ function inspectDict(dict, refStr, findings) {
28294
28320
  detail: "Object references sound content via a /Sound entry (ISO 32000 §13.3).",
28295
28321
  objectRef: refStr
28296
28322
  });
28297
- if (nameValue$1(dict.get("/Type")) === "Filespec" || dict.has("/EF")) {
28298
- const fileName = stringValue(dict.get("/UF")) ?? stringValue(dict.get("/F"));
28323
+ if (nameValue$1(dict.get("/Type"), registry) === "Filespec" || dict.has("/EF")) {
28324
+ const fileName = stringValue(dict.get("/UF"), registry) ?? stringValue(dict.get("/F"), registry);
28299
28325
  if (fileName !== void 0) {
28300
28326
  const ext = extensionOf(fileName);
28301
28327
  if (ext !== "" && EXECUTABLE_EXTENSIONS.has(ext)) findings.push({
@@ -28311,7 +28337,7 @@ function inspectDict(dict, refStr, findings) {
28311
28337
  * Flag a dangerous action `/S` subtype. Subtypes are ISO 32000 §12.6.4 /
28312
28338
  * §12.7.5 action types.
28313
28339
  */
28314
- function inspectActionSubtype(subtype, dict, refStr, findings) {
28340
+ function inspectActionSubtype(subtype, dict, refStr, findings, registry) {
28315
28341
  switch (subtype) {
28316
28342
  case "JavaScript":
28317
28343
  findings.push({
@@ -28333,7 +28359,7 @@ function inspectActionSubtype(subtype, dict, refStr, findings) {
28333
28359
  findings.push({
28334
28360
  category: "URI",
28335
28361
  severity: "medium",
28336
- detail: `URI action (/S /URI, ISO 32000 §12.6.4.7) opens a remote URL ${describeUri(dict)}.`,
28362
+ detail: `URI action (/S /URI, ISO 32000 §12.6.4.7) opens a remote URL ${describeUri(dict, registry)}.`,
28337
28363
  objectRef: refStr
28338
28364
  });
28339
28365
  break;
@@ -28381,8 +28407,8 @@ function inspectActionSubtype(subtype, dict, refStr, findings) {
28381
28407
  }
28382
28408
  }
28383
28409
  /** Append the target URL of a /URI action to the detail string, if present. */
28384
- function describeUri(dict) {
28385
- const uri = stringValue(dict.get("/URI"));
28410
+ function describeUri(dict, registry) {
28411
+ const uri = stringValue(dict.get("/URI"), registry);
28386
28412
  return uri !== void 0 ? `→ "${uri}"` : "";
28387
28413
  }
28388
28414
  /**
@@ -28882,6 +28908,10 @@ async function verifyRedactions(pdf, regions) {
28882
28908
  if (regions === void 0 || regions.length === 0) throw new TypeError("verifyRedactions requires an explicit, non-empty `regions` array. Automatic redaction-region detection is not performed: black-filled rectangles are indistinguishable from legitimate graphics, and a loaded page's original /Redact annotations are not exposed through a stable public API. Pass the regions you redacted (PDF points, bottom-left origin, y-up).");
28883
28909
  const doc = await require_pdfDocument.loadPdf(pdf);
28884
28910
  const pageCount = doc.getPageCount();
28911
+ for (const region of regions) {
28912
+ const { page } = region;
28913
+ if (!Number.isInteger(page) || page < 0 || page >= pageCount) throw new RangeError(`verifyRedactions: region page index ${page} is out of range. The document has ${pageCount} page(s) (valid indices 0..${pageCount - 1}). Every region must name a real, inspectable page — an out-of-range page cannot be verified and must not be silently reported clean.`);
28914
+ }
28885
28915
  const itemsByPage = /* @__PURE__ */ new Map();
28886
28916
  const getItems = (pageIndex) => {
28887
28917
  const cached = itemsByPage.get(pageIndex);
@@ -29087,21 +29117,41 @@ function decodePermissionBits(p) {
29087
29117
  };
29088
29118
  }
29089
29119
  /**
29090
- * Test whether the empty user password (`""`) authenticates against the
29091
- * standard security handler. Reuses {@link PdfEncryptionHandler.fromEncryptDict},
29092
- * which throws when the password is incorrect; a successful return means
29093
- * the empty password derived a valid file key.
29120
+ * Test whether the empty **user** password (`""`) authenticates against the
29121
+ * standard security handler.
29122
+ *
29123
+ * This must verify specifically against the `/U` (user) value using the
29124
+ * standard /U validation algorithm — **not** simply check whether the empty
29125
+ * password opens the document. The general file-key derivation
29126
+ * (`PdfEncryptionHandler.fromEncryptDict` / `computeFileEncryptionKey`)
29127
+ * accepts a password that matches **either** the user **or** the owner
29128
+ * password, so a document whose *owner* password is empty (while the user
29129
+ * password is non-empty) would otherwise be mis-reported as having an empty
29130
+ * user password. To avoid asserting a possibly-false `true`, we run the
29131
+ * user-password-specific check ({@link verifyUserPassword}) against `/U`.
29094
29132
  *
29095
- * Returns `undefined` when the test cannot be performed (e.g. the handler
29096
- * implementation does not support the document's /V/R).
29133
+ * Returns `undefined` when the test cannot be performed (e.g. the `/O` / `/U`
29134
+ * values are missing, or the maths throws for an unsupported /V/R).
29097
29135
  */
29098
29136
  async function testEmptyUserPassword(dict, fileId) {
29137
+ const ownerKey = getStringBytes(dict, "/O");
29138
+ const userKey = getStringBytes(dict, "/U");
29139
+ if (ownerKey === void 0 || userKey === void 0) return void 0;
29140
+ const dictValues = {
29141
+ version: getNumber(dict, "/V") ?? 0,
29142
+ revision: getNumber(dict, "/R") ?? 0,
29143
+ keyLength: getNumber(dict, "/Length") ?? 40,
29144
+ ownerKey,
29145
+ userKey,
29146
+ permissions: getNumber(dict, "/P") ?? 0,
29147
+ ownerEncryptionKey: getStringBytes(dict, "/OE"),
29148
+ userEncryptionKey: getStringBytes(dict, "/UE"),
29149
+ perms: getStringBytes(dict, "/Perms"),
29150
+ encryptMetadata: getBool(dict, "/EncryptMetadata") ?? true
29151
+ };
29099
29152
  try {
29100
- await require_pdfDocument.PdfEncryptionHandler.fromEncryptDict(dict, fileId, "");
29101
- return true;
29102
- } catch (err) {
29103
- const message = err instanceof Error ? err.message : String(err);
29104
- if (/password/i.test(message)) return false;
29153
+ return await require_pdfDocument.verifyUserPassword("", dictValues, fileId);
29154
+ } catch {
29105
29155
  return;
29106
29156
  }
29107
29157
  }
@@ -29115,6 +29165,32 @@ function getNumber(dict, key) {
29115
29165
  const obj = dict.get(key);
29116
29166
  if (obj !== void 0 && obj.kind === "number") return obj.value;
29117
29167
  }
29168
+ /** Read a boolean value from a dict. */
29169
+ function getBool(dict, key) {
29170
+ const obj = dict.get(key);
29171
+ if (obj !== void 0 && obj.kind === "bool") return obj.value;
29172
+ }
29173
+ /**
29174
+ * Read raw bytes from a `/String` entry.
29175
+ *
29176
+ * Hex strings whose value is still raw hex digits (e.g. produced by
29177
+ * `PdfString.hexFromBytes`) are decoded from hex pairs; otherwise the
29178
+ * character codes are treated as raw Latin-1 bytes. Mirrors the decoding
29179
+ * used by `crypto/encryptionHandler.fromEncryptDict` so the /U validation
29180
+ * sees the same /O, /U, /OE, /UE, /Perms bytes.
29181
+ */
29182
+ function getStringBytes(dict, key) {
29183
+ const obj = dict.get(key);
29184
+ if (obj === void 0 || obj.kind !== "string") return void 0;
29185
+ const str = obj;
29186
+ if (str.hex && /^[\da-fA-F\s]*$/.test(str.value)) {
29187
+ const clean = str.value.replace(/\s/g, "");
29188
+ return Uint8Array.fromHex(clean.length % 2 === 0 ? clean : clean + "0");
29189
+ }
29190
+ const bytes = new Uint8Array(str.value.length);
29191
+ for (let i = 0; i < str.value.length; i++) bytes[i] = str.value.charCodeAt(i) & 255;
29192
+ return bytes;
29193
+ }
29118
29194
  //#endregion
29119
29195
  //#region src/text/bidi.ts
29120
29196
  /** UAX #9 §3.3.2: the maximum explicit embedding depth. */
@@ -29866,24 +29942,16 @@ function resolveImplicit(seq, types, levels) {
29866
29942
  */
29867
29943
  function resetWhitespaceLevels(originalTypes, levels, baseLevel) {
29868
29944
  const n = levels.length;
29869
- let resetFrom = n;
29870
- for (let i = n - 1; i >= 0; i--) {
29945
+ const resettable = (t) => t === "WS" || t === "FSI" || t === "LRI" || t === "RLI" || t === "PDI" || isRemovedByX9(t);
29946
+ let runStart = 0;
29947
+ for (let i = 0; i < n; i++) {
29871
29948
  const t = originalTypes[i];
29872
29949
  if (t === "B" || t === "S") {
29873
- levels[i] = baseLevel;
29874
- for (let j = i + 1; j < resetFrom; j++) levels[j] = baseLevel;
29875
- resetFrom = i;
29876
- } else if (t === "WS" || t === "FSI" || t === "LRI" || t === "RLI" || t === "PDI" || isRemovedByX9(t)) {} else {
29877
- if (resetFrom === n) {}
29878
- resetFrom = i + 1 > resetFrom ? resetFrom : i + 1;
29879
- resetFrom = i + 1;
29880
- }
29881
- }
29882
- for (let i = n - 1; i >= 0; i--) {
29883
- const t = originalTypes[i];
29884
- if (t === "WS" || t === "FSI" || t === "LRI" || t === "RLI" || t === "PDI" || isRemovedByX9(t)) levels[i] = baseLevel;
29885
- else break;
29950
+ for (let j = runStart; j <= i; j++) levels[j] = baseLevel;
29951
+ runStart = i + 1;
29952
+ } else if (resettable(t)) {} else runStart = i + 1;
29886
29953
  }
29954
+ for (let i = runStart; i < n; i++) levels[i] = baseLevel;
29887
29955
  }
29888
29956
  /** L2: reorder code-unit indices into visual order from the resolved levels. */
29889
29957
  function reorderLevels(levels) {
@@ -31913,11 +31981,15 @@ function boxBlurD(stdDev) {
31913
31981
  return Math.floor(stdDev * 3 * Math.sqrt(2 * Math.PI) / 4 + .5);
31914
31982
  }
31915
31983
  /**
31916
- * One horizontal box blur of the given (odd) length, premultiplied float
31917
- * data, with an asymmetric left/right radius to support the even-`d` rule.
31918
- * The window covers offsets `[-leftRadius, +rightRadius]` inclusive. Edge
31919
- * samples are clamped (extend-edge) rather than treated as transparent so a
31920
- * flat region stays flat.
31984
+ * One horizontal box blur of the given length, premultiplied float data, with
31985
+ * an asymmetric left/right radius to support the even-`d` rule. The window
31986
+ * covers offsets `[-leftRadius, +rightRadius]` inclusive.
31987
+ *
31988
+ * Samples that fall outside the filter region are treated as **transparent
31989
+ * black** `[0,0,0,0]` (SVG 1.1 §15.17 `edgeMode="none"`, the spec default).
31990
+ * In premultiplied space those samples contribute zero to every channel, so
31991
+ * the running sum simply skips them (the window length stays the full box
31992
+ * size, which is what fades edge pixels per spec).
31921
31993
  */
31922
31994
  function boxBlurH(data, width, height, leftRadius, rightRadius) {
31923
31995
  const out = new Float32Array(data.length);
@@ -31927,38 +31999,40 @@ function boxBlurH(data, width, height, leftRadius, rightRadius) {
31927
31999
  for (let c = 0; c < 4; c++) {
31928
32000
  let sum = 0;
31929
32001
  for (let k = -leftRadius; k <= rightRadius; k++) {
31930
- const cx = k < 0 ? 0 : k >= width ? width - 1 : k;
31931
- sum += data[(row + cx) * 4 + c];
32002
+ if (k < 0 || k >= width) continue;
32003
+ sum += data[(row + k) * 4 + c];
31932
32004
  }
31933
32005
  for (let x = 0; x < width; x++) {
31934
32006
  out[(row + x) * 4 + c] = sum / windowLen;
31935
32007
  const dropX = x - leftRadius;
31936
32008
  const addX = x + rightRadius + 1;
31937
- const dc = dropX < 0 ? 0 : dropX >= width ? width - 1 : dropX;
31938
- const ac = addX < 0 ? 0 : addX >= width ? width - 1 : addX;
31939
- sum += data[(row + ac) * 4 + c] - data[(row + dc) * 4 + c];
32009
+ if (addX >= 0 && addX < width) sum += data[(row + addX) * 4 + c];
32010
+ if (dropX >= 0 && dropX < width) sum -= data[(row + dropX) * 4 + c];
31940
32011
  }
31941
32012
  }
31942
32013
  }
31943
32014
  return out;
31944
32015
  }
31945
- /** One vertical box blur — the transpose of {@link boxBlurH}. */
32016
+ /**
32017
+ * One vertical box blur — the transpose of {@link boxBlurH}. Out-of-bounds
32018
+ * samples are transparent black (SVG 1.1 §15.17 `edgeMode="none"`) and so
32019
+ * contribute zero to the running sum.
32020
+ */
31946
32021
  function boxBlurV(data, width, height, topRadius, bottomRadius) {
31947
32022
  const out = new Float32Array(data.length);
31948
32023
  const windowLen = topRadius + bottomRadius + 1;
31949
32024
  for (let x = 0; x < width; x++) for (let c = 0; c < 4; c++) {
31950
32025
  let sum = 0;
31951
32026
  for (let k = -topRadius; k <= bottomRadius; k++) {
31952
- const cy = k < 0 ? 0 : k >= height ? height - 1 : k;
31953
- sum += data[(cy * width + x) * 4 + c];
32027
+ if (k < 0 || k >= height) continue;
32028
+ sum += data[(k * width + x) * 4 + c];
31954
32029
  }
31955
32030
  for (let y = 0; y < height; y++) {
31956
32031
  out[(y * width + x) * 4 + c] = sum / windowLen;
31957
32032
  const dropY = y - topRadius;
31958
32033
  const addY = y + bottomRadius + 1;
31959
- const dc = dropY < 0 ? 0 : dropY >= height ? height - 1 : dropY;
31960
- const ac = addY < 0 ? 0 : addY >= height ? height - 1 : addY;
31961
- sum += data[(ac * width + x) * 4 + c] - data[(dc * width + x) * 4 + c];
32034
+ if (addY >= 0 && addY < height) sum += data[(addY * width + x) * 4 + c];
32035
+ if (dropY >= 0 && dropY < height) sum -= data[(dropY * width + x) * 4 + c];
31962
32036
  }
31963
32037
  }
31964
32038
  return out;
@@ -33449,6 +33523,7 @@ function collectPages(children) {
33449
33523
  */
33450
33524
  async function renderToPdf(root) {
33451
33525
  let documentEl;
33526
+ const synthesized = [];
33452
33527
  const stack = resolveNode(root);
33453
33528
  while (stack.length > 0) {
33454
33529
  const node = stack.shift();
@@ -33461,13 +33536,13 @@ async function renderToPdf(root) {
33461
33536
  documentEl = node;
33462
33537
  break;
33463
33538
  }
33464
- documentEl = {
33465
- type: "document",
33466
- props: {},
33467
- children: [node]
33468
- };
33469
- break;
33539
+ synthesized.push(node);
33470
33540
  }
33541
+ if (documentEl === void 0 && synthesized.length > 0) documentEl = {
33542
+ type: "document",
33543
+ props: {},
33544
+ children: synthesized
33545
+ };
33471
33546
  const doc = require_pdfDocument.createPdf();
33472
33547
  const ctx = new RenderContext(doc);
33473
33548
  await ctx.init();
@@ -37243,7 +37318,7 @@ function conformanceString(conformance) {
37243
37318
  function convertPdfAConformanceXmp(xmp, toPart, toConformance) {
37244
37319
  const partVal = partString(toPart);
37245
37320
  let result = xmp;
37246
- const partAttr = /pdfaid:part\s*=\s*"[^"]*"/;
37321
+ const partAttr = /pdfaid:part\s*=\s*(["'])[^"']*\1/;
37247
37322
  const partElem = /<pdfaid:part>[^<]*<\/pdfaid:part>/;
37248
37323
  if (partAttr.test(result)) result = result.replace(partAttr, `pdfaid:part="${partVal}"`);
37249
37324
  else if (partElem.test(result)) result = result.replace(partElem, `<pdfaid:part>${partVal}</pdfaid:part>`);
@@ -37253,7 +37328,7 @@ function convertPdfAConformanceXmp(xmp, toPart, toConformance) {
37253
37328
  }
37254
37329
  if (toConformance !== void 0) {
37255
37330
  const confVal = conformanceString(toConformance);
37256
- const confAttr = /pdfaid:conformance\s*=\s*"[^"]*"/;
37331
+ const confAttr = /pdfaid:conformance\s*=\s*(["'])[^"']*\1/;
37257
37332
  const confElem = /<pdfaid:conformance>[^<]*<\/pdfaid:conformance>/;
37258
37333
  if (confAttr.test(result)) result = result.replace(confAttr, `pdfaid:conformance="${confVal}"`);
37259
37334
  else if (confElem.test(result)) result = result.replace(confElem, `<pdfaid:conformance>${confVal}</pdfaid:conformance>`);
@@ -37291,7 +37366,7 @@ function injectIdentification(xmp, partVal, toConformance) {
37291
37366
  function addConformanceBesidePart(xmp, confVal) {
37292
37367
  const partElem = /(<pdfaid:part>[^<]*<\/pdfaid:part>)/;
37293
37368
  if (partElem.test(xmp)) return xmp.replace(partElem, `$1<pdfaid:conformance>${confVal}</pdfaid:conformance>`);
37294
- const partAttr = /(pdfaid:part\s*=\s*"[^"]*")/;
37369
+ const partAttr = /(pdfaid:part\s*=\s*(["'])[^"']*\2)/;
37295
37370
  if (partAttr.test(xmp)) return xmp.replace(partAttr, `$1 pdfaid:conformance="${confVal}"`);
37296
37371
  return xmp;
37297
37372
  }
@@ -37544,10 +37619,28 @@ function isNonEmptyText(value) {
37544
37619
  function amountsEqual(a, b) {
37545
37620
  return Math.abs(a - b) <= MONETARY_TOLERANCE;
37546
37621
  }
37547
- /** Sum the line net amounts (Σ quantity × unit price) of an invoice. */
37622
+ /**
37623
+ * Round a monetary amount to two decimals using half-away-from-zero, the
37624
+ * commercial rounding convention EN 16931 applies to BT-* amounts (and the
37625
+ * convention the Factur-X generator emits). `Math.round` is half-up toward
37626
+ * +∞, which is wrong for negative amounts (e.g. a credit line), so the sign
37627
+ * is factored out explicitly.
37628
+ */
37629
+ function roundToCents(value) {
37630
+ return Math.sign(value) * Math.round(Math.abs(value) * 100) / 100;
37631
+ }
37632
+ /**
37633
+ * Sum the line net amounts (Σ BT-131) of an invoice.
37634
+ *
37635
+ * Per EN 16931 each line net amount (BT-131) is rounded to two decimals
37636
+ * before the document-level sum (BT-106 / BT-109) is formed. Summing the raw
37637
+ * `quantity × unitPrice` products instead would diverge from the spec total
37638
+ * for fractional unit prices and could falsely flag (or falsely pass) a
37639
+ * borderline BR-CO-10 / BR-CO-13 comparison.
37640
+ */
37548
37641
  function sumLineNet(invoice) {
37549
37642
  let total = 0;
37550
- for (const line of invoice.lines) total += line.quantity * line.unitPrice;
37643
+ for (const line of invoice.lines) total += roundToCents(line.quantity * line.unitPrice);
37551
37644
  return total;
37552
37645
  }
37553
37646
  /**
@@ -37825,15 +37918,15 @@ async function initWasm(options) {
37825
37918
  if (wasmInitialized) return;
37826
37919
  const inits = [];
37827
37920
  if (options.deflate || options.deflateWasm) inits.push((async () => {
37828
- const { initDeflateWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.libdeflateWasm_exports);
37921
+ const { initDeflateWasm } = await Promise.resolve().then(() => require("./pdfDocument-CnVPQ_Cm.cjs")).then((n) => n.libdeflateWasm_exports);
37829
37922
  await initDeflateWasm(options.deflateWasm);
37830
37923
  })());
37831
37924
  if (options.png || options.pngWasm) inits.push((async () => {
37832
- const { initPngWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.pngEmbed_exports);
37925
+ const { initPngWasm } = await Promise.resolve().then(() => require("./pdfDocument-CnVPQ_Cm.cjs")).then((n) => n.pngEmbed_exports);
37833
37926
  await initPngWasm(options.pngWasm);
37834
37927
  })());
37835
37928
  if (options.fonts || options.fontWasm) inits.push((async () => {
37836
- const { initSubsetWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.fontSubset_exports);
37929
+ const { initSubsetWasm } = await Promise.resolve().then(() => require("./pdfDocument-CnVPQ_Cm.cjs")).then((n) => n.fontSubset_exports);
37837
37930
  await initSubsetWasm(options.fontWasm);
37838
37931
  })());
37839
37932
  if (options.jpeg || options.jpegWasm) inits.push((async () => {
@@ -40293,4 +40386,4 @@ Object.defineProperty(exports, "xyzToRgb", {
40293
40386
  }
40294
40387
  });
40295
40388
 
40296
- //# sourceMappingURL=src-aDYhSnal.cjs.map
40389
+ //# sourceMappingURL=src-DFojMRwY.cjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modern-pdf-lib",
3
- "version": "0.40.0",
3
+ "version": "0.40.2",
4
4
  "description": "A modern, WASM-accelerated PDF engine to create, parse, extract, fill, merge, and sign PDFs in every JavaScript runtime",
5
5
  "type": "module",
6
6
  "exports": {
@@ -97,11 +97,11 @@
97
97
  "fflate": "0.8.3"
98
98
  },
99
99
  "devDependencies": {
100
- "@playwright/test": "1.62.0-alpha-2026-06-25",
100
+ "@playwright/test": "1.62.0-alpha-2026-06-27",
101
101
  "@vitest/coverage-v8": "5.0.0-beta.5",
102
102
  "esbuild": "0.28.1",
103
103
  "fast-png": "8.0.0",
104
- "miniflare": "4.20260623.0",
104
+ "miniflare": "4.20260625.0",
105
105
  "oxlint": "^1.71.0",
106
106
  "oxlint-tsgolint": "^0.23.0",
107
107
  "pdf-lib": "^1.17.1",