modern-pdf-lib 0.14.0 → 0.14.1

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/dist/index.mjs CHANGED
@@ -6306,15 +6306,76 @@ function padPassword(password) {
6306
6306
  return result;
6307
6307
  }
6308
6308
  /**
6309
- * Truncate a UTF-8 password to 127 bytes for R=5/R=6 (SASLprep would
6310
- * be ideal but we approximate with simple truncation as most PDF
6311
- * libraries do).
6309
+ * Prepare a password for R=5/R=6 (AES-256) using SASLprep (RFC 4013).
6310
+ *
6311
+ * ISO 32000-2 SS7.6.3.1 requires SASLprep normalization for passwords
6312
+ * used with encryption revision 5 and 6. The steps are:
6313
+ *
6314
+ * 1. Map: Convert non-ASCII space characters to U+0020, remove
6315
+ * "commonly mapped to nothing" characters (RFC 3454 B.1).
6316
+ * 2. Normalize: Apply Unicode NFKC normalization.
6317
+ * 3. Prohibit: Reject characters from RFC 3454 prohibited tables.
6318
+ * 4. Bidi: Check bidirectional string rules.
6319
+ *
6320
+ * The result is truncated to 127 UTF-8 bytes per the PDF spec.
6312
6321
  */
6313
6322
  function preparePasswordV5(password) {
6314
- const encoded = new TextEncoder().encode(password);
6323
+ const prepared = saslprep(password);
6324
+ const encoded = new TextEncoder().encode(prepared);
6315
6325
  return encoded.length > 127 ? encoded.subarray(0, 127) : encoded;
6316
6326
  }
6317
6327
  /**
6328
+ * Simplified SASLprep (RFC 4013) profile of stringprep (RFC 3454).
6329
+ *
6330
+ * Covers the mapping, normalization, and prohibited-character steps
6331
+ * needed for PDF password preparation. Bidi checking is omitted
6332
+ * since PDF passwords are typically LTR and the spec allows
6333
+ * implementations to skip it.
6334
+ *
6335
+ * @internal
6336
+ */
6337
+ function saslprep(input) {
6338
+ let mapped = "";
6339
+ for (const ch of input) {
6340
+ const cp = ch.codePointAt(0);
6341
+ if (isMappedToNothing(cp)) continue;
6342
+ if (isNonAsciiSpace(cp)) {
6343
+ mapped += " ";
6344
+ continue;
6345
+ }
6346
+ mapped += ch;
6347
+ }
6348
+ const normalized = mapped.normalize("NFKC");
6349
+ for (const ch of normalized) {
6350
+ const cp = ch.codePointAt(0);
6351
+ if (isProhibited(cp)) throw new Error(`Password contains prohibited character U+${cp.toString(16).toUpperCase().padStart(4, "0")} (SASLprep)`);
6352
+ }
6353
+ return normalized;
6354
+ }
6355
+ /** RFC 3454 Table B.1 — Commonly mapped to nothing. */
6356
+ function isMappedToNothing(cp) {
6357
+ return cp === 173 || cp === 6150 || cp === 8203 || cp === 8288 || cp === 65279 || cp === 847 || cp === 6155 || cp === 6156 || cp === 6157 || cp === 8204 || cp === 8205 || cp >= 65024 && cp <= 65039;
6358
+ }
6359
+ /** RFC 3454 Table C.1.2 — Non-ASCII space characters. */
6360
+ function isNonAsciiSpace(cp) {
6361
+ return cp === 160 || cp === 5760 || cp === 8192 || cp === 8193 || cp === 8194 || cp === 8195 || cp === 8196 || cp === 8197 || cp === 8198 || cp === 8199 || cp === 8200 || cp === 8201 || cp === 8202 || cp === 8239 || cp === 8287 || cp === 12288;
6362
+ }
6363
+ /**
6364
+ * RFC 3454 prohibited tables (C.2.1, C.2.2, C.3, C.4, C.5, C.6, C.7, C.8, C.9).
6365
+ * Simplified to the ranges most likely to appear in passwords.
6366
+ */
6367
+ function isProhibited(cp) {
6368
+ if (cp <= 31 || cp === 127) return true;
6369
+ if (cp >= 128 && cp <= 159 || cp === 1757 || cp === 1807 || cp === 6158 || cp >= 8204 && cp <= 8205 || cp >= 8232 && cp <= 8233 || cp >= 8288 && cp <= 8291 || cp >= 8298 && cp <= 8303 || cp === 65279 || cp >= 65529 && cp <= 65532 || cp >= 119155 && cp <= 119162) return true;
6370
+ if (cp >= 57344 && cp <= 63743 || cp >= 983040 && cp <= 1048573 || cp >= 1048576 && cp <= 1114109) return true;
6371
+ if (cp >= 64976 && cp <= 65007 || (cp & 65535) === 65534 || (cp & 65535) === 65535) return true;
6372
+ if (cp >= 55296 && cp <= 57343) return true;
6373
+ if (cp === 65529 || cp === 65530 || cp === 65531 || cp === 65532) return true;
6374
+ if (cp === 832 || cp === 833 || cp === 8206 || cp === 8207 || cp >= 8234 && cp <= 8238 || cp >= 8298 && cp <= 8303) return true;
6375
+ if (cp === 917505 || cp >= 917536 && cp <= 917631) return true;
6376
+ return false;
6377
+ }
6378
+ /**
6318
6379
  * Concatenate multiple Uint8Arrays into one.
6319
6380
  */
6320
6381
  function concat$1(...arrays) {
@@ -6593,11 +6654,33 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
6593
6654
  }
6594
6655
  }
6595
6656
  /**
6657
+ * LRU cache for file encryption keys.
6658
+ *
6659
+ * Keyed on a hash derived from password + encryption dict parameters,
6660
+ * so re-opening the same PDF with the same password skips the expensive
6661
+ * key derivation (especially for R=6 which runs 64+ rounds of AES+SHA).
6662
+ */
6663
+ const fileKeyCache = /* @__PURE__ */ new Map();
6664
+ const FILE_KEY_CACHE_MAX = 32;
6665
+ /**
6666
+ * Build a cache key string from the inputs that uniquely identify
6667
+ * a key derivation result.
6668
+ */
6669
+ function buildCacheKey(password, dict, fileId) {
6670
+ const oHex = Array.from(dict.ownerKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6671
+ const uHex = Array.from(dict.userKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6672
+ const fHex = Array.from(fileId.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6673
+ return `${dict.revision}:${dict.permissions}:${password}:${oHex}:${uHex}:${fHex}`;
6674
+ }
6675
+ /**
6596
6676
  * Compute the file encryption key from a password and encryption dict.
6597
6677
  *
6598
6678
  * Tries the password as both user and owner password. Returns the key
6599
6679
  * on the first successful match, or throws if neither works.
6600
6680
  *
6681
+ * Results are cached so that re-opening the same PDF with the same
6682
+ * password skips the expensive key derivation.
6683
+ *
6601
6684
  * @param password The password to try.
6602
6685
  * @param dict Encryption dictionary values.
6603
6686
  * @param fileId The first element of the /ID array (unused for R>=5).
@@ -6605,23 +6688,35 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
6605
6688
  * @throws If the password is incorrect.
6606
6689
  */
6607
6690
  async function computeFileEncryptionKey(password, dict, fileId) {
6691
+ const ck = buildCacheKey(password, dict, fileId);
6692
+ const cached = fileKeyCache.get(ck);
6693
+ if (cached) return cached.slice();
6694
+ let result;
6608
6695
  if (dict.revision >= 6) {
6609
6696
  const userKey = await computeEncryptionKeyR6(password, dict, false);
6610
- if (userKey) return userKey;
6611
- const ownerKey = await computeEncryptionKeyR6(password, dict, true);
6612
- if (ownerKey) return ownerKey;
6613
- throw new Error("Incorrect password for R=6 encryption");
6614
- }
6615
- if (dict.revision === 5) {
6697
+ if (userKey) result = userKey;
6698
+ else {
6699
+ const ownerKey = await computeEncryptionKeyR6(password, dict, true);
6700
+ if (ownerKey) result = ownerKey;
6701
+ else throw new Error("Incorrect password for R=6 encryption");
6702
+ }
6703
+ } else if (dict.revision === 5) {
6616
6704
  const userKey = await computeEncryptionKeyR5(password, dict, false);
6617
- if (userKey) return userKey;
6618
- const ownerKey = await computeEncryptionKeyR5(password, dict, true);
6619
- if (ownerKey) return ownerKey;
6620
- throw new Error("Incorrect password for R=5 encryption");
6621
- }
6622
- if (await verifyUserPassword(password, dict, fileId)) return computeEncryptionKeyR2R4(password, dict, fileId);
6623
- if (await verifyOwnerPassword(password, dict, fileId)) return computeEncryptionKeyR2R4Bytes(recoverUserKeyFromOwner(password, dict), dict, fileId);
6624
- throw new Error("Incorrect password");
6705
+ if (userKey) result = userKey;
6706
+ else {
6707
+ const ownerKey = await computeEncryptionKeyR5(password, dict, true);
6708
+ if (ownerKey) result = ownerKey;
6709
+ else throw new Error("Incorrect password for R=5 encryption");
6710
+ }
6711
+ } else if (await verifyUserPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4(password, dict, fileId);
6712
+ else if (await verifyOwnerPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4Bytes(recoverUserKeyFromOwner(password, dict), dict, fileId);
6713
+ else throw new Error("Incorrect password");
6714
+ if (fileKeyCache.size >= FILE_KEY_CACHE_MAX) {
6715
+ const firstKey = fileKeyCache.keys().next().value;
6716
+ if (firstKey !== void 0) fileKeyCache.delete(firstKey);
6717
+ }
6718
+ fileKeyCache.set(ck, result.slice());
6719
+ return result;
6625
6720
  }
6626
6721
  /**
6627
6722
  * Compute encryption key from raw password bytes (already padded/recovered).
@@ -6884,6 +6979,15 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
6884
6979
  perms;
6885
6980
  /** The file ID (first element of /ID array). */
6886
6981
  fileId;
6982
+ /**
6983
+ * Cache for per-object derived keys (V=1-4 only).
6984
+ * Key: `(objNum << 16) | genNum` — unique integer per object.
6985
+ * Value: the derived encryption key.
6986
+ *
6987
+ * Avoids recomputing MD5(fileKey + objNum + genNum [+ sAlT]) for
6988
+ * every string and stream in the same object.
6989
+ */
6990
+ objectKeyCache = /* @__PURE__ */ new Map();
6887
6991
  constructor(params) {
6888
6992
  this.fileKey = params.fileKey;
6889
6993
  this.version = params.version;
@@ -7036,6 +7140,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
7036
7140
  */
7037
7141
  deriveObjectKey(objNum, genNum) {
7038
7142
  if (this.version === 5) return this.fileKey;
7143
+ const cacheKey = objNum << 16 | genNum;
7144
+ const cached = this.objectKeyCache.get(cacheKey);
7145
+ if (cached) return cached;
7039
7146
  const extra = this.useAes ? 4 : 0;
7040
7147
  const input = new Uint8Array(this.fileKey.length + 5 + extra);
7041
7148
  input.set(this.fileKey, 0);
@@ -7053,7 +7160,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
7053
7160
  }
7054
7161
  const hash = md5(input);
7055
7162
  const keyLen = Math.min(this.keyLengthBits / 8 + 5, 16);
7056
- return hash.subarray(0, keyLen);
7163
+ const key = hash.subarray(0, keyLen);
7164
+ this.objectKeyCache.set(cacheKey, key);
7165
+ return key;
7057
7166
  }
7058
7167
  /**
7059
7168
  * Encrypt raw data for a specific object.
@@ -10215,23 +10324,73 @@ function findStringForward(data, needle, startOffset) {
10215
10324
  return -1;
10216
10325
  }
10217
10326
  /**
10218
- * Prepare a PDF for signing by appending a signature dictionary
10219
- * via incremental update.
10327
+ * Build a PDF content stream for a visible signature appearance.
10220
10328
  *
10221
- * This function:
10222
- * 1. Appends a new signature field and value to the PDF
10223
- * 2. Inserts an empty `/Contents` placeholder of the specified size
10224
- * 3. Computes the `/ByteRange` that excludes the `/Contents` value
10329
+ * Renders a bordered rectangle with optional background color and
10330
+ * text lines rendered in Helvetica.
10225
10331
  *
10226
- * The resulting PDF bytes can be hashed (excluding the placeholder gap)
10227
- * and the hash can be signed.
10228
- *
10229
- * @param pdfBytes The original PDF file bytes.
10230
- * @param signatureFieldName The name for the signature field.
10231
- * @param placeholderSize Size in bytes for the signature. Default 8192.
10232
- * @returns The prepared PDF and ByteRange info.
10332
+ * @internal
10333
+ */
10334
+ function buildSignatureAppearanceStream(options) {
10335
+ const [, , w, h] = options.rect;
10336
+ const fontSize = options.fontSize ?? 10;
10337
+ const borderWidth = options.borderWidth ?? 1;
10338
+ const borderColor = options.borderColor ?? [
10339
+ 0,
10340
+ 0,
10341
+ 0
10342
+ ];
10343
+ const bgColor = options.backgroundColor;
10344
+ const lines = options.textLines;
10345
+ const ops = [];
10346
+ ops.push("q");
10347
+ if (bgColor) {
10348
+ ops.push(`${n$5(bgColor[0])} ${n$5(bgColor[1])} ${n$5(bgColor[2])} rg`);
10349
+ ops.push(`0 0 ${n$5(w)} ${n$5(h)} re`);
10350
+ ops.push("f");
10351
+ }
10352
+ if (borderWidth > 0) {
10353
+ ops.push(`${n$5(borderColor[0])} ${n$5(borderColor[1])} ${n$5(borderColor[2])} RG`);
10354
+ ops.push(`${n$5(borderWidth)} w`);
10355
+ const bw2 = borderWidth / 2;
10356
+ ops.push(`${n$5(bw2)} ${n$5(bw2)} ${n$5(w - borderWidth)} ${n$5(h - borderWidth)} re`);
10357
+ ops.push("S");
10358
+ }
10359
+ if (lines.length > 0) {
10360
+ const margin = borderWidth + 4;
10361
+ const lineHeight = fontSize * 1.2;
10362
+ ops.push("BT");
10363
+ ops.push(`/F1 ${n$5(fontSize)} Tf`);
10364
+ ops.push("0 0 0 rg");
10365
+ const startY = h - margin - fontSize;
10366
+ for (let i = 0; i < lines.length; i++) {
10367
+ const y = startY - i * lineHeight;
10368
+ if (y < margin) break;
10369
+ ops.push(`${n$5(margin)} ${n$5(y)} Td`);
10370
+ ops.push(`(${escapePdfString(lines[i])}) Tj`);
10371
+ ops.push(`${n$5(-margin)} ${n$5(-y)} Td`);
10372
+ }
10373
+ ops.push("ET");
10374
+ }
10375
+ ops.push("Q");
10376
+ return ops.join("\n");
10377
+ }
10378
+ /**
10379
+ * Escape a string for use inside a PDF literal string `(...)`.
10380
+ * @internal
10381
+ */
10382
+ function escapePdfString(str) {
10383
+ return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
10384
+ }
10385
+ /**
10386
+ * Format a number for PDF operators (max 6 decimal places, no trailing zeros).
10387
+ * @internal
10233
10388
  */
10234
- function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192) {
10389
+ function n$5(value) {
10390
+ if (Number.isInteger(value)) return value.toString();
10391
+ return value.toFixed(6).replace(/\.?0+$/, "");
10392
+ }
10393
+ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192, appearance) {
10235
10394
  const pdfStr = decoder$4.decode(pdfBytes);
10236
10395
  const startxrefIdx = pdfStr.lastIndexOf("startxref");
10237
10396
  if (startxrefIdx === -1) throw new Error("Cannot find startxref in PDF — file may be corrupted");
@@ -10248,9 +10407,21 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
10248
10407
  const infoMatch = pdfStr.match(/\/Info\s+(\d+)\s+(\d+)\s+R/);
10249
10408
  const sigValueObjNum = originalSize;
10250
10409
  const sigFieldObjNum = originalSize + 1;
10251
- const newSize = originalSize + 2;
10410
+ let apStreamObjNum = -1;
10411
+ let newSize = originalSize + 2;
10412
+ if (appearance) {
10413
+ apStreamObjNum = newSize;
10414
+ newSize++;
10415
+ }
10252
10416
  const sigDictStr = buildSignatureDictString(placeholderSize, signatureFieldName);
10253
- const sigFieldDict = `<< /Type /Annot /Subtype /Widget /FT /Sig /T (${signatureFieldName}) /V ${sigValueObjNum} 0 R /F 132 /Rect [0 0 0 0] >>`;
10417
+ let rectStr = "0 0 0 0";
10418
+ if (appearance) {
10419
+ const [x, y, w, h] = appearance.rect;
10420
+ rectStr = `${x} ${y} ${x + w} ${y + h}`;
10421
+ }
10422
+ let sigFieldDict = `<< /Type /Annot /Subtype /Widget /FT /Sig /T (${signatureFieldName}) /V ${sigValueObjNum} 0 R /F 132 /Rect [${rectStr}]`;
10423
+ if (appearance && apStreamObjNum >= 0) sigFieldDict += ` /AP << /N ${apStreamObjNum} 0 R >>`;
10424
+ sigFieldDict += " >>";
10254
10425
  let appendix = "\n";
10255
10426
  const objOffsets = /* @__PURE__ */ new Map();
10256
10427
  const sigValueStart = pdfBytes.length + encoder$3.encode(appendix).length;
@@ -10263,11 +10434,28 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
10263
10434
  appendix += `${sigFieldObjNum} 0 obj\n`;
10264
10435
  appendix += sigFieldDict;
10265
10436
  appendix += `\nendobj\n`;
10437
+ let apStreamStart = -1;
10438
+ if (appearance && apStreamObjNum >= 0) {
10439
+ apStreamStart = pdfBytes.length + encoder$3.encode(appendix).length;
10440
+ objOffsets.set(apStreamObjNum, apStreamStart);
10441
+ const apContent = buildSignatureAppearanceStream(appearance);
10442
+ const [, , w, h] = appearance.rect;
10443
+ appendix += `${apStreamObjNum} 0 obj\n`;
10444
+ appendix += `<< /Type /XObject /Subtype /Form /BBox [0 0 ${w} ${h}]`;
10445
+ appendix += ` /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>`;
10446
+ appendix += ` /Length ${apContent.length} >>\n`;
10447
+ appendix += `stream\n`;
10448
+ appendix += apContent;
10449
+ appendix += `\nendstream\n`;
10450
+ appendix += `endobj\n`;
10451
+ }
10266
10452
  const xrefOffset = pdfBytes.length + encoder$3.encode(appendix).length;
10453
+ const objCount = appearance ? 3 : 2;
10267
10454
  appendix += "xref\n";
10268
- appendix += `${sigValueObjNum} 2\n`;
10455
+ appendix += `${sigValueObjNum} ${objCount}\n`;
10269
10456
  appendix += `${sigValueStart.toString().padStart(10, "0")} 00000 n \n`;
10270
10457
  appendix += `${sigFieldStart.toString().padStart(10, "0")} 00000 n \n`;
10458
+ if (appearance && apStreamStart >= 0) appendix += `${apStreamStart.toString().padStart(10, "0")} 00000 n \n`;
10271
10459
  appendix += "trailer\n";
10272
10460
  appendix += "<<\n";
10273
10461
  appendix += `/Size ${newSize}\n`;
@@ -11057,7 +11245,32 @@ function parsePkcs7ForCert(pkcs7Bytes) {
11057
11245
  */
11058
11246
  async function signPdf(pdfBytes, fieldName, options) {
11059
11247
  const hashAlgorithm = options.hashAlgorithm ?? "SHA-256";
11060
- const { preparedPdf, byteRange } = prepareForSigning(pdfBytes, fieldName, 8192);
11248
+ let prepareAppearance;
11249
+ if (options.appearance) {
11250
+ const ap = options.appearance;
11251
+ let textLines = ap.text;
11252
+ if (!textLines) {
11253
+ textLines = [];
11254
+ try {
11255
+ const { subjectCN } = extractIssuerAndSerial(options.certificate);
11256
+ if (subjectCN) textLines.push(`Signed by: ${subjectCN}`);
11257
+ } catch {
11258
+ textLines.push("Digitally Signed");
11259
+ }
11260
+ if (options.reason) textLines.push(`Reason: ${options.reason}`);
11261
+ if (options.location) textLines.push(`Location: ${options.location}`);
11262
+ textLines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString().substring(0, 10)}`);
11263
+ }
11264
+ prepareAppearance = {
11265
+ rect: ap.rect,
11266
+ textLines,
11267
+ fontSize: ap.fontSize,
11268
+ backgroundColor: ap.backgroundColor,
11269
+ borderColor: ap.borderColor,
11270
+ borderWidth: ap.borderWidth
11271
+ };
11272
+ }
11273
+ const { preparedPdf, byteRange } = prepareForSigning(pdfBytes, fieldName, 8192, prepareAppearance);
11061
11274
  return embedSignature(preparedPdf, await buildPkcs7Signature(await computeSignatureHash(preparedPdf, byteRange.byteRange, hashAlgorithm), {
11062
11275
  signerInfo: {
11063
11276
  certificate: options.certificate,
@@ -18869,6 +19082,30 @@ var PdfDocument = class PdfDocument {
18869
19082
  return imageRef;
18870
19083
  }
18871
19084
  /**
19085
+ * Embed an image, auto-detecting the format from file headers.
19086
+ *
19087
+ * Inspects the first bytes to determine whether the data is PNG or JPEG,
19088
+ * then delegates to {@link embedPng} or {@link embedJpeg} accordingly.
19089
+ *
19090
+ * @param imageData Raw image file bytes (PNG or JPEG).
19091
+ * @returns An {@link ImageRef} to pass to `page.drawImage()`.
19092
+ * @throws If the image format cannot be detected.
19093
+ *
19094
+ * @example
19095
+ * ```ts
19096
+ * const bytes = new Uint8Array(await readFile('photo.jpg'));
19097
+ * const image = await pdf.embedImage(bytes);
19098
+ * page.drawImage(image, { x: 50, y: 400, width: 200, height: 150 });
19099
+ * ```
19100
+ */
19101
+ async embedImage(imageData) {
19102
+ const data = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : imageData;
19103
+ if (data.length < 4) throw new Error("Image data too short to detect format");
19104
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return this.embedPng(data);
19105
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return this.embedJpeg(data);
19106
+ throw new Error(`Unsupported image format. Expected PNG (89 50 4E 47) or JPEG (FF D8 FF), got ${Array.from(data.subarray(0, 4)).map((b) => b.toString(16).padStart(2, "0")).join(" ")}.`);
19107
+ }
19108
+ /**
18872
19109
  * Embed pages from another PDF as Form XObjects.
18873
19110
  *
18874
19111
  * Each embedded page is turned into a self-contained Form XObject that
@@ -21769,6 +22006,261 @@ var PdfRedactAnnotation = class PdfRedactAnnotation extends PdfAnnotation {
21769
22006
  }
21770
22007
  };
21771
22008
 
22009
+ //#endregion
22010
+ //#region src/annotation/types/popupAnnotation.ts
22011
+ /**
22012
+ * @module annotation/types/popupAnnotation
22013
+ *
22014
+ * Popup annotation — a floating window that displays the text of its
22015
+ * parent annotation (typically a text/sticky note annotation).
22016
+ *
22017
+ * Popup annotations have no appearance of their own; the PDF viewer
22018
+ * renders them as a resizable window near the parent annotation.
22019
+ *
22020
+ * Reference: PDF 1.7 spec, Section 12.5.6.14 (Popup Annotations).
22021
+ */
22022
+ /**
22023
+ * A popup annotation (subtype /Popup).
22024
+ *
22025
+ * Displays a floating window containing the text of its parent
22026
+ * annotation. The parent annotation references this popup via its
22027
+ * `/Popup` entry, and this popup references its parent via `/Parent`.
22028
+ */
22029
+ var PdfPopupAnnotation = class PdfPopupAnnotation extends PdfAnnotation {
22030
+ constructor(dict) {
22031
+ super("Popup", dict);
22032
+ }
22033
+ /**
22034
+ * Create a new popup annotation.
22035
+ *
22036
+ * @param options.open Whether the popup is initially open. Default: false.
22037
+ * @param options.parent Reference to the parent annotation (set after registration).
22038
+ */
22039
+ static create(options) {
22040
+ const annot = new PdfPopupAnnotation(buildAnnotationDict("Popup", options));
22041
+ if (options.open !== void 0) annot.setOpen(options.open);
22042
+ return annot;
22043
+ }
22044
+ /**
22045
+ * Create a PdfPopupAnnotation from an existing dictionary.
22046
+ */
22047
+ static fromDict(dict, _resolver) {
22048
+ return new PdfPopupAnnotation(dict);
22049
+ }
22050
+ /** Whether the popup is initially open. */
22051
+ isOpen() {
22052
+ const obj = this.dict.get("/Open");
22053
+ if (obj && obj.kind === "bool") return obj.value;
22054
+ return false;
22055
+ }
22056
+ /** Set the initial open state. */
22057
+ setOpen(open) {
22058
+ this.dict.set("/Open", PdfBool.of(open));
22059
+ }
22060
+ /**
22061
+ * Set the parent annotation reference.
22062
+ * The parent is the annotation whose text this popup displays.
22063
+ */
22064
+ setParent(parentRef) {
22065
+ this.dict.set("/Parent", parentRef);
22066
+ }
22067
+ /** Get the parent annotation reference, if set. */
22068
+ getParent() {
22069
+ const obj = this.dict.get("/Parent");
22070
+ if (obj && obj.kind === "ref") return obj;
22071
+ }
22072
+ };
22073
+
22074
+ //#endregion
22075
+ //#region src/annotation/types/caretAnnotation.ts
22076
+ /**
22077
+ * @module annotation/types/caretAnnotation
22078
+ *
22079
+ * Caret annotation — marks a text insertion point in the document.
22080
+ *
22081
+ * A caret annotation indicates where text should be inserted. It is
22082
+ * typically used in document review workflows to suggest additions.
22083
+ * The annotation renders as a caret (^) symbol at the specified
22084
+ * location.
22085
+ *
22086
+ * Reference: PDF 1.7 spec, Section 12.5.6.11 (Caret Annotations).
22087
+ */
22088
+ /**
22089
+ * A caret annotation (subtype /Caret).
22090
+ *
22091
+ * Marks an insertion point in the text. Used in review workflows
22092
+ * to indicate where new content should be added.
22093
+ */
22094
+ var PdfCaretAnnotation = class PdfCaretAnnotation extends PdfAnnotation {
22095
+ constructor(dict) {
22096
+ super("Caret", dict);
22097
+ }
22098
+ /**
22099
+ * Create a new caret annotation.
22100
+ *
22101
+ * @param options.symbol The caret symbol. Default: 'None'.
22102
+ * @param options.caretRect The inner rectangle (RD) that describes
22103
+ * the difference between the annotation rect and the actual caret
22104
+ * position. Format: [left, bottom, right, top] insets.
22105
+ */
22106
+ static create(options) {
22107
+ const annot = new PdfCaretAnnotation(buildAnnotationDict("Caret", options));
22108
+ if (options.symbol !== void 0) annot.setSymbol(options.symbol);
22109
+ if (options.caretRect !== void 0) annot.setCaretRect(options.caretRect);
22110
+ return annot;
22111
+ }
22112
+ /**
22113
+ * Create a PdfCaretAnnotation from an existing dictionary.
22114
+ */
22115
+ static fromDict(dict, _resolver) {
22116
+ return new PdfCaretAnnotation(dict);
22117
+ }
22118
+ /** Get the caret symbol. Defaults to 'None'. */
22119
+ getSymbol() {
22120
+ const obj = this.dict.get("/Sy");
22121
+ if (obj && obj.kind === "name") {
22122
+ if ((obj.value.startsWith("/") ? obj.value.slice(1) : obj.value) === "P") return "P";
22123
+ }
22124
+ return "None";
22125
+ }
22126
+ /** Set the caret symbol. */
22127
+ setSymbol(symbol) {
22128
+ this.dict.set("/Sy", PdfName.of(symbol));
22129
+ }
22130
+ /**
22131
+ * Get the inner rectangle differences (RD entry).
22132
+ * Returns [left, bottom, right, top] insets from the annotation rect.
22133
+ */
22134
+ getCaretRect() {
22135
+ const obj = this.dict.get("/RD");
22136
+ if (obj && obj.kind === "array" && obj.items.length === 4) return obj.items.map((item) => {
22137
+ if (item.kind === "number") return item.value;
22138
+ return 0;
22139
+ });
22140
+ }
22141
+ /** Set the inner rectangle differences (RD entry). */
22142
+ setCaretRect(rd) {
22143
+ this.dict.set("/RD", PdfArray.of(rd.map(PdfNumber.of)));
22144
+ }
22145
+ };
22146
+
22147
+ //#endregion
22148
+ //#region src/annotation/types/fileAttachmentAnnotation.ts
22149
+ /**
22150
+ * @module annotation/types/fileAttachmentAnnotation
22151
+ *
22152
+ * File attachment annotation — embeds a file as an inline annotation
22153
+ * on a page, displayed as a clickable icon.
22154
+ *
22155
+ * Unlike document-level attachments (via `attachFile()`), file attachment
22156
+ * annotations are positioned on a specific page and rendered as an icon
22157
+ * that users can click to open or save the embedded file.
22158
+ *
22159
+ * Reference: PDF 1.7 spec, Section 12.5.6.15 (File Attachment Annotations).
22160
+ */
22161
+ /**
22162
+ * A file attachment annotation (subtype /FileAttachment).
22163
+ *
22164
+ * Embeds a file directly in the annotation, rendered as a clickable
22165
+ * icon on the page. When the user clicks the icon, the PDF viewer
22166
+ * allows them to open or save the embedded file.
22167
+ */
22168
+ var PdfFileAttachmentAnnotation = class PdfFileAttachmentAnnotation extends PdfAnnotation {
22169
+ /** The raw file data to embed. */
22170
+ fileData;
22171
+ /** The filename to display. */
22172
+ fileName;
22173
+ /** Optional MIME type. */
22174
+ mimeType;
22175
+ /** Optional file description. */
22176
+ fileDescription;
22177
+ constructor(dict) {
22178
+ super("FileAttachment", dict);
22179
+ }
22180
+ /**
22181
+ * Create a new file attachment annotation.
22182
+ *
22183
+ * @param options.file The file data to embed.
22184
+ * @param options.fileName The filename (e.g., 'invoice.xml').
22185
+ * @param options.mimeType Optional MIME type (e.g., 'application/xml').
22186
+ * @param options.description Optional description of the file.
22187
+ * @param options.icon Icon to display. Default: 'GraphPushPin'.
22188
+ */
22189
+ static create(options) {
22190
+ const annot = new PdfFileAttachmentAnnotation(buildAnnotationDict("FileAttachment", options));
22191
+ annot.fileData = options.file;
22192
+ annot.fileName = options.fileName;
22193
+ annot.mimeType = options.mimeType;
22194
+ annot.fileDescription = options.description;
22195
+ if (options.icon !== void 0) annot.setIcon(options.icon);
22196
+ return annot;
22197
+ }
22198
+ /**
22199
+ * Create a PdfFileAttachmentAnnotation from an existing dictionary.
22200
+ */
22201
+ static fromDict(dict, _resolver) {
22202
+ return new PdfFileAttachmentAnnotation(dict);
22203
+ }
22204
+ /** Get the icon name. Defaults to 'GraphPushPin'. */
22205
+ getIcon() {
22206
+ const obj = this.dict.get("/Name");
22207
+ if (obj && obj.kind === "name") {
22208
+ const val = obj.value.startsWith("/") ? obj.value.slice(1) : obj.value;
22209
+ if ([
22210
+ "GraphPushPin",
22211
+ "PaperclipTag",
22212
+ "Paperclip",
22213
+ "Tag"
22214
+ ].includes(val)) return val;
22215
+ }
22216
+ return "GraphPushPin";
22217
+ }
22218
+ /** Set the icon name. */
22219
+ setIcon(icon) {
22220
+ this.dict.set("/Name", PdfName.of(icon));
22221
+ }
22222
+ /** Get the filename, if set. */
22223
+ getFileName() {
22224
+ const fs = this.dict.get("/FS");
22225
+ if (fs && fs.kind === "dict") {
22226
+ const uf = fs.get("/UF");
22227
+ if (uf && uf.kind === "string") return uf.value;
22228
+ const f = fs.get("/F");
22229
+ if (f && f.kind === "string") return f.value;
22230
+ }
22231
+ return this.fileName;
22232
+ }
22233
+ /**
22234
+ * Build the file specification dictionary and register the embedded
22235
+ * file stream. Call this before serializing the annotation.
22236
+ *
22237
+ * @param registry The document's object registry.
22238
+ * @returns The annotation dict with `/FS` referencing the file.
22239
+ */
22240
+ buildFileSpec(registry) {
22241
+ if (!this.fileData || !this.fileName) return this.dict;
22242
+ const efStreamDict = new PdfDict();
22243
+ efStreamDict.set("/Type", PdfName.of("EmbeddedFile"));
22244
+ if (this.mimeType) efStreamDict.set("/Subtype", PdfName.of(this.mimeType.replace("/", "#2F")));
22245
+ const params = new PdfDict();
22246
+ params.set("/Size", PdfNumber.of(this.fileData.length));
22247
+ efStreamDict.set("/Params", params);
22248
+ const efStream = new PdfStream(efStreamDict, this.fileData);
22249
+ const efRef = registry.register(efStream);
22250
+ const efDict = new PdfDict();
22251
+ efDict.set("/F", efRef);
22252
+ const fsDict = new PdfDict();
22253
+ fsDict.set("/Type", PdfName.of("Filespec"));
22254
+ fsDict.set("/F", PdfString.literal(this.fileName));
22255
+ fsDict.set("/UF", PdfString.literal(this.fileName));
22256
+ fsDict.set("/EF", efDict);
22257
+ if (this.fileDescription) fsDict.set("/Desc", PdfString.literal(this.fileDescription));
22258
+ const fsRef = registry.register(fsDict);
22259
+ this.dict.set("/FS", fsRef);
22260
+ return this.dict;
22261
+ }
22262
+ };
22263
+
21772
22264
  //#endregion
21773
22265
  //#region src/parser/textExtractor.ts
21774
22266
  /**
@@ -23852,6 +24344,339 @@ function addTrailerId(data) {
23852
24344
  return concatBytes(...parts);
23853
24345
  }
23854
24346
 
24347
+ //#endregion
24348
+ //#region src/assets/image/imageOptimize.ts
24349
+ /**
24350
+ * Downscale an image to fit within the specified dimensions.
24351
+ *
24352
+ * If the image is already smaller than the target dimensions, it is
24353
+ * returned unchanged.
24354
+ *
24355
+ * @param image - The raw image pixel data.
24356
+ * @param options - Downscaling options (target dimensions, algorithm).
24357
+ * @returns The downscaled image, or the original if no scaling needed.
24358
+ *
24359
+ * @example
24360
+ * ```ts
24361
+ * const result = downscaleImage(rawImage, {
24362
+ * maxWidth: 1024,
24363
+ * maxHeight: 768,
24364
+ * algorithm: 'bilinear',
24365
+ * });
24366
+ * ```
24367
+ */
24368
+ function downscaleImage(image, options = {}) {
24369
+ const target = computeTargetDimensions(image.width, image.height, options);
24370
+ if (target.width >= image.width && target.height >= image.height) return image;
24371
+ switch (options.algorithm ?? "bilinear") {
24372
+ case "nearest": return resampleNearest(image, target.width, target.height);
24373
+ case "bilinear": return resampleBilinear(image, target.width, target.height);
24374
+ case "lanczos": return resampleLanczos(image, target.width, target.height);
24375
+ default: return resampleBilinear(image, target.width, target.height);
24376
+ }
24377
+ }
24378
+ /**
24379
+ * Recompress raw image pixel data using the specified format.
24380
+ *
24381
+ * @param image - The raw image pixel data.
24382
+ * @param options - Recompression options (format, quality).
24383
+ * @returns The compressed image data.
24384
+ *
24385
+ * @example
24386
+ * ```ts
24387
+ * const result = await recompressImage(rawImage, {
24388
+ * format: 'deflate',
24389
+ * compressionLevel: 9,
24390
+ * });
24391
+ * ```
24392
+ */
24393
+ async function recompressImage(image, options = {}) {
24394
+ switch (options.format ?? "deflate") {
24395
+ case "deflate": return recompressDeflate(image, options.compressionLevel ?? 6);
24396
+ case "jpeg": return recompressJpeg(image, options.quality ?? 85);
24397
+ default: return {
24398
+ data: image.pixels,
24399
+ width: image.width,
24400
+ height: image.height,
24401
+ channels: image.channels,
24402
+ format: "raw",
24403
+ wasOptimized: false
24404
+ };
24405
+ }
24406
+ }
24407
+ /**
24408
+ * Run the full image optimization pipeline: downscale then recompress.
24409
+ *
24410
+ * @param image - The raw image pixel data.
24411
+ * @param options - Combined optimization options.
24412
+ * @returns The optimized result.
24413
+ */
24414
+ async function optimizeImage(image, options = {}) {
24415
+ if (options.skipBelowBytes && image.pixels.length < options.skipBelowBytes) return {
24416
+ data: image.pixels,
24417
+ width: image.width,
24418
+ height: image.height,
24419
+ channels: image.channels,
24420
+ format: "raw",
24421
+ wasOptimized: false
24422
+ };
24423
+ return recompressImage(downscaleImage(image, options), options);
24424
+ }
24425
+ /**
24426
+ * Compute target dimensions from options, preserving aspect ratio.
24427
+ * @internal
24428
+ */
24429
+ function computeTargetDimensions(srcWidth, srcHeight, options) {
24430
+ let targetWidth = srcWidth;
24431
+ let targetHeight = srcHeight;
24432
+ if (options.targetDpi && options.printWidth && options.printHeight) {
24433
+ const printWidthInches = options.printWidth / 72;
24434
+ const printHeightInches = options.printHeight / 72;
24435
+ const dpiWidth = Math.round(printWidthInches * options.targetDpi);
24436
+ const dpiHeight = Math.round(printHeightInches * options.targetDpi);
24437
+ targetWidth = Math.min(targetWidth, dpiWidth);
24438
+ targetHeight = Math.min(targetHeight, dpiHeight);
24439
+ }
24440
+ if (options.maxWidth && targetWidth > options.maxWidth) {
24441
+ const scale = options.maxWidth / targetWidth;
24442
+ targetWidth = options.maxWidth;
24443
+ targetHeight = Math.round(targetHeight * scale);
24444
+ }
24445
+ if (options.maxHeight && targetHeight > options.maxHeight) {
24446
+ const scale = options.maxHeight / targetHeight;
24447
+ targetHeight = options.maxHeight;
24448
+ targetWidth = Math.round(targetWidth * scale);
24449
+ }
24450
+ targetWidth = Math.max(1, targetWidth);
24451
+ targetHeight = Math.max(1, targetHeight);
24452
+ return {
24453
+ width: targetWidth,
24454
+ height: targetHeight
24455
+ };
24456
+ }
24457
+ /**
24458
+ * Nearest-neighbor resampling.
24459
+ * @internal
24460
+ */
24461
+ function resampleNearest(src, dstWidth, dstHeight) {
24462
+ const channels = src.channels;
24463
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24464
+ const xRatio = src.width / dstWidth;
24465
+ const yRatio = src.height / dstHeight;
24466
+ for (let y = 0; y < dstHeight; y++) {
24467
+ const srcY = Math.min(Math.floor(y * yRatio), src.height - 1);
24468
+ for (let x = 0; x < dstWidth; x++) {
24469
+ const srcX = Math.min(Math.floor(x * xRatio), src.width - 1);
24470
+ const srcIdx = (srcY * src.width + srcX) * channels;
24471
+ const dstIdx = (y * dstWidth + x) * channels;
24472
+ for (let c = 0; c < channels; c++) dst[dstIdx + c] = src.pixels[srcIdx + c];
24473
+ }
24474
+ }
24475
+ return {
24476
+ pixels: dst,
24477
+ width: dstWidth,
24478
+ height: dstHeight,
24479
+ channels,
24480
+ bitsPerChannel: src.bitsPerChannel
24481
+ };
24482
+ }
24483
+ /**
24484
+ * Bilinear interpolation resampling.
24485
+ * @internal
24486
+ */
24487
+ function resampleBilinear(src, dstWidth, dstHeight) {
24488
+ const channels = src.channels;
24489
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24490
+ const xRatio = (src.width - 1) / Math.max(1, dstWidth - 1);
24491
+ const yRatio = (src.height - 1) / Math.max(1, dstHeight - 1);
24492
+ for (let y = 0; y < dstHeight; y++) {
24493
+ const srcYf = y * yRatio;
24494
+ const srcY0 = Math.floor(srcYf);
24495
+ const srcY1 = Math.min(srcY0 + 1, src.height - 1);
24496
+ const yFrac = srcYf - srcY0;
24497
+ for (let x = 0; x < dstWidth; x++) {
24498
+ const srcXf = x * xRatio;
24499
+ const srcX0 = Math.floor(srcXf);
24500
+ const srcX1 = Math.min(srcX0 + 1, src.width - 1);
24501
+ const xFrac = srcXf - srcX0;
24502
+ const dstIdx = (y * dstWidth + x) * channels;
24503
+ for (let c = 0; c < channels; c++) {
24504
+ const topLeft = src.pixels[(srcY0 * src.width + srcX0) * channels + c];
24505
+ const topRight = src.pixels[(srcY0 * src.width + srcX1) * channels + c];
24506
+ const bottomLeft = src.pixels[(srcY1 * src.width + srcX0) * channels + c];
24507
+ const bottomRight = src.pixels[(srcY1 * src.width + srcX1) * channels + c];
24508
+ const top = topLeft + (topRight - topLeft) * xFrac;
24509
+ const value = top + (bottomLeft + (bottomRight - bottomLeft) * xFrac - top) * yFrac;
24510
+ dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, value)));
24511
+ }
24512
+ }
24513
+ }
24514
+ return {
24515
+ pixels: dst,
24516
+ width: dstWidth,
24517
+ height: dstHeight,
24518
+ channels,
24519
+ bitsPerChannel: src.bitsPerChannel
24520
+ };
24521
+ }
24522
+ /**
24523
+ * Lanczos kernel function.
24524
+ *
24525
+ * Computes the Lanczos windowed sinc value for a given distance `x`
24526
+ * and window size `a`. For Lanczos-3, `a = 3`.
24527
+ *
24528
+ * @param x - Distance from the center sample.
24529
+ * @param a - Window radius (3 for Lanczos-3).
24530
+ * @returns The kernel weight.
24531
+ * @internal
24532
+ */
24533
+ function lanczos(x, a = 3) {
24534
+ if (x === 0) return 1;
24535
+ if (Math.abs(x) >= a) return 0;
24536
+ const pix = Math.PI * x;
24537
+ return Math.sin(pix) / pix * (Math.sin(pix / a) / (pix / a));
24538
+ }
24539
+ /**
24540
+ * Lanczos-3 resampling.
24541
+ *
24542
+ * Uses a 6-tap (a=3) windowed sinc filter in both dimensions for
24543
+ * high-quality downscaling. This is the best quality option but
24544
+ * also the slowest.
24545
+ *
24546
+ * @internal
24547
+ */
24548
+ function resampleLanczos(src, dstWidth, dstHeight) {
24549
+ const channels = src.channels;
24550
+ const a = 3;
24551
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24552
+ const xRatio = src.width / dstWidth;
24553
+ const yRatio = src.height / dstHeight;
24554
+ for (let y = 0; y < dstHeight; y++) {
24555
+ const srcYf = (y + .5) * yRatio - .5;
24556
+ for (let x = 0; x < dstWidth; x++) {
24557
+ const srcXf = (x + .5) * xRatio - .5;
24558
+ const dstIdx = (y * dstWidth + x) * channels;
24559
+ const sum = new Float64Array(channels);
24560
+ let weightSum = 0;
24561
+ const yStart = Math.floor(srcYf) - a + 1;
24562
+ const yEnd = Math.floor(srcYf) + a;
24563
+ const xStart = Math.floor(srcXf) - a + 1;
24564
+ const xEnd = Math.floor(srcXf) + a;
24565
+ for (let sy = yStart; sy <= yEnd; sy++) {
24566
+ const wy = lanczos(srcYf - sy, a);
24567
+ if (wy === 0) continue;
24568
+ const clampedY = Math.max(0, Math.min(src.height - 1, sy));
24569
+ for (let sx = xStart; sx <= xEnd; sx++) {
24570
+ const wx = lanczos(srcXf - sx, a);
24571
+ if (wx === 0) continue;
24572
+ const w = wx * wy;
24573
+ const clampedX = Math.max(0, Math.min(src.width - 1, sx));
24574
+ const srcIdx = (clampedY * src.width + clampedX) * channels;
24575
+ for (let c = 0; c < channels; c++) sum[c] = (sum[c] ?? 0) + src.pixels[srcIdx + c] * w;
24576
+ weightSum += w;
24577
+ }
24578
+ }
24579
+ if (weightSum > 0) for (let c = 0; c < channels; c++) dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, sum[c] / weightSum)));
24580
+ }
24581
+ }
24582
+ return {
24583
+ pixels: dst,
24584
+ width: dstWidth,
24585
+ height: dstHeight,
24586
+ channels,
24587
+ bitsPerChannel: src.bitsPerChannel
24588
+ };
24589
+ }
24590
+ /**
24591
+ * Recompress image data using deflate (for PDF FlateDecode).
24592
+ * @internal
24593
+ */
24594
+ async function recompressDeflate(image, level) {
24595
+ if (typeof CompressionStream !== "undefined") {
24596
+ const cs = new CompressionStream("deflate");
24597
+ const writer = cs.writable.getWriter();
24598
+ const reader = cs.readable.getReader();
24599
+ const chunks = [];
24600
+ writer.write(new Uint8Array(image.pixels)).catch(() => {});
24601
+ writer.close().catch(() => {});
24602
+ while (true) {
24603
+ const { done, value } = await reader.read();
24604
+ if (done) break;
24605
+ chunks.push(value);
24606
+ }
24607
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
24608
+ const result = new Uint8Array(totalLength);
24609
+ let pos = 0;
24610
+ for (const chunk of chunks) {
24611
+ result.set(chunk, pos);
24612
+ pos += chunk.length;
24613
+ }
24614
+ return {
24615
+ data: result,
24616
+ width: image.width,
24617
+ height: image.height,
24618
+ channels: image.channels,
24619
+ format: "deflate",
24620
+ wasOptimized: true
24621
+ };
24622
+ }
24623
+ try {
24624
+ const { deflateSync } = await import("fflate");
24625
+ return {
24626
+ data: deflateSync(image.pixels, { level }),
24627
+ width: image.width,
24628
+ height: image.height,
24629
+ channels: image.channels,
24630
+ format: "deflate",
24631
+ wasOptimized: true
24632
+ };
24633
+ } catch {
24634
+ return {
24635
+ data: image.pixels,
24636
+ width: image.width,
24637
+ height: image.height,
24638
+ channels: image.channels,
24639
+ format: "raw",
24640
+ wasOptimized: false
24641
+ };
24642
+ }
24643
+ }
24644
+ /**
24645
+ * Recompress image data as JPEG.
24646
+ * @internal
24647
+ */
24648
+ /**
24649
+ * Recompress image data as JPEG.
24650
+ *
24651
+ * JPEG encoding in pure JS is complex (DCT, Huffman coding, quantization).
24652
+ * A full implementation requires either:
24653
+ *
24654
+ * 1. **WASM-based encoder** (preferred) -- compile libjpeg-turbo or mozjpeg
24655
+ * to WASM, feed raw pixels, get JPEG bytes back.
24656
+ * 2. **Canvas API** (browser-only fallback) -- use `OffscreenCanvas` with
24657
+ * `convertToBlob({ type: 'image/jpeg', quality })`.
24658
+ * 3. **Pure JS encoder** (last resort) -- very slow but works everywhere.
24659
+ *
24660
+ * Until a WASM encoder module is bundled, this function returns the input
24661
+ * data unchanged. The caller receives `format: 'raw'` and
24662
+ * `wasOptimized: false` to indicate that JPEG encoding was not applied.
24663
+ *
24664
+ * @param image - The raw image pixel data.
24665
+ * @param quality - JPEG quality 1-100 (reserved for future use).
24666
+ * @returns The input data as-is, marked as un-optimized.
24667
+ * @internal
24668
+ */
24669
+ async function recompressJpeg(image, quality) {
24670
+ return {
24671
+ data: image.pixels,
24672
+ width: image.width,
24673
+ height: image.height,
24674
+ channels: image.channels,
24675
+ format: "raw",
24676
+ wasOptimized: false
24677
+ };
24678
+ }
24679
+
23855
24680
  //#endregion
23856
24681
  //#region src/errors.ts
23857
24682
  /**
@@ -24034,5 +24859,5 @@ async function initWasm(options) {
24034
24859
  }
24035
24860
 
24036
24861
  //#endregion
24037
- export { AnnotationFlags, BlendMode, ChangeTracker, CombedTextLayoutError, EmbeddedFont, EncryptedPdfError, ExceededMaxLengthError, FieldAlreadyExistsError, FieldExistsAsNonTerminalError, FieldFlags, FontNotEmbeddedError, ForeignPageError, ImageAlignment, InvalidFieldNamePartError, LineCapStyle, LineJoinStyle, MissingOnValueCheckError, NoSuchFieldError, PDFOperator, PageSizes, ParseSpeeds, PdfAnnotation, PdfArray, PdfBool, PdfButtonField, PdfCheckboxField, PdfCircleAnnotation, PdfDict, PdfDocument, PdfDropdownField, PdfEncryptionHandler, PdfField, PdfForm, PdfFreeTextAnnotation, PdfHighlightAnnotation, PdfInkAnnotation, PdfLayer, PdfLayerManager, PdfLineAnnotation, PdfLinkAnnotation, PdfListboxField, PdfName, PdfNull, PdfNumber, PdfObjectRegistry, PdfOutlineItem, PdfOutlineTree, PdfPage, PdfParseError, PdfPolyLineAnnotation, PdfPolygonAnnotation, PdfRadioGroup, PdfRedactAnnotation, PdfRef, PdfSignatureField, PdfSquareAnnotation, PdfSquigglyAnnotation, PdfStampAnnotation, PdfStream, PdfStreamWriter, PdfStrikeOutAnnotation, PdfString, PdfStructureElement, PdfStructureTree, PdfTextAnnotation, PdfTextField, PdfUnderlineAnnotation, PdfViewerPreferences, PdfWriter, RemovePageFromEmptyDocumentError, RichTextFieldReadError, StandardFonts, TextAlignment, TextRenderingMode, UnexpectedFieldTypeError, addWatermark, addWatermarkToPage, aesDecryptCBC, aesEncryptCBC, annotationFromDict, applyFillColor, applyRedactions, applyStrokeColor, asNumber, asPDFName, asPDFNumber, attachFile, base64Decode, base64Encode, beginArtifact, beginArtifactWithType, beginLayerContent, beginMarkedContent, beginMarkedContentSequence, beginMarkedContentWithProperties, beginText, buildAnnotationDict, buildCatalog, buildDocumentStructure, buildEmbeddedFilesNameTree, buildGradientObjects, buildInfoDict, buildPageTree, buildPatternObjects, buildPkcs7Signature, buildTimestampRequest, buildViewerPreferencesDict, buildXmpMetadata, checkAccessibility, circlePath, clipEvenOdd, clip as clipOp, closeAndStroke, closeFillAndStroke, closeFillEvenOddAndStroke, closePath as closePathOp, cmyk, colorToComponents, componentsToColor, computeFileEncryptionKey, computeFontSize, computeSignatureHash, concatMatrix, concatMatrix as concatTransformationMatrix, copyPages, createAnnotation, createMarkedContentScope, createPdf, createXmpStream, cropPage, curveToFinal, curveToInitial, curveTo as curveToOp, decodePermissions, decodeStream, degrees, degreesToRadians, drawImageWithMatrix, drawImageXObject, drawXObject as drawObject, drawXObject, drawSvgOnPage, ellipsePath, embedPageAsFormXObject, embedSignature, encodeContextTag, encodeInteger, encodeLength, encodeOID, encodeOctetString, encodePermissions, encodePrintableString, encodeSequence, encodeSet, encodeUTCTime, encodeUtf8String, endArtifact, endLayerContent, endMarkedContent, endPath as endPathOp, endText, enforcePdfA, extractMetrics, extractText, extractTextWithPositions, fillAndStroke as fillAndStrokeOp, fillEvenOdd, fillEvenOddAndStroke, fill as fillOp, findSignatures, formatHexContext, formatPdfDate, generateButtonAppearance, generateCheckboxAppearance, generateCircleAppearance, generateDropdownAppearance, generateFreeTextAppearance, generateHighlightAppearance, generateInkAppearance, generateLineAppearance, generateListboxAppearance, generateRadioAppearance, generateSignatureAppearance, generateSquareAppearance, generateSquigglyAppearance, generateStrikeOutAppearance, generateTextAppearance, generateUnderlineAppearance, getAttachments, getPageSize, getRedactionMarks, getSignatures, grayscale, initWasm, insertPage, isAccessible, isLinearized, isOpenTypeCFF, isTrueType, layoutCombedText, layoutMultilineText, layoutSinglelineText, lineTo as lineToOp, linearGradient, linearizePdf, loadPdf, markForRedaction, md5, mergePdfs, movePage, moveText as moveTextOp, moveTextSetLeading, moveTo as moveToOp, nextLine as nextLineOp, parseContentStream, parseSvg, parseSvgColor, parseSvgPath, parseSvgTransform, parseTimestampResponse, parseViewerPreferences, parseXmpMetadata, restoreState as popGraphicsState, restoreState, prepareForSigning, saveState as pushGraphicsState, saveState, radialGradient, radians, radiansToDegrees, rc4, rectangle as rectangleOp, removePage, removePages, requestTimestamp, resizePage, reversePages, rgb, rotateAllPages, rotate as rotateOp, rotatePage, rotationMatrix, saveDocumentIncremental, saveIncremental, scale as scaleOp, serializePdf, setCharacterSpacing as setCharacterSpacingOp, setCharacterSpacing as setCharacterSqueeze, setColorSpace, setDashPattern as setDashPatternOp, setFillColor, setFillColorCmyk, setFillColorGray, setFillColorRgb, setFillingColor, setFlatness, setFont as setFontAndSize, setFont as setFontOp, setFontSize as setFontSizeOp, setGraphicsState as setGraphicsStateOp, setLeading as setLeadingOp, setLeading as setLineHeight, setLineCap as setLineCapOp, setLineJoin as setLineJoinOp, setLineWidth as setLineWidthOp, setMiterLimit, setStrokeColor, setStrokeColorCmyk, setStrokeColorGray, setStrokeColorRgb, setStrokeColorSpace, setStrokingColor, setTextMatrix as setTextMatrixOp, setTextRenderingMode as setTextRenderingModeOp, setTextRise as setTextRiseOp, setWordSpacing as setWordSpacingOp, sha256, sha384, sha512, showTextArray, showTextHex, showTextNextLine, showText as showTextOp, showTextWithSpacing, signPdf, skew as skewOp, splitPdf, stroke as strokeOp, summarizeIssues, svgToPdfOperators, tilingPattern, translate as translateOp, validatePdfA, verifyOwnerPassword, verifySignature, verifySignatures, verifyUserPassword, wrapInMarkedContent };
24862
+ export { AnnotationFlags, BlendMode, ChangeTracker, CombedTextLayoutError, EmbeddedFont, EncryptedPdfError, ExceededMaxLengthError, FieldAlreadyExistsError, FieldExistsAsNonTerminalError, FieldFlags, FontNotEmbeddedError, ForeignPageError, ImageAlignment, InvalidFieldNamePartError, LineCapStyle, LineJoinStyle, MissingOnValueCheckError, NoSuchFieldError, PDFOperator, PageSizes, ParseSpeeds, PdfAnnotation, PdfArray, PdfBool, PdfButtonField, PdfCaretAnnotation, PdfCheckboxField, PdfCircleAnnotation, PdfDict, PdfDocument, PdfDropdownField, PdfEncryptionHandler, PdfField, PdfFileAttachmentAnnotation, PdfForm, PdfFreeTextAnnotation, PdfHighlightAnnotation, PdfInkAnnotation, PdfLayer, PdfLayerManager, PdfLineAnnotation, PdfLinkAnnotation, PdfListboxField, PdfName, PdfNull, PdfNumber, PdfObjectRegistry, PdfOutlineItem, PdfOutlineTree, PdfPage, PdfParseError, PdfPolyLineAnnotation, PdfPolygonAnnotation, PdfPopupAnnotation, PdfRadioGroup, PdfRedactAnnotation, PdfRef, PdfSignatureField, PdfSquareAnnotation, PdfSquigglyAnnotation, PdfStampAnnotation, PdfStream, PdfStreamWriter, PdfStrikeOutAnnotation, PdfString, PdfStructureElement, PdfStructureTree, PdfTextAnnotation, PdfTextField, PdfUnderlineAnnotation, PdfViewerPreferences, PdfWriter, RemovePageFromEmptyDocumentError, RichTextFieldReadError, StandardFonts, TextAlignment, TextRenderingMode, UnexpectedFieldTypeError, addWatermark, addWatermarkToPage, aesDecryptCBC, aesEncryptCBC, annotationFromDict, applyFillColor, applyRedactions, applyStrokeColor, asNumber, asPDFName, asPDFNumber, attachFile, base64Decode, base64Encode, beginArtifact, beginArtifactWithType, beginLayerContent, beginMarkedContent, beginMarkedContentSequence, beginMarkedContentWithProperties, beginText, buildAnnotationDict, buildCatalog, buildDocumentStructure, buildEmbeddedFilesNameTree, buildGradientObjects, buildInfoDict, buildPageTree, buildPatternObjects, buildPkcs7Signature, buildTimestampRequest, buildViewerPreferencesDict, buildXmpMetadata, checkAccessibility, circlePath, clipEvenOdd, clip as clipOp, closeAndStroke, closeFillAndStroke, closeFillEvenOddAndStroke, closePath as closePathOp, cmyk, colorToComponents, componentsToColor, computeFileEncryptionKey, computeFontSize, computeSignatureHash, concatMatrix, concatMatrix as concatTransformationMatrix, copyPages, createAnnotation, createMarkedContentScope, createPdf, createXmpStream, cropPage, curveToFinal, curveToInitial, curveTo as curveToOp, decodePermissions, decodeStream, degrees, degreesToRadians, downscaleImage, drawImageWithMatrix, drawImageXObject, drawXObject as drawObject, drawXObject, drawSvgOnPage, ellipsePath, embedPageAsFormXObject, embedSignature, encodeContextTag, encodeInteger, encodeLength, encodeOID, encodeOctetString, encodePermissions, encodePrintableString, encodeSequence, encodeSet, encodeUTCTime, encodeUtf8String, endArtifact, endLayerContent, endMarkedContent, endPath as endPathOp, endText, enforcePdfA, extractMetrics, extractText, extractTextWithPositions, fillAndStroke as fillAndStrokeOp, fillEvenOdd, fillEvenOddAndStroke, fill as fillOp, findSignatures, formatHexContext, formatPdfDate, generateButtonAppearance, generateCheckboxAppearance, generateCircleAppearance, generateDropdownAppearance, generateFreeTextAppearance, generateHighlightAppearance, generateInkAppearance, generateLineAppearance, generateListboxAppearance, generateRadioAppearance, generateSignatureAppearance, generateSquareAppearance, generateSquigglyAppearance, generateStrikeOutAppearance, generateTextAppearance, generateUnderlineAppearance, getAttachments, getPageSize, getRedactionMarks, getSignatures, grayscale, initWasm, insertPage, isAccessible, isLinearized, isOpenTypeCFF, isTrueType, layoutCombedText, layoutMultilineText, layoutSinglelineText, lineTo as lineToOp, linearGradient, linearizePdf, loadPdf, markForRedaction, md5, mergePdfs, movePage, moveText as moveTextOp, moveTextSetLeading, moveTo as moveToOp, nextLine as nextLineOp, optimizeImage, parseContentStream, parseSvg, parseSvgColor, parseSvgPath, parseSvgTransform, parseTimestampResponse, parseViewerPreferences, parseXmpMetadata, restoreState as popGraphicsState, restoreState, prepareForSigning, saveState as pushGraphicsState, saveState, radialGradient, radians, radiansToDegrees, rc4, recompressImage, rectangle as rectangleOp, removePage, removePages, requestTimestamp, resizePage, reversePages, rgb, rotateAllPages, rotate as rotateOp, rotatePage, rotationMatrix, saveDocumentIncremental, saveIncremental, scale as scaleOp, serializePdf, setCharacterSpacing as setCharacterSpacingOp, setCharacterSpacing as setCharacterSqueeze, setColorSpace, setDashPattern as setDashPatternOp, setFillColor, setFillColorCmyk, setFillColorGray, setFillColorRgb, setFillingColor, setFlatness, setFont as setFontAndSize, setFont as setFontOp, setFontSize as setFontSizeOp, setGraphicsState as setGraphicsStateOp, setLeading as setLeadingOp, setLeading as setLineHeight, setLineCap as setLineCapOp, setLineJoin as setLineJoinOp, setLineWidth as setLineWidthOp, setMiterLimit, setStrokeColor, setStrokeColorCmyk, setStrokeColorGray, setStrokeColorRgb, setStrokeColorSpace, setStrokingColor, setTextMatrix as setTextMatrixOp, setTextRenderingMode as setTextRenderingModeOp, setTextRise as setTextRiseOp, setWordSpacing as setWordSpacingOp, sha256, sha384, sha512, showTextArray, showTextHex, showTextNextLine, showText as showTextOp, showTextWithSpacing, signPdf, skew as skewOp, splitPdf, stroke as strokeOp, summarizeIssues, svgToPdfOperators, tilingPattern, translate as translateOp, validatePdfA, verifyOwnerPassword, verifySignature, verifySignatures, verifyUserPassword, wrapInMarkedContent };
24038
24863
  //# sourceMappingURL=index.mjs.map