modern-pdf-lib 0.34.0 → 0.35.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.
@@ -1,4 +1,4 @@
1
- const require_pdfDocument = require("./pdfDocument-Dyj4AmT1.cjs");
1
+ const require_pdfDocument = require("./pdfDocument-D5qz7duP.cjs");
2
2
  const require_pdfObjects = require("./pdfObjects-BcPlSI0a.cjs");
3
3
  const require_streamDecode = require("./streamDecode-kQ-REV7v.cjs");
4
4
  const require_pdfForm = require("./pdfForm-BVS_do95.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-Dyj4AmT1.cjs")).then((n) => n.pdfCatalog_exports);
280
+ const { buildDocumentStructure } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.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(),
@@ -28081,6 +28081,1041 @@ function buildCertPath(leafCertDer, intermediates, anchors) {
28081
28081
  };
28082
28082
  }
28083
28083
  //#endregion
28084
+ //#region src/security/threatScanner.ts
28085
+ /** Numeric rank for severities so we can take a maximum. */
28086
+ const SEVERITY_RANK = {
28087
+ low: 1,
28088
+ medium: 2,
28089
+ high: 3
28090
+ };
28091
+ /** Reverse lookup from rank back to severity. */
28092
+ const RANK_SEVERITY = [
28093
+ "low",
28094
+ "medium",
28095
+ "high"
28096
+ ];
28097
+ /**
28098
+ * File-name extensions that denote directly executable / script payloads.
28099
+ * An attachment ending in one of these is treated as high severity because a
28100
+ * viewer's "open attachment" (or a `/Launch` action targeting it) can run it.
28101
+ */
28102
+ const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
28103
+ "exe",
28104
+ "com",
28105
+ "scr",
28106
+ "pif",
28107
+ "cpl",
28108
+ "msi",
28109
+ "msp",
28110
+ "mst",
28111
+ "dll",
28112
+ "sys",
28113
+ "drv",
28114
+ "ocx",
28115
+ "bat",
28116
+ "cmd",
28117
+ "ps1",
28118
+ "psm1",
28119
+ "vbs",
28120
+ "vbe",
28121
+ "vb",
28122
+ "wsf",
28123
+ "wsh",
28124
+ "js",
28125
+ "jse",
28126
+ "jar",
28127
+ "class",
28128
+ "sh",
28129
+ "bash",
28130
+ "zsh",
28131
+ "ksh",
28132
+ "csh",
28133
+ "command",
28134
+ "app",
28135
+ "dmg",
28136
+ "pkg",
28137
+ "lnk",
28138
+ "url",
28139
+ "reg",
28140
+ "hta",
28141
+ "gadget",
28142
+ "inf",
28143
+ "scf",
28144
+ "apk",
28145
+ "deb",
28146
+ "rpm",
28147
+ "run",
28148
+ "bin",
28149
+ "elf",
28150
+ "so",
28151
+ "dylib",
28152
+ "py",
28153
+ "pyc",
28154
+ "pyw",
28155
+ "rb",
28156
+ "pl",
28157
+ "php"
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;
28163
+ return v.startsWith("/") ? v.slice(1) : v;
28164
+ }
28165
+ }
28166
+ /** Read a `PdfString` value from a dict entry. */
28167
+ function stringValue(obj) {
28168
+ if (obj !== void 0 && obj.kind === "string") return obj.value;
28169
+ }
28170
+ /** Format a `PdfRef` as the canonical `"N G R"` string, if available. */
28171
+ function refString(ref) {
28172
+ return ref ? `${ref.objectNumber} ${ref.generationNumber} R` : void 0;
28173
+ }
28174
+ /** Lower-case file extension of a name, or `''` when there is none. */
28175
+ function extensionOf(name) {
28176
+ const base = name.replace(/[\\/]+/g, "/").split("/").pop() ?? name;
28177
+ const dot = base.lastIndexOf(".");
28178
+ if (dot < 0 || dot === base.length - 1) return "";
28179
+ return base.slice(dot + 1).toLowerCase();
28180
+ }
28181
+ /**
28182
+ * Scan a PDF for hostile constructs and return a {@link ThreatReport}.
28183
+ *
28184
+ * The scan never executes any document content. It parses the PDF and walks
28185
+ * the parsed object graph (see the module header for the precise method and
28186
+ * ISO 32000 references). When parsing fails, a guarded raw-byte fallback runs.
28187
+ *
28188
+ * @param pdf Raw PDF bytes.
28189
+ * @returns A report of findings plus the aggregate `riskLevel`.
28190
+ */
28191
+ async function scanPdfThreats(pdf) {
28192
+ const findings = [];
28193
+ let scannedGraph = false;
28194
+ try {
28195
+ scanObjectGraph((await require_pdfDocument.loadPdf(pdf, { ignoreEncryption: true })).getRegistry(), findings);
28196
+ scannedGraph = true;
28197
+ } catch {
28198
+ scannedGraph = false;
28199
+ }
28200
+ if (!scannedGraph) scanRawBytesFallback(pdf, findings);
28201
+ return {
28202
+ findings,
28203
+ riskLevel: aggregateRisk(findings)
28204
+ };
28205
+ }
28206
+ /**
28207
+ * Walk every registered object and emit findings.
28208
+ *
28209
+ * We iterate the registry (every resolved indirect object). For each
28210
+ * dictionary (including stream dictionaries) we inspect that dict's *own
28211
+ * keys* and, where relevant, the resolved action-subtype `/S` *name*. Content
28212
+ * streams' raw bytes are never substring-scanned here, which is what makes the
28213
+ * scan precise (drawn text that merely says "JavaScript" is not a finding).
28214
+ */
28215
+ function scanObjectGraph(registry, findings) {
28216
+ for (const entry of registry) {
28217
+ const obj = entry.object;
28218
+ const refStr = refString(entry.ref);
28219
+ let dict;
28220
+ if (obj.kind === "dict") dict = obj;
28221
+ else if (obj.kind === "stream") dict = obj.dict;
28222
+ if (dict === void 0) continue;
28223
+ inspectDict(dict, refStr, findings);
28224
+ }
28225
+ }
28226
+ /**
28227
+ * Inspect a single dictionary's keys/values for dangerous constructs.
28228
+ *
28229
+ * @param dict The dictionary (or stream dictionary).
28230
+ * @param refStr The owning object's `"N G R"` string, if known.
28231
+ * @param findings Accumulator.
28232
+ */
28233
+ function inspectDict(dict, refStr, findings) {
28234
+ if (dict.has("/OpenAction")) findings.push({
28235
+ category: "OpenAction",
28236
+ severity: "medium",
28237
+ detail: "Document defines an /OpenAction that runs automatically when the PDF is opened (ISO 32000 §12.6.3).",
28238
+ objectRef: refStr
28239
+ });
28240
+ if (dict.has("/AA")) findings.push({
28241
+ category: "AA",
28242
+ severity: "medium",
28243
+ detail: "Document or object defines /AA additional actions that fire on viewer events such as open/close/print/save (ISO 32000 §12.6.2).",
28244
+ objectRef: refStr
28245
+ });
28246
+ if (dict.has("/JavaScript")) findings.push({
28247
+ category: "JavaScript",
28248
+ severity: "high",
28249
+ 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
+ objectRef: refStr
28251
+ });
28252
+ const subtype = nameValue$1(dict.get("/S"));
28253
+ if (subtype !== void 0) inspectActionSubtype(subtype, dict, refStr, findings);
28254
+ if (dict.has("/XFA")) findings.push({
28255
+ category: "XFA",
28256
+ severity: "low",
28257
+ detail: "Document contains an XFA form (/AcroForm → /XFA, ISO 32000 §12.7.8); XFA carries its own scripting and data-binding model.",
28258
+ objectRef: refStr
28259
+ });
28260
+ const annotSubtype = nameValue$1(dict.get("/Subtype"));
28261
+ if (annotSubtype === "RichMedia") findings.push({
28262
+ category: "RichMedia",
28263
+ severity: "low",
28264
+ detail: "Rich-media annotation (/Subtype /RichMedia, ISO 32000-2 §13.6) embeds interactive multimedia (e.g. Flash/video).",
28265
+ objectRef: refStr
28266
+ });
28267
+ else if (annotSubtype === "Movie") findings.push({
28268
+ category: "Movie",
28269
+ severity: "low",
28270
+ detail: "Movie annotation (/Subtype /Movie, ISO 32000 §13.4) embeds video/animation content.",
28271
+ objectRef: refStr
28272
+ });
28273
+ else if (annotSubtype === "Sound") findings.push({
28274
+ category: "Sound",
28275
+ severity: "low",
28276
+ detail: "Sound annotation (/Subtype /Sound, ISO 32000 §13.3) embeds audio content.",
28277
+ objectRef: refStr
28278
+ });
28279
+ if (dict.has("/RichMedia") && annotSubtype !== "RichMedia") findings.push({
28280
+ category: "RichMedia",
28281
+ severity: "low",
28282
+ detail: "Object references rich-media content via a /RichMedia entry (ISO 32000-2 §13.6).",
28283
+ objectRef: refStr
28284
+ });
28285
+ if (dict.has("/Movie") && annotSubtype !== "Movie") findings.push({
28286
+ category: "Movie",
28287
+ severity: "low",
28288
+ detail: "Object references movie content via a /Movie entry (ISO 32000 §13.4).",
28289
+ objectRef: refStr
28290
+ });
28291
+ if (dict.has("/Sound") && annotSubtype !== "Sound" && subtype !== "Sound") findings.push({
28292
+ category: "Sound",
28293
+ severity: "low",
28294
+ detail: "Object references sound content via a /Sound entry (ISO 32000 §13.3).",
28295
+ objectRef: refStr
28296
+ });
28297
+ if (nameValue$1(dict.get("/Type")) === "Filespec" || dict.has("/EF")) {
28298
+ const fileName = stringValue(dict.get("/UF")) ?? stringValue(dict.get("/F"));
28299
+ if (fileName !== void 0) {
28300
+ const ext = extensionOf(fileName);
28301
+ if (ext !== "" && EXECUTABLE_EXTENSIONS.has(ext)) findings.push({
28302
+ category: "EmbeddedFile",
28303
+ severity: "high",
28304
+ detail: `Embedded file "${fileName}" has an executable/script extension ".${ext}" (ISO 32000 §7.11.3/§7.11.4); opening it can run code.`,
28305
+ objectRef: refStr
28306
+ });
28307
+ }
28308
+ }
28309
+ }
28310
+ /**
28311
+ * Flag a dangerous action `/S` subtype. Subtypes are ISO 32000 §12.6.4 /
28312
+ * §12.7.5 action types.
28313
+ */
28314
+ function inspectActionSubtype(subtype, dict, refStr, findings) {
28315
+ switch (subtype) {
28316
+ case "JavaScript":
28317
+ findings.push({
28318
+ category: "JavaScript",
28319
+ severity: "high",
28320
+ detail: "JavaScript action (/S /JavaScript, ISO 32000 §12.6.4.16) executes script in the viewer.",
28321
+ objectRef: refStr
28322
+ });
28323
+ break;
28324
+ case "Launch":
28325
+ findings.push({
28326
+ category: "Launch",
28327
+ severity: "high",
28328
+ detail: "Launch action (/S /Launch, ISO 32000 §12.6.4.5) can run an external application or open a file.",
28329
+ objectRef: refStr
28330
+ });
28331
+ break;
28332
+ case "URI":
28333
+ findings.push({
28334
+ category: "URI",
28335
+ severity: "medium",
28336
+ detail: `URI action (/S /URI, ISO 32000 §12.6.4.7) opens a remote URL ${describeUri(dict)}.`,
28337
+ objectRef: refStr
28338
+ });
28339
+ break;
28340
+ case "SubmitForm":
28341
+ findings.push({
28342
+ category: "SubmitForm",
28343
+ severity: "medium",
28344
+ detail: "SubmitForm action (/S /SubmitForm, ISO 32000 §12.7.5.2) sends form data to a URL (potential data exfiltration).",
28345
+ objectRef: refStr
28346
+ });
28347
+ break;
28348
+ case "ImportData":
28349
+ findings.push({
28350
+ category: "ImportData",
28351
+ severity: "low",
28352
+ detail: "ImportData action (/S /ImportData, ISO 32000 §12.7.5.4) imports external form data.",
28353
+ objectRef: refStr
28354
+ });
28355
+ break;
28356
+ case "GoToR":
28357
+ findings.push({
28358
+ category: "GoToR",
28359
+ severity: "low",
28360
+ detail: "Remote Go-To action (/S /GoToR, ISO 32000 §12.6.4.3) targets an external document.",
28361
+ objectRef: refStr
28362
+ });
28363
+ break;
28364
+ case "Movie":
28365
+ findings.push({
28366
+ category: "Movie",
28367
+ severity: "low",
28368
+ detail: "Movie action (/S /Movie, ISO 32000 §12.6.4.10) plays embedded video.",
28369
+ objectRef: refStr
28370
+ });
28371
+ break;
28372
+ case "Sound":
28373
+ findings.push({
28374
+ category: "Sound",
28375
+ severity: "low",
28376
+ detail: "Sound action (/S /Sound, ISO 32000 §12.6.4.8) plays embedded audio.",
28377
+ objectRef: refStr
28378
+ });
28379
+ break;
28380
+ default: break;
28381
+ }
28382
+ }
28383
+ /** 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"));
28386
+ return uri !== void 0 ? `→ "${uri}"` : "";
28387
+ }
28388
+ /**
28389
+ * Best-effort raw-byte fallback used only when the PDF cannot be parsed.
28390
+ *
28391
+ * To stay precise we match constructs only in their *structural* dictionary
28392
+ * form — action subtypes as `/S /Type` (optionally without the inner space)
28393
+ * and `/OpenAction`/`/AA`/`/JavaScript` as dictionary keys (a `/` immediately
28394
+ * followed by the token and then a PDF delimiter). This avoids flagging the
28395
+ * same words when they appear as content-stream text or inside a literal
28396
+ * string. It is heuristic and clearly less reliable than the graph walk.
28397
+ */
28398
+ function scanRawBytesFallback(pdf, findings) {
28399
+ const text = new TextDecoder("latin1").decode(pdf);
28400
+ const keyForm = (key) => new RegExp(`/${key}(?=[\\s/(<\\[\\]>]|$)`);
28401
+ if (keyForm("OpenAction").test(text)) findings.push({
28402
+ category: "OpenAction",
28403
+ severity: "medium",
28404
+ detail: "Raw scan: /OpenAction key present (ISO 32000 §12.6.3)."
28405
+ });
28406
+ if (keyForm("AA").test(text)) findings.push({
28407
+ category: "AA",
28408
+ severity: "medium",
28409
+ detail: "Raw scan: /AA additional-actions key present (ISO 32000 §12.6.2)."
28410
+ });
28411
+ if (keyForm("JavaScript").test(text) || /\/S\s*\/JavaScript\b/.test(text)) findings.push({
28412
+ category: "JavaScript",
28413
+ severity: "high",
28414
+ detail: "Raw scan: JavaScript action/name tree present (ISO 32000 §12.6.4.16)."
28415
+ });
28416
+ if (/\/S\s*\/Launch\b/.test(text)) findings.push({
28417
+ category: "Launch",
28418
+ severity: "high",
28419
+ detail: "Raw scan: /S /Launch action present (ISO 32000 §12.6.4.5)."
28420
+ });
28421
+ if (/\/S\s*\/URI\b/.test(text)) findings.push({
28422
+ category: "URI",
28423
+ severity: "medium",
28424
+ detail: "Raw scan: /S /URI action present (ISO 32000 §12.6.4.7)."
28425
+ });
28426
+ if (/\/S\s*\/SubmitForm\b/.test(text)) findings.push({
28427
+ category: "SubmitForm",
28428
+ severity: "medium",
28429
+ detail: "Raw scan: /S /SubmitForm action present (ISO 32000 §12.7.5.2)."
28430
+ });
28431
+ if (/\/S\s*\/ImportData\b/.test(text)) findings.push({
28432
+ category: "ImportData",
28433
+ severity: "low",
28434
+ detail: "Raw scan: /S /ImportData action present (ISO 32000 §12.7.5.4)."
28435
+ });
28436
+ if (/\/S\s*\/GoToR\b/.test(text)) findings.push({
28437
+ category: "GoToR",
28438
+ severity: "low",
28439
+ detail: "Raw scan: /S /GoToR remote go-to present (ISO 32000 §12.6.4.3)."
28440
+ });
28441
+ if (/\/Subtype\s*\/RichMedia\b/.test(text) || keyForm("RichMedia").test(text)) findings.push({
28442
+ category: "RichMedia",
28443
+ severity: "low",
28444
+ detail: "Raw scan: RichMedia annotation present (ISO 32000-2 §13.6)."
28445
+ });
28446
+ if (/\/Subtype\s*\/Movie\b/.test(text) || /\/S\s*\/Movie\b/.test(text)) findings.push({
28447
+ category: "Movie",
28448
+ severity: "low",
28449
+ detail: "Raw scan: Movie content/action present (ISO 32000 §13.4)."
28450
+ });
28451
+ if (/\/Subtype\s*\/Sound\b/.test(text) || /\/S\s*\/Sound\b/.test(text)) findings.push({
28452
+ category: "Sound",
28453
+ severity: "low",
28454
+ detail: "Raw scan: Sound content/action present (ISO 32000 §13.3)."
28455
+ });
28456
+ if (keyForm("XFA").test(text)) findings.push({
28457
+ category: "XFA",
28458
+ severity: "low",
28459
+ detail: "Raw scan: /XFA form present (ISO 32000 §12.7.8)."
28460
+ });
28461
+ }
28462
+ /** Maximum severity across findings, or `'none'` when there are none. */
28463
+ function aggregateRisk(findings) {
28464
+ let maxRank = 0;
28465
+ for (const f of findings) {
28466
+ const rank = SEVERITY_RANK[f.severity];
28467
+ if (rank > maxRank) maxRank = rank;
28468
+ }
28469
+ if (maxRank === 0) return "none";
28470
+ return RANK_SEVERITY[maxRank - 1];
28471
+ }
28472
+ //#endregion
28473
+ //#region src/security/sanitize.ts
28474
+ /**
28475
+ * @module security/sanitize
28476
+ *
28477
+ * PDF sanitizer — produce a cleaned copy of a PDF with active / hidden
28478
+ * content neutralised.
28479
+ *
28480
+ * The sanitizer removes four classes of potentially dangerous or
28481
+ * privacy-leaking content from a document, each independently toggleable:
28482
+ *
28483
+ * - **javascript** — document-level JavaScript: the catalog's
28484
+ * `/Names → /JavaScript` name tree (ISO 32000-1 §7.7.4 / §12.6.4.16)
28485
+ * and the catalog's `/AA` (document additional-actions) dictionary
28486
+ * (§12.6.3) plus any page-level `/AA`.
28487
+ * - **openActions** — the catalog's auto-run `/OpenAction` (§12.3.4 / §12.6.4).
28488
+ * - **embeddedFiles** — embedded file attachments: the catalog's
28489
+ * `/Names → /EmbeddedFiles` name tree (§7.7.4 / §7.11.4) and the
28490
+ * PDF 2.0 catalog `/AF` (associated files) array (§7.11.4).
28491
+ * - **metadata** — the XMP `/Metadata` stream (§14.3.2) and the document
28492
+ * information `/Info` dictionary (§14.3.3).
28493
+ *
28494
+ * Implementation strategy (verified against this library's parser/writer):
28495
+ * the cleaned bytes are produced by mutating the *parsed object graph* in
28496
+ * place and re-serializing the registry directly through
28497
+ * {@link serializePdf}, **without** rebuilding the catalog. This is what
28498
+ * lets a disabled option genuinely preserve its content — the original
28499
+ * catalog (minus the removed references) is written verbatim.
28500
+ *
28501
+ * Removing a reference from the catalog is not sufficient on its own: the
28502
+ * underlying object (a JavaScript action dictionary, an `/EmbeddedFile`
28503
+ * stream, an XMP metadata stream, …) would otherwise remain physically
28504
+ * present in the output as an orphan. To guarantee the payload bytes are
28505
+ * actually gone, after deleting references this module runs the registry's
28506
+ * reachability filter from the catalog + info roots, dropping every object
28507
+ * no longer referenced.
28508
+ *
28509
+ * @packageDocumentation
28510
+ */
28511
+ /**
28512
+ * Resolve a {@link PdfObject} that may be an indirect reference to its
28513
+ * underlying value using the registry. Non-refs are returned as-is.
28514
+ * Returns `undefined` if a ref cannot be resolved.
28515
+ */
28516
+ function resolve$2(obj, registry) {
28517
+ if (obj === void 0) return void 0;
28518
+ if (obj instanceof require_pdfObjects.PdfRef) return registry.resolve(obj);
28519
+ return obj;
28520
+ }
28521
+ /** Resolve `obj` and return it only if it is a {@link PdfDict}. */
28522
+ function asDict(obj, registry) {
28523
+ const resolved = resolve$2(obj, registry);
28524
+ return resolved instanceof require_pdfObjects.PdfDict ? resolved : void 0;
28525
+ }
28526
+ /** Read a {@link PdfString} value, resolving an indirect reference if needed. */
28527
+ function asStringValue(obj, registry) {
28528
+ const resolved = resolve$2(obj, registry);
28529
+ return resolved instanceof require_pdfObjects.PdfString ? resolved.value : void 0;
28530
+ }
28531
+ /**
28532
+ * Collect every distinct `/Type /Catalog` dictionary registered in the
28533
+ * document, paired with its reference. A well-formed PDF has exactly one,
28534
+ * but malformed or incrementally-updated files can register more than one;
28535
+ * sanitizing all of them is the conservative, correct choice. The first
28536
+ * entry is treated as the document root.
28537
+ */
28538
+ function findCatalogEntries(registry) {
28539
+ const byObjNum = /* @__PURE__ */ new Map();
28540
+ for (const { ref, object } of registry) if (object instanceof require_pdfObjects.PdfDict) {
28541
+ const type = object.get("/Type");
28542
+ if (type instanceof require_pdfObjects.PdfName && type.value === "/Catalog") {
28543
+ if (!byObjNum.has(ref.objectNumber)) byObjNum.set(ref.objectNumber, {
28544
+ ref,
28545
+ dict: object
28546
+ });
28547
+ }
28548
+ }
28549
+ return [...byObjNum.values()];
28550
+ }
28551
+ /** Collect every distinct `/Type /Page` dictionary in the registry. */
28552
+ function findPages(registry) {
28553
+ const pages = /* @__PURE__ */ new Set();
28554
+ for (const { object } of registry) if (object instanceof require_pdfObjects.PdfDict) {
28555
+ const type = object.get("/Type");
28556
+ if (type instanceof require_pdfObjects.PdfName && type.value === "/Page") pages.add(object);
28557
+ }
28558
+ return [...pages];
28559
+ }
28560
+ /**
28561
+ * Determine whether a PDF name tree (ISO 32000-1 §7.7.4) contains at least
28562
+ * one entry. A name-tree node holds either a `/Names` array of
28563
+ * `[key value key value …]` pairs (leaf) or a `/Kids` array of child node
28564
+ * references (intermediate). The tree is walked recursively with a visited
28565
+ * guard against cyclic references in malformed files.
28566
+ */
28567
+ function nameTreeHasEntries(node, registry, visited = /* @__PURE__ */ new Set()) {
28568
+ const dict = asDict(node, registry);
28569
+ if (dict === void 0 || visited.has(dict)) return false;
28570
+ visited.add(dict);
28571
+ const names = resolve$2(dict.get("/Names"), registry);
28572
+ if (names instanceof require_pdfObjects.PdfArray && names.length >= 2) return true;
28573
+ const kids = resolve$2(dict.get("/Kids"), registry);
28574
+ if (kids instanceof require_pdfObjects.PdfArray) {
28575
+ for (const kid of kids.items) if (nameTreeHasEntries(kid, registry, visited)) return true;
28576
+ }
28577
+ return false;
28578
+ }
28579
+ /** Return the catalog's resolved `/Names` sub-dictionary, if present. */
28580
+ function getNamesDict(catalog, registry) {
28581
+ return asDict(catalog.get("/Names"), registry);
28582
+ }
28583
+ /**
28584
+ * Determine whether an additional-actions (`/AA`) dictionary contains any
28585
+ * action whose `/S` (action type) is `/JavaScript`. Each value in an `/AA`
28586
+ * dictionary is itself an action dictionary (§12.6.3).
28587
+ */
28588
+ function aaHasJavaScript(aa, registry) {
28589
+ if (aa === void 0) return false;
28590
+ for (const [, value] of aa) {
28591
+ const action = asDict(value, registry);
28592
+ if (action === void 0) continue;
28593
+ const s = action.get("/S");
28594
+ if (s instanceof require_pdfObjects.PdfName && s.value === "/JavaScript") return true;
28595
+ }
28596
+ return false;
28597
+ }
28598
+ /**
28599
+ * Descriptive `/Info` keys that constitute identifying metadata. The
28600
+ * auto-generated `/Producer`, `/CreationDate`, and `/ModDate` are NOT
28601
+ * treated as "present" for reporting purposes: a freshly created document
28602
+ * carries those by default and must still be considered clean. (When
28603
+ * metadata removal runs, the entire `/Info` dict is dropped regardless.)
28604
+ */
28605
+ const DESCRIPTIVE_INFO_KEYS = [
28606
+ "/Title",
28607
+ "/Author",
28608
+ "/Subject",
28609
+ "/Keywords",
28610
+ "/Creator"
28611
+ ];
28612
+ /**
28613
+ * Locate the trailer's `/Info` dictionary in the registry. The parser does
28614
+ * not expose the trailer publicly, so the Info dict is identified
28615
+ * structurally: it is a dictionary that is NOT a catalog/page/pages node
28616
+ * and carries at least one recognised document-information key.
28617
+ */
28618
+ function findInfoDict(registry, catalogs) {
28619
+ const catalogDicts = new Set(catalogs.map((c) => c.dict));
28620
+ const INFO_KEYS = [
28621
+ "/Title",
28622
+ "/Author",
28623
+ "/Subject",
28624
+ "/Keywords",
28625
+ "/Creator",
28626
+ "/Producer",
28627
+ "/CreationDate",
28628
+ "/ModDate",
28629
+ "/Trapped"
28630
+ ];
28631
+ for (const { object } of registry) {
28632
+ if (!(object instanceof require_pdfObjects.PdfDict) || catalogDicts.has(object)) continue;
28633
+ const type = object.get("/Type");
28634
+ if (type instanceof require_pdfObjects.PdfName) {
28635
+ const v = type.value;
28636
+ if (v === "/Catalog" || v === "/Pages" || v === "/Page" || v === "/Font") continue;
28637
+ }
28638
+ if (INFO_KEYS.some((k) => object.has(k))) return object;
28639
+ }
28640
+ }
28641
+ /** Read descriptive metadata from an `/Info` dict into a typed bag. */
28642
+ function readInfoMetadata(info, registry) {
28643
+ const meta = {};
28644
+ if (info === void 0) return meta;
28645
+ const title = asStringValue(info.get("/Title"), registry);
28646
+ if (title !== void 0) meta.title = title;
28647
+ const author = asStringValue(info.get("/Author"), registry);
28648
+ if (author !== void 0) meta.author = author;
28649
+ const subject = asStringValue(info.get("/Subject"), registry);
28650
+ if (subject !== void 0) meta.subject = subject;
28651
+ const keywords = asStringValue(info.get("/Keywords"), registry);
28652
+ if (keywords !== void 0) meta.keywords = keywords;
28653
+ const creator = asStringValue(info.get("/Creator"), registry);
28654
+ if (creator !== void 0) meta.creator = creator;
28655
+ const producer = asStringValue(info.get("/Producer"), registry);
28656
+ if (producer !== void 0) meta.producer = producer;
28657
+ return meta;
28658
+ }
28659
+ /**
28660
+ * Produce a cleaned copy of a PDF with active / hidden content neutralised.
28661
+ *
28662
+ * Loads the PDF, removes each enabled class of content from the parsed
28663
+ * object graph, prunes the orphaned objects, re-serializes the cleaned
28664
+ * document, and returns the new bytes alongside a report listing the
28665
+ * classes that were actually present and removed.
28666
+ *
28667
+ * @param pdf The source PDF bytes.
28668
+ * @param options Which classes to remove (each defaults to `true`).
28669
+ * @returns The cleaned PDF bytes and a {@link SanitizeReport}.
28670
+ *
28671
+ * @example
28672
+ * ```ts
28673
+ * const { pdf, report } = await sanitizePdf(bytes);
28674
+ * // report.removed === ['javascript', 'embeddedFiles', 'metadata']
28675
+ * ```
28676
+ */
28677
+ async function sanitizePdf(pdf, options) {
28678
+ const removeJavaScript = options?.javascript ?? true;
28679
+ const removeOpenActions = options?.openActions ?? true;
28680
+ const removeEmbeddedFiles = options?.embeddedFiles ?? true;
28681
+ const removeMetadata = options?.metadata ?? true;
28682
+ const registry = (await require_pdfDocument.loadPdf(pdf)).getRegistry();
28683
+ const catalogs = findCatalogEntries(registry);
28684
+ if (catalogs.length === 0) return {
28685
+ pdf,
28686
+ report: { removed: [] }
28687
+ };
28688
+ const pages = findPages(registry);
28689
+ const infoDict = findInfoDict(registry, catalogs);
28690
+ const removed = /* @__PURE__ */ new Set();
28691
+ if (removeJavaScript) {
28692
+ for (const { dict: catalog } of catalogs) {
28693
+ const names = getNamesDict(catalog, registry);
28694
+ if (names !== void 0 && nameTreeHasEntries(names.get("/JavaScript"), registry)) removed.add("javascript");
28695
+ if (aaHasJavaScript(asDict(catalog.get("/AA"), registry), registry)) removed.add("javascript");
28696
+ }
28697
+ for (const page of pages) if (aaHasJavaScript(asDict(page.get("/AA"), registry), registry)) removed.add("javascript");
28698
+ for (const { dict: catalog } of catalogs) {
28699
+ const names = getNamesDict(catalog, registry);
28700
+ if (names !== void 0) {
28701
+ names.delete("/JavaScript");
28702
+ if (names.size === 0) catalog.delete("/Names");
28703
+ }
28704
+ catalog.delete("/AA");
28705
+ }
28706
+ for (const page of pages) page.delete("/AA");
28707
+ }
28708
+ if (removeOpenActions) {
28709
+ for (const { dict: catalog } of catalogs) if (catalog.has("/OpenAction")) {
28710
+ removed.add("openActions");
28711
+ catalog.delete("/OpenAction");
28712
+ }
28713
+ }
28714
+ if (removeEmbeddedFiles) {
28715
+ for (const { dict: catalog } of catalogs) {
28716
+ const names = getNamesDict(catalog, registry);
28717
+ if (names !== void 0 && nameTreeHasEntries(names.get("/EmbeddedFiles"), registry)) removed.add("embeddedFiles");
28718
+ const af = resolve$2(catalog.get("/AF"), registry);
28719
+ if (af instanceof require_pdfObjects.PdfArray && af.length > 0) removed.add("embeddedFiles");
28720
+ }
28721
+ for (const { dict: catalog } of catalogs) {
28722
+ const names = getNamesDict(catalog, registry);
28723
+ if (names !== void 0) {
28724
+ names.delete("/EmbeddedFiles");
28725
+ if (names.size === 0) catalog.delete("/Names");
28726
+ }
28727
+ catalog.delete("/AF");
28728
+ }
28729
+ }
28730
+ const preservedMeta = readInfoMetadata(infoDict, registry);
28731
+ if (removeMetadata) {
28732
+ let metadataPresent = false;
28733
+ for (const { dict: catalog } of catalogs) if (resolve$2(catalog.get("/Metadata"), registry) instanceof require_pdfObjects.PdfStream) metadataPresent = true;
28734
+ if (infoDict !== void 0 && DESCRIPTIVE_INFO_KEYS.some((k) => infoDict.has(k))) metadataPresent = true;
28735
+ if (metadataPresent) removed.add("metadata");
28736
+ for (const { dict: catalog } of catalogs) catalog.delete("/Metadata");
28737
+ if (infoDict !== void 0) for (const [key] of [...infoDict]) infoDict.delete(key);
28738
+ }
28739
+ const root = catalogs[0];
28740
+ const pagesRef = root.dict.get("/Pages");
28741
+ let infoRef;
28742
+ if (removeMetadata) infoRef = require_pdfDocument.buildInfoDict({}, registry);
28743
+ else infoRef = require_pdfDocument.buildInfoDict(preservedMeta, registry);
28744
+ const structure = {
28745
+ catalogRef: root.ref,
28746
+ infoRef,
28747
+ pagesRef: pagesRef instanceof require_pdfObjects.PdfRef ? pagesRef : root.ref
28748
+ };
28749
+ registry.filterReachable([structure.catalogRef, structure.infoRef]);
28750
+ return {
28751
+ pdf: await require_pdfDocument.serializePdf(registry, structure, { compress: true }),
28752
+ report: { removed: [...removed] }
28753
+ };
28754
+ }
28755
+ //#endregion
28756
+ //#region src/security/redactionVerifier.ts
28757
+ /**
28758
+ * @module security/redactionVerifier
28759
+ *
28760
+ * Detect FAILED / fake redactions — text that is still extractable underneath
28761
+ * a redaction region.
28762
+ *
28763
+ * A *secure* redaction physically removes the underlying text-showing
28764
+ * operators from the page content stream (see `redactRegions` in
28765
+ * `../render/redactContent.js`), so the redacted text cannot be recovered by
28766
+ * copy/paste, text extraction, or raw stream inspection. A *fake* redaction
28767
+ * merely paints an opaque black box on top of the page while the original text
28768
+ * remains in the byte stream — a genuine, well-documented security failure
28769
+ * (the "select-all-and-copy" leak). This verifier finds those leaks.
28770
+ *
28771
+ * ## How it works
28772
+ *
28773
+ * For each page named by a region:
28774
+ * 1. The page's content stream is decoded
28775
+ * (`PdfPage.getContentStream()`) and parsed (`parseContentStream`).
28776
+ * 2. `extractTextWithPositions` replays the text/graphics-state machine and
28777
+ * yields one positioned {@link import('../parser/textExtractor.js').TextItem}
28778
+ * per shown text run, with its page-space origin and size.
28779
+ * 3. Every text item whose bounding box intersects a region rectangle is
28780
+ * reported as a leak. `clean` is true iff there are no leaks.
28781
+ *
28782
+ * ## Coordinate / encoding conventions (confirmed, not assumed)
28783
+ *
28784
+ * - **Space:** PDF user space — the coordinate space produced by
28785
+ * `extractTextWithPositions`. Origin is **bottom-left**, the y-axis points
28786
+ * **up**, and units are **PDF points** (1/72 inch). This matches
28787
+ * `RedactRect` in `../render/redactContent.js` ("page space, PDF points,
28788
+ * y-up, lower-left origin"), so a {@link RedactionRegion} uses exactly the
28789
+ * same convention: `(x, y)` is the lower-left corner and `width` / `height`
28790
+ * extend in the +x / +y directions.
28791
+ * - **Glyph box:** a `TextItem` reports `(x, y)` = the text origin
28792
+ * (baseline-left), `width` extending +x, and `height` (equal to the font
28793
+ * size) extending +y. The run's axis-aligned box is therefore
28794
+ * `[x, y, x + width, y + height]`. Note `width`/`height` are heuristic
28795
+ * estimates from `extractTextWithPositions` (0.5·fontSize per character;
28796
+ * height = fontSize) because per-glyph font metrics are not available there;
28797
+ * intersection testing tolerates this by treating any overlap as a leak.
28798
+ * - **Text decoding:** strings are decoded by `extractTextWithPositions` using
28799
+ * the font's `/ToUnicode` CMap when present, else WinAnsiEncoding (1-byte) or
28800
+ * 2-byte Identity-H for CID fonts — see `../parser/textExtractor.js`.
28801
+ *
28802
+ * ## Region auto-detection is intentionally NOT performed
28803
+ *
28804
+ * The `regions` argument is **required**. Deriving candidate regions from the
28805
+ * PDF itself is not reliable through the public API and is not fabricated here:
28806
+ * - Filled black rectangles in the content stream are indistinguishable from
28807
+ * legitimate black-filled graphics (chart bars, rules, design elements), so
28808
+ * a heuristic would produce false positives/negatives.
28809
+ * - `/Redact` annotations (ISO 32000-2 §12.5.6.23) *do* carry explicit
28810
+ * `/Rect` / `/QuadPoints` regions, but a loaded page's original annotation
28811
+ * dictionaries are not exposed through a stable public `PdfPage` accessor,
28812
+ * so reading them here is not possible without reaching into private state.
28813
+ * Callers must therefore pass the regions they redacted (or believe were
28814
+ * redacted). When `regions` is omitted or empty, {@link verifyRedactions}
28815
+ * throws.
28816
+ *
28817
+ * @packageDocumentation
28818
+ */
28819
+ /**
28820
+ * Axis-aligned bounding box of a text run in page space (bottom-left origin).
28821
+ * `width`/`height` may be zero (empty text); such runs never intersect.
28822
+ */
28823
+ function itemBox(item) {
28824
+ const x0 = item.x;
28825
+ const y0 = item.y;
28826
+ const x1 = item.x + item.width;
28827
+ const y1 = item.y + item.height;
28828
+ return {
28829
+ minX: Math.min(x0, x1),
28830
+ minY: Math.min(y0, y1),
28831
+ maxX: Math.max(x0, x1),
28832
+ maxY: Math.max(y0, y1)
28833
+ };
28834
+ }
28835
+ /**
28836
+ * Whether a text run's box overlaps a region rectangle.
28837
+ *
28838
+ * Uses the standard separating-axis test for axis-aligned rectangles: they
28839
+ * overlap unless one is entirely to the left/right/above/below the other.
28840
+ * Touching edges count as overlap (inclusive bounds), matching the intersection
28841
+ * test in `../render/redactContent.js`.
28842
+ */
28843
+ function intersects$1(box, region) {
28844
+ const rMinX = region.x;
28845
+ const rMinY = region.y;
28846
+ const rMaxX = region.x + region.width;
28847
+ const rMaxY = region.y + region.height;
28848
+ return box.minX <= rMaxX && box.maxX >= rMinX && box.minY <= rMaxY && box.maxY >= rMinY;
28849
+ }
28850
+ /**
28851
+ * Verify that the given regions of a PDF contain no still-extractable text,
28852
+ * detecting fake / failed redactions.
28853
+ *
28854
+ * For each region, the corresponding page's text is extracted with positions
28855
+ * and any text run whose bounding box intersects the region is reported as a
28856
+ * {@link RedactionLeak}. A clean result means no text was found under any
28857
+ * region (the redactions truly removed the content, or there was none to begin
28858
+ * with).
28859
+ *
28860
+ * @param pdf The PDF file bytes to inspect.
28861
+ * @param regions The regions to check (REQUIRED — see the module docs for why
28862
+ * automatic region detection is not performed). Coordinates are
28863
+ * in PDF user space: origin bottom-left, y-up, units in points;
28864
+ * `(x, y)` is the lower-left corner.
28865
+ * @returns A report listing every leak; `clean` is true iff there are
28866
+ * none.
28867
+ * @throws `TypeError` if `regions` is omitted or empty.
28868
+ *
28869
+ * @example
28870
+ * ```ts
28871
+ * const report = await verifyRedactions(pdfBytes, [
28872
+ * { page: 0, x: 45, y: 545, width: 120, height: 25 },
28873
+ * ]);
28874
+ * if (!report.clean) {
28875
+ * for (const leak of report.leaks) {
28876
+ * console.warn(`Leak on page ${leak.page}: "${leak.text}"`);
28877
+ * }
28878
+ * }
28879
+ * ```
28880
+ */
28881
+ async function verifyRedactions(pdf, regions) {
28882
+ 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
+ const doc = await require_pdfDocument.loadPdf(pdf);
28884
+ const pageCount = doc.getPageCount();
28885
+ const itemsByPage = /* @__PURE__ */ new Map();
28886
+ const getItems = (pageIndex) => {
28887
+ const cached = itemsByPage.get(pageIndex);
28888
+ if (cached !== void 0) return cached;
28889
+ let items = [];
28890
+ if (pageIndex >= 0 && pageIndex < pageCount) {
28891
+ const page = doc.getPage(pageIndex);
28892
+ items = require_compressionAnalysis.extractTextWithPositions(require_compressionAnalysis.parseContentStream(page.getContentStream()), page.getOriginalResources());
28893
+ }
28894
+ itemsByPage.set(pageIndex, items);
28895
+ return items;
28896
+ };
28897
+ const leaks = [];
28898
+ for (const region of regions) {
28899
+ if (region.width <= 0 || region.height <= 0) continue;
28900
+ const items = getItems(region.page);
28901
+ for (const item of items) {
28902
+ if (item.text.length === 0) continue;
28903
+ const box = itemBox(item);
28904
+ if (box.maxX === box.minX && box.maxY === box.minY) continue;
28905
+ if (intersects$1(box, region)) leaks.push({
28906
+ page: region.page,
28907
+ text: item.text,
28908
+ x: item.x,
28909
+ y: item.y
28910
+ });
28911
+ }
28912
+ }
28913
+ return {
28914
+ leaks,
28915
+ clean: leaks.length === 0,
28916
+ regionsChecked: regions.length
28917
+ };
28918
+ }
28919
+ //#endregion
28920
+ //#region src/security/encryptionInspector.ts
28921
+ const BIT_PRINT = 4;
28922
+ const BIT_MODIFY = 8;
28923
+ const BIT_COPY = 16;
28924
+ const BIT_ANNOTATE = 32;
28925
+ const BIT_FILL_FORMS = 256;
28926
+ const BIT_ASSEMBLE = 1024;
28927
+ const BIT_PRINT_HIGH_RES = 2048;
28928
+ /**
28929
+ * Inspect a PDF's encryption + permission posture.
28930
+ *
28931
+ * @param pdf The raw PDF file bytes.
28932
+ * @returns A {@link EncryptionReport}. If the trailer has no
28933
+ * `/Encrypt` entry (or the file is unparseable) the report is
28934
+ * `{ encrypted: false }`.
28935
+ */
28936
+ async function inspectEncryption(pdf) {
28937
+ const located = await locateEncryptDict(pdf);
28938
+ if (located === void 0) return { encrypted: false };
28939
+ const { encryptDict, fileId, encryptRef } = located;
28940
+ const filterName = getName(encryptDict, "/Filter");
28941
+ let handler;
28942
+ if (filterName === "/Standard") handler = "password";
28943
+ else if (filterName === "/Adobe.PubSec") handler = "publicKey";
28944
+ const version = getNumber(encryptDict, "/V");
28945
+ const revision = getNumber(encryptDict, "/R");
28946
+ const { method, keyBits } = decodeAlgorithm(encryptDict, version);
28947
+ let permissions;
28948
+ const pValue = getNumber(encryptDict, "/P");
28949
+ if (handler === "password" && pValue !== void 0) permissions = decodePermissionBits(pValue);
28950
+ let emptyUserPassword;
28951
+ if (handler === "password") emptyUserPassword = await testEmptyUserPassword(encryptDict, fileId);
28952
+ const report = { encrypted: true };
28953
+ if (method !== void 0) report.method = method;
28954
+ if (keyBits !== void 0) report.keyBits = keyBits;
28955
+ if (version !== void 0) report.version = version;
28956
+ if (revision !== void 0) report.revision = revision;
28957
+ if (handler !== void 0) report.handler = handler;
28958
+ if (emptyUserPassword !== void 0) report.emptyUserPassword = emptyUserPassword;
28959
+ if (permissions !== void 0) report.permissions = permissions;
28960
+ return report;
28961
+ }
28962
+ /**
28963
+ * Parse just enough of the PDF (header + xref + trailer) to resolve the
28964
+ * `/Encrypt` dictionary and the first element of the `/ID` array.
28965
+ *
28966
+ * Returns `undefined` when the file has no `/Encrypt` entry or when it
28967
+ * cannot be parsed far enough to find one.
28968
+ */
28969
+ async function locateEncryptDict(pdf) {
28970
+ if (pdf.length < 8) return void 0;
28971
+ let encryptRef;
28972
+ let fileIdFirst;
28973
+ let entry;
28974
+ let objectParser;
28975
+ try {
28976
+ objectParser = new require_pdfDocument.PdfObjectParser(new require_pdfDocument.PdfLexer(pdf), new require_pdfObjects.PdfObjectRegistry());
28977
+ const { entries, trailer } = await new require_pdfDocument.XrefParser(pdf, objectParser).parseXref();
28978
+ encryptRef = trailer.encryptRef;
28979
+ if (encryptRef === void 0) return void 0;
28980
+ fileIdFirst = trailer.id?.[0] ?? /* @__PURE__ */ new Uint8Array(0);
28981
+ entry = entries.get(encryptRef.objectNumber);
28982
+ } catch {
28983
+ return;
28984
+ }
28985
+ if (entry === void 0 || entry.type !== "in-use") return void 0;
28986
+ let encryptObj;
28987
+ try {
28988
+ const { object } = objectParser.parseIndirectObjectAt(entry.offset);
28989
+ encryptObj = object;
28990
+ } catch {
28991
+ return;
28992
+ }
28993
+ if (encryptObj.kind !== "dict") return void 0;
28994
+ return {
28995
+ encryptDict: encryptObj,
28996
+ encryptRef,
28997
+ fileId: fileIdFirst
28998
+ };
28999
+ }
29000
+ /**
29001
+ * Decode the bulk cipher and key length from an encryption dictionary.
29002
+ *
29003
+ * - V=1: 40-bit RC4 (Table 20/21).
29004
+ * - V=2: RC4 with `/Length` bits.
29005
+ * - V=4: inspect the crypt filter `/CFM` of the filter named by `/StmF`
29006
+ * (falling back to `/StdCF`): `/V2` → RC4, `/AESV2` → 128-bit AES,
29007
+ * `/AESV3` → 256-bit AES, `/Identity`/`/None` → no encryption.
29008
+ * - V=5: 256-bit AES (`/AESV3`).
29009
+ */
29010
+ function decodeAlgorithm(dict, version) {
29011
+ const lengthBits = getNumber(dict, "/Length");
29012
+ if (version === 1) return {
29013
+ method: "rc4",
29014
+ keyBits: 40
29015
+ };
29016
+ if (version === 2) return {
29017
+ method: "rc4",
29018
+ keyBits: lengthBits ?? 40
29019
+ };
29020
+ if (version === 5) return {
29021
+ method: "aes",
29022
+ keyBits: 256
29023
+ };
29024
+ if (version === 4) {
29025
+ const cfm = resolveCryptFilterMethod(dict);
29026
+ if (cfm === "/AESV3") return {
29027
+ method: "aes",
29028
+ keyBits: 256
29029
+ };
29030
+ if (cfm === "/AESV2") return {
29031
+ method: "aes",
29032
+ keyBits: lengthBits ?? 128
29033
+ };
29034
+ if (cfm === "/V2") return {
29035
+ method: "rc4",
29036
+ keyBits: lengthBits ?? 128
29037
+ };
29038
+ return {
29039
+ method: void 0,
29040
+ keyBits: lengthBits ?? void 0
29041
+ };
29042
+ }
29043
+ return {
29044
+ method: void 0,
29045
+ keyBits: lengthBits ?? void 0
29046
+ };
29047
+ }
29048
+ /**
29049
+ * For V=4 documents, resolve the `/CFM` (crypt filter method) name of the
29050
+ * stream crypt filter. Looks up `/StmF` in `/CF`, then falls back to the
29051
+ * conventional `/StdCF` entry.
29052
+ */
29053
+ function resolveCryptFilterMethod(dict) {
29054
+ const cf = dict.get("/CF");
29055
+ if (cf === void 0 || cf.kind !== "dict") return void 0;
29056
+ const cfDict = cf;
29057
+ let filterName = getName(dict, "/StmF");
29058
+ if (filterName === "/Identity") return "/Identity";
29059
+ let stdCf = filterName !== void 0 ? cfDict.get(filterName) : void 0;
29060
+ if (stdCf === void 0 || stdCf.kind !== "dict") {
29061
+ stdCf = cfDict.get("/StdCF");
29062
+ filterName = "/StdCF";
29063
+ }
29064
+ if (stdCf === void 0 || stdCf.kind !== "dict") return void 0;
29065
+ return getName(stdCf, "/CFM");
29066
+ }
29067
+ /**
29068
+ * Decode the 32-bit `/P` integer into individual permission flags.
29069
+ *
29070
+ * The `/P` value is a *signed* 32-bit integer; bitwise AND in JavaScript
29071
+ * operates on the 32-bit two's-complement representation, so masks apply
29072
+ * correctly even when `/P` is negative (the usual case, since reserved
29073
+ * high bits are set to 1).
29074
+ */
29075
+ function decodePermissionBits(p) {
29076
+ const has = (mask) => (p & mask) !== 0;
29077
+ const copy = has(BIT_COPY);
29078
+ return {
29079
+ print: has(BIT_PRINT),
29080
+ modify: has(BIT_MODIFY),
29081
+ copy,
29082
+ annotate: has(BIT_ANNOTATE),
29083
+ fillForms: has(BIT_FILL_FORMS),
29084
+ extract: copy,
29085
+ assemble: has(BIT_ASSEMBLE),
29086
+ printHighRes: has(BIT_PRINT_HIGH_RES)
29087
+ };
29088
+ }
29089
+ /**
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.
29094
+ *
29095
+ * Returns `undefined` when the test cannot be performed (e.g. the handler
29096
+ * implementation does not support the document's /V/R).
29097
+ */
29098
+ async function testEmptyUserPassword(dict, fileId) {
29099
+ 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;
29105
+ return;
29106
+ }
29107
+ }
29108
+ /** Read a `/Name` value (including the leading `/`) from a dict. */
29109
+ function getName(dict, key) {
29110
+ const obj = dict.get(key);
29111
+ if (obj !== void 0 && obj.kind === "name") return obj.value;
29112
+ }
29113
+ /** Read a numeric value from a dict. */
29114
+ function getNumber(dict, key) {
29115
+ const obj = dict.get(key);
29116
+ if (obj !== void 0 && obj.kind === "number") return obj.value;
29117
+ }
29118
+ //#endregion
28084
29119
  //#region src/render/matrix.ts
28085
29120
  /** The identity transform. */
28086
29121
  function identity() {
@@ -31889,15 +32924,15 @@ async function initWasm(options) {
31889
32924
  if (wasmInitialized) return;
31890
32925
  const inits = [];
31891
32926
  if (options.deflate || options.deflateWasm) inits.push((async () => {
31892
- const { initDeflateWasm } = await Promise.resolve().then(() => require("./pdfDocument-Dyj4AmT1.cjs")).then((n) => n.libdeflateWasm_exports);
32927
+ const { initDeflateWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.libdeflateWasm_exports);
31893
32928
  await initDeflateWasm(options.deflateWasm);
31894
32929
  })());
31895
32930
  if (options.png || options.pngWasm) inits.push((async () => {
31896
- const { initPngWasm } = await Promise.resolve().then(() => require("./pdfDocument-Dyj4AmT1.cjs")).then((n) => n.pngEmbed_exports);
32931
+ const { initPngWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.pngEmbed_exports);
31897
32932
  await initPngWasm(options.pngWasm);
31898
32933
  })());
31899
32934
  if (options.fonts || options.fontWasm) inits.push((async () => {
31900
- const { initSubsetWasm } = await Promise.resolve().then(() => require("./pdfDocument-Dyj4AmT1.cjs")).then((n) => n.fontSubset_exports);
32935
+ const { initSubsetWasm } = await Promise.resolve().then(() => require("./pdfDocument-D5qz7duP.cjs")).then((n) => n.fontSubset_exports);
31901
32936
  await initSubsetWasm(options.fontWasm);
31902
32937
  })());
31903
32938
  if (options.jpeg || options.jpegWasm) inits.push((async () => {
@@ -33312,6 +34347,12 @@ Object.defineProperty(exports, "injectJpegMetadata", {
33312
34347
  return injectJpegMetadata;
33313
34348
  }
33314
34349
  });
34350
+ Object.defineProperty(exports, "inspectEncryption", {
34351
+ enumerable: true,
34352
+ get: function() {
34353
+ return inspectEncryption;
34354
+ }
34355
+ });
33315
34356
  Object.defineProperty(exports, "interpretContentStream", {
33316
34357
  enumerable: true,
33317
34358
  get: function() {
@@ -33684,6 +34725,12 @@ Object.defineProperty(exports, "sampleShadingColor", {
33684
34725
  return sampleShadingColor;
33685
34726
  }
33686
34727
  });
34728
+ Object.defineProperty(exports, "sanitizePdf", {
34729
+ enumerable: true,
34730
+ get: function() {
34731
+ return sanitizePdf;
34732
+ }
34733
+ });
33687
34734
  Object.defineProperty(exports, "saveDocumentIncremental", {
33688
34735
  enumerable: true,
33689
34736
  get: function() {
@@ -33702,6 +34749,12 @@ Object.defineProperty(exports, "saveIncrementalWithSignaturePreservation", {
33702
34749
  return saveIncrementalWithSignaturePreservation;
33703
34750
  }
33704
34751
  });
34752
+ Object.defineProperty(exports, "scanPdfThreats", {
34753
+ enumerable: true,
34754
+ get: function() {
34755
+ return scanPdfThreats;
34756
+ }
34757
+ });
33705
34758
  Object.defineProperty(exports, "searchTextItems", {
33706
34759
  enumerable: true,
33707
34760
  get: function() {
@@ -33978,6 +35031,12 @@ Object.defineProperty(exports, "verifyOfflineRevocation", {
33978
35031
  return verifyOfflineRevocation;
33979
35032
  }
33980
35033
  });
35034
+ Object.defineProperty(exports, "verifyRedactions", {
35035
+ enumerable: true,
35036
+ get: function() {
35037
+ return verifyRedactions;
35038
+ }
35039
+ });
33981
35040
  Object.defineProperty(exports, "verifySignatureDetailed", {
33982
35041
  enumerable: true,
33983
35042
  get: function() {
@@ -34003,4 +35062,4 @@ Object.defineProperty(exports, "wrapText", {
34003
35062
  }
34004
35063
  });
34005
35064
 
34006
- //# sourceMappingURL=src-PxZwqIKQ.cjs.map
35065
+ //# sourceMappingURL=src-C-mBizNU.cjs.map