modern-pdf-lib 0.13.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.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_pdfPage = require('./pdfPage-BebMv6fN.cjs');
3
- const require_pdfCatalog = require('./pdfCatalog-y_XG8Hq1.cjs');
2
+ const require_pdfPage = require('./pdfPage-DBfdinTR.cjs');
3
+ const require_pdfCatalog = require('./pdfCatalog-COKoYQ8C.cjs');
4
4
  const require_libdeflateWasm = require('./libdeflateWasm-OkNoqBnO.cjs');
5
5
  const require_fontSubset = require('./fontSubset-pFc8Dueu.cjs');
6
6
  const require_pngEmbed = require('./pngEmbed-OYyOe_W0.cjs');
@@ -200,20 +200,19 @@ var PdfWriter = class {
200
200
  */
201
201
  writeObjectStream(entries, xrefEntries) {
202
202
  const serializedObjects = [];
203
- new StringByteWriter();
204
203
  for (const entry of entries) {
205
204
  const objWriter = new StringByteWriter();
206
205
  entry.object.serialize(objWriter);
207
206
  serializedObjects.push(objWriter.toUint8Array());
208
207
  }
209
- let headerStr = "";
208
+ const headerParts = [];
210
209
  let dataOffset = 0;
211
210
  for (let i = 0; i < entries.length; i++) {
212
- if (i > 0) headerStr += " ";
213
- headerStr += `${entries[i].ref.objectNumber} ${dataOffset}`;
211
+ headerParts.push(`${entries[i].ref.objectNumber} ${dataOffset}`);
214
212
  dataOffset += serializedObjects[i].length;
215
213
  if (i < entries.length - 1) dataOffset += 1;
216
214
  }
215
+ const headerStr = headerParts.join(" ");
217
216
  const headerBytes = encoder$5.encode(headerStr + " ");
218
217
  const firstOffset = headerBytes.length;
219
218
  let totalDataLen = firstOffset;
@@ -3755,7 +3754,7 @@ const isDelimiter = /* @__PURE__ */ (() => {
3755
3754
  * `hexVal[b]` is the numeric value (0-15) of a hex character, or -1 if
3756
3755
  * the byte is not a valid hex digit.
3757
3756
  */
3758
- const hexVal = /* @__PURE__ */ (() => {
3757
+ const hexVal$1 = /* @__PURE__ */ (() => {
3759
3758
  const t = new Int8Array(256).fill(-1);
3760
3759
  for (let i = 0; i <= 9; i++) t[CH_0 + i] = i;
3761
3760
  for (let i = 0; i < 6; i++) {
@@ -4052,7 +4051,7 @@ var PdfLexer = class {
4052
4051
  const d = this._data;
4053
4052
  let pos = startPos + 1;
4054
4053
  let depth = 1;
4055
- let result = "";
4054
+ const parts = [];
4056
4055
  while (pos < this.len && depth > 0) {
4057
4056
  const c = d[pos];
4058
4057
  if (c === CH_BACKSLASH) {
@@ -4061,35 +4060,35 @@ var PdfLexer = class {
4061
4060
  const esc = d[pos];
4062
4061
  switch (esc) {
4063
4062
  case 110:
4064
- result += "\n";
4063
+ parts.push("\n");
4065
4064
  pos++;
4066
4065
  break;
4067
4066
  case 114:
4068
- result += "\r";
4067
+ parts.push("\r");
4069
4068
  pos++;
4070
4069
  break;
4071
4070
  case 116:
4072
- result += " ";
4071
+ parts.push(" ");
4073
4072
  pos++;
4074
4073
  break;
4075
4074
  case 98:
4076
- result += "\b";
4075
+ parts.push("\b");
4077
4076
  pos++;
4078
4077
  break;
4079
4078
  case 102:
4080
- result += "\f";
4079
+ parts.push("\f");
4081
4080
  pos++;
4082
4081
  break;
4083
4082
  case CH_LPAREN:
4084
- result += "(";
4083
+ parts.push("(");
4085
4084
  pos++;
4086
4085
  break;
4087
4086
  case CH_RPAREN:
4088
- result += ")";
4087
+ parts.push(")");
4089
4088
  pos++;
4090
4089
  break;
4091
4090
  case CH_BACKSLASH:
4092
- result += "\\";
4091
+ parts.push("\\");
4093
4092
  pos++;
4094
4093
  break;
4095
4094
  case WS_LF:
@@ -4111,9 +4110,9 @@ var PdfLexer = class {
4111
4110
  pos++;
4112
4111
  }
4113
4112
  }
4114
- result += String.fromCharCode(octal & 255);
4113
+ parts.push(String.fromCharCode(octal & 255));
4115
4114
  } else {
4116
- result += String.fromCharCode(esc);
4115
+ parts.push(String.fromCharCode(esc));
4117
4116
  pos++;
4118
4117
  }
4119
4118
  break;
@@ -4122,23 +4121,23 @@ var PdfLexer = class {
4122
4121
  }
4123
4122
  if (c === CH_LPAREN) {
4124
4123
  depth++;
4125
- result += "(";
4124
+ parts.push("(");
4126
4125
  pos++;
4127
4126
  continue;
4128
4127
  }
4129
4128
  if (c === CH_RPAREN) {
4130
4129
  depth--;
4131
- if (depth > 0) result += ")";
4130
+ if (depth > 0) parts.push(")");
4132
4131
  pos++;
4133
4132
  continue;
4134
4133
  }
4135
4134
  if (c === WS_CR) {
4136
- result += "\n";
4135
+ parts.push("\n");
4137
4136
  pos++;
4138
4137
  if (pos < this.len && d[pos] === WS_LF) pos++;
4139
4138
  continue;
4140
4139
  }
4141
- result += String.fromCharCode(c);
4140
+ parts.push(String.fromCharCode(c));
4142
4141
  pos++;
4143
4142
  }
4144
4143
  if (depth !== 0) throw new PdfParseError({
@@ -4151,7 +4150,7 @@ var PdfLexer = class {
4151
4150
  this.position = pos;
4152
4151
  return {
4153
4152
  type: TokenType$1.LiteralString,
4154
- value: result,
4153
+ value: parts.join(""),
4155
4154
  offset: startPos
4156
4155
  };
4157
4156
  }
@@ -4164,7 +4163,8 @@ var PdfLexer = class {
4164
4163
  readHexString(startPos) {
4165
4164
  const d = this._data;
4166
4165
  let pos = startPos + 1;
4167
- let hex = "";
4166
+ const bytes = [];
4167
+ let hi = -1;
4168
4168
  while (pos < this.len) {
4169
4169
  const c = d[pos];
4170
4170
  if (c === CH_GT) {
@@ -4175,27 +4175,26 @@ var PdfLexer = class {
4175
4175
  pos++;
4176
4176
  continue;
4177
4177
  }
4178
- if (hexVal[c] === -1) throw new PdfParseError({
4178
+ const v = hexVal$1[c];
4179
+ if (v === -1) throw new PdfParseError({
4179
4180
  message: `PdfLexer: invalid hex digit 0x${c.toString(16).padStart(2, "0")} at offset ${pos} in hex string starting at ${startPos}`,
4180
4181
  offset: pos,
4181
4182
  expected: "hex digit (0-9, a-f, A-F)",
4182
4183
  actual: `0x${c.toString(16).padStart(2, "0")}`,
4183
4184
  data: this._data
4184
4185
  });
4185
- hex += String.fromCharCode(c);
4186
+ if (hi === -1) hi = v;
4187
+ else {
4188
+ bytes.push(hi << 4 | v);
4189
+ hi = -1;
4190
+ }
4186
4191
  pos++;
4187
4192
  }
4188
- if (hex.length % 2 !== 0) hex += "0";
4189
- let result = "";
4190
- for (let i = 0; i < hex.length; i += 2) {
4191
- const hi = hexVal[hex.charCodeAt(i)];
4192
- const lo = hexVal[hex.charCodeAt(i + 1)];
4193
- result += String.fromCharCode(hi << 4 | lo);
4194
- }
4193
+ if (hi !== -1) bytes.push(hi << 4);
4195
4194
  this.position = pos;
4196
4195
  return {
4197
4196
  type: TokenType$1.HexString,
4198
- value: result,
4197
+ value: String.fromCharCode.apply(null, bytes),
4199
4198
  offset: startPos
4200
4199
  };
4201
4200
  }
@@ -4208,7 +4207,7 @@ var PdfLexer = class {
4208
4207
  readName(startPos) {
4209
4208
  const d = this._data;
4210
4209
  let pos = startPos + 1;
4211
- let name = "/";
4210
+ const parts = ["/"];
4212
4211
  while (pos < this.len) {
4213
4212
  const c = d[pos];
4214
4213
  if (isWhitespace[c] || isDelimiter[c]) break;
@@ -4220,8 +4219,8 @@ var PdfLexer = class {
4220
4219
  actual: "end of input",
4221
4220
  data: this._data
4222
4221
  });
4223
- const hi = hexVal[d[pos + 1]];
4224
- const lo = hexVal[d[pos + 2]];
4222
+ const hi = hexVal$1[d[pos + 1]];
4223
+ const lo = hexVal$1[d[pos + 2]];
4225
4224
  if (hi === -1 || lo === -1) throw new PdfParseError({
4226
4225
  message: `PdfLexer: invalid #XX escape in name at offset ${pos}`,
4227
4226
  offset: pos,
@@ -4229,17 +4228,17 @@ var PdfLexer = class {
4229
4228
  actual: `#${String.fromCharCode(d[pos + 1])}${String.fromCharCode(d[pos + 2])}`,
4230
4229
  data: this._data
4231
4230
  });
4232
- name += String.fromCharCode(hi << 4 | lo);
4231
+ parts.push(String.fromCharCode(hi << 4 | lo));
4233
4232
  pos += 3;
4234
4233
  continue;
4235
4234
  }
4236
- name += String.fromCharCode(c);
4235
+ parts.push(String.fromCharCode(c));
4237
4236
  pos++;
4238
4237
  }
4239
4238
  this.position = pos;
4240
4239
  return {
4241
4240
  type: TokenType$1.Name,
4242
- value: name,
4241
+ value: parts.join(""),
4243
4242
  offset: startPos
4244
4243
  };
4245
4244
  }
@@ -4354,10 +4353,7 @@ var PdfLexer = class {
4354
4353
  * because it avoids the per-call overhead of the streaming decoder.
4355
4354
  */
4356
4355
  bytesToAscii(from, to) {
4357
- const d = this._data;
4358
- let s = "";
4359
- for (let i = from; i < to; i++) s += String.fromCharCode(d[i]);
4360
- return s;
4356
+ return String.fromCharCode.apply(null, this._data.subarray(from, to));
4361
4357
  }
4362
4358
  };
4363
4359
 
@@ -5251,18 +5247,20 @@ var XrefParser = class {
5251
5247
  parseTraditionalXref(offset) {
5252
5248
  const entries = [];
5253
5249
  let pos = offset;
5254
- const xrefTag = TEXT_DECODER$1.decode(this.data.subarray(pos, pos + 4));
5255
- if (xrefTag !== "xref") throw new PdfParseError({
5256
- message: `Invalid PDF: expected "xref" keyword at offset ${offset}, found "${xrefTag}".`,
5257
- offset,
5258
- expected: "\"xref\" keyword",
5259
- actual: `"${xrefTag}"`,
5260
- data: this.data
5261
- });
5250
+ if (!this.isTraditionalXref(pos)) {
5251
+ const xrefTag = TEXT_DECODER$1.decode(this.data.subarray(pos, pos + 4));
5252
+ throw new PdfParseError({
5253
+ message: `Invalid PDF: expected "xref" keyword at offset ${offset}, found "${xrefTag}".`,
5254
+ offset,
5255
+ expected: "\"xref\" keyword",
5256
+ actual: `"${xrefTag}"`,
5257
+ data: this.data
5258
+ });
5259
+ }
5262
5260
  pos += 4;
5263
5261
  pos = this.skipWhitespaceAt(pos);
5264
5262
  while (pos < this.data.length) {
5265
- if (TEXT_DECODER$1.decode(this.data.subarray(pos, Math.min(pos + 7, this.data.length))).startsWith("trailer")) break;
5263
+ if (this.matchesBytes(pos, 116, 114, 97, 105, 108, 101, 114)) break;
5266
5264
  const headerLine = this.readLineAt(pos);
5267
5265
  const headerMatch = headerLine.text.trim().match(/^(\d+)\s+(\d+)/);
5268
5266
  if (!headerMatch) throw new PdfParseError({
@@ -5277,35 +5275,45 @@ var XrefParser = class {
5277
5275
  pos = headerLine.nextPos;
5278
5276
  for (let i = 0; i < count; i++) {
5279
5277
  const objectNumber = firstObjNum + i;
5280
- const entryText = this.readXrefEntryAt(pos);
5281
- const entryMatch = entryText.text.trim().match(/^(\d{10})\s+(\d{5})\s+([fn])/);
5282
- if (!entryMatch) throw new PdfParseError({
5283
- message: `Invalid PDF: malformed xref entry at offset ${pos} for object ${objectNumber}: "${entryText.text.trim()}"`,
5284
- offset: pos,
5285
- expected: "xref entry \"OOOOOOOOOO GGGGG f/n\"",
5286
- actual: `"${entryText.text.trim()}"`,
5287
- data: this.data
5288
- });
5289
- const entryOffset = parseInt(entryMatch[1], 10);
5290
- const gen = parseInt(entryMatch[2], 10);
5291
- const marker = entryMatch[3];
5292
- entries.push({
5293
- objectNumber,
5294
- generationNumber: gen,
5295
- offset: entryOffset,
5296
- type: marker === "n" ? "in-use" : "free"
5297
- });
5298
- pos = entryText.nextPos;
5278
+ const parsed = this.parseXrefEntryDirect(pos);
5279
+ if (parsed) {
5280
+ entries.push({
5281
+ objectNumber,
5282
+ generationNumber: parsed.gen,
5283
+ offset: parsed.offset,
5284
+ type: parsed.type
5285
+ });
5286
+ pos = parsed.nextPos;
5287
+ } else {
5288
+ const entryText = this.readXrefEntryAt(pos);
5289
+ const entryMatch = entryText.text.trim().match(/^(\d{10})\s+(\d{5})\s+([fn])/);
5290
+ if (!entryMatch) throw new PdfParseError({
5291
+ message: `Invalid PDF: malformed xref entry at offset ${pos} for object ${objectNumber}: "${entryText.text.trim()}"`,
5292
+ offset: pos,
5293
+ expected: "xref entry \"OOOOOOOOOO GGGGG f/n\"",
5294
+ actual: `"${entryText.text.trim()}"`,
5295
+ data: this.data
5296
+ });
5297
+ entries.push({
5298
+ objectNumber,
5299
+ generationNumber: parseInt(entryMatch[2], 10),
5300
+ offset: parseInt(entryMatch[1], 10),
5301
+ type: entryMatch[3] === "n" ? "in-use" : "free"
5302
+ });
5303
+ pos = entryText.nextPos;
5304
+ }
5299
5305
  }
5300
5306
  }
5301
- const trailerTag = TEXT_DECODER$1.decode(this.data.subarray(pos, pos + 7));
5302
- if (trailerTag !== "trailer") throw new PdfParseError({
5303
- message: `Invalid PDF: expected "trailer" keyword at offset ${pos}, found "${trailerTag}".`,
5304
- offset: pos,
5305
- expected: "\"trailer\" keyword",
5306
- actual: `"${trailerTag}"`,
5307
- data: this.data
5308
- });
5307
+ if (!this.matchesBytes(pos, 116, 114, 97, 105, 108, 101, 114)) {
5308
+ const trailerTag = TEXT_DECODER$1.decode(this.data.subarray(pos, pos + 7));
5309
+ throw new PdfParseError({
5310
+ message: `Invalid PDF: expected "trailer" keyword at offset ${pos}, found "${trailerTag}".`,
5311
+ offset: pos,
5312
+ expected: "\"trailer\" keyword",
5313
+ actual: `"${trailerTag}"`,
5314
+ data: this.data
5315
+ });
5316
+ }
5309
5317
  pos += 7;
5310
5318
  const trailerObj = this.objectParser.parseObjectAt(pos);
5311
5319
  if (trailerObj.kind !== "dict") throw new PdfParseError({
@@ -5460,17 +5468,33 @@ var XrefParser = class {
5460
5468
  */
5461
5469
  rebuildXrefFromScan() {
5462
5470
  const entries = /* @__PURE__ */ new Map();
5463
- const fileText = TEXT_DECODER$1.decode(this.data);
5464
- const objPattern = /(\d+)\s+(\d+)\s+obj\b/g;
5465
- let match;
5466
- while ((match = objPattern.exec(fileText)) !== null) {
5467
- const objectNumber = parseInt(match[1], 10);
5468
- const gen = parseInt(match[2], 10);
5469
- const offset = match.index;
5471
+ const d = this.data;
5472
+ const len = d.length;
5473
+ for (let i = 0; i < len - 2; i++) {
5474
+ if (d[i] !== 111 || d[i + 1] !== 98 || d[i + 2] !== 106) continue;
5475
+ if (i + 3 < len) {
5476
+ const after = d[i + 3];
5477
+ if (after > 32 && after !== 37 && after !== 40 && after !== 41 && after !== 47 && after !== 60 && after !== 62 && after !== 91 && after !== 93 && after !== 123 && after !== 125) continue;
5478
+ }
5479
+ let j = i - 1;
5480
+ while (j >= 0 && (d[j] === 32 || d[j] === 10 || d[j] === 13 || d[j] === 9 || d[j] === 0)) j--;
5481
+ const genEnd = j + 1;
5482
+ while (j >= 0 && d[j] >= 48 && d[j] <= 57) j--;
5483
+ const genStart = j + 1;
5484
+ if (genStart >= genEnd) continue;
5485
+ while (j >= 0 && (d[j] === 32 || d[j] === 10 || d[j] === 13 || d[j] === 9 || d[j] === 0)) j--;
5486
+ const objEnd = j + 1;
5487
+ while (j >= 0 && d[j] >= 48 && d[j] <= 57) j--;
5488
+ const objStart = j + 1;
5489
+ if (objStart >= objEnd) continue;
5490
+ let objectNumber = 0;
5491
+ for (let k = objStart; k < objEnd; k++) objectNumber = objectNumber * 10 + (d[k] - 48);
5492
+ let gen = 0;
5493
+ for (let k = genStart; k < genEnd; k++) gen = gen * 10 + (d[k] - 48);
5470
5494
  entries.set(objectNumber, {
5471
5495
  objectNumber,
5472
5496
  generationNumber: gen,
5473
- offset,
5497
+ offset: objStart,
5474
5498
  type: "in-use"
5475
5499
  });
5476
5500
  }
@@ -5483,7 +5507,11 @@ var XrefParser = class {
5483
5507
  });
5484
5508
  let rootRef;
5485
5509
  let infoRef;
5486
- const trailerIdx = fileText.lastIndexOf("trailer");
5510
+ let trailerIdx = -1;
5511
+ for (let i = len - 7; i >= 0; i--) if (d[i] === 116 && d[i + 1] === 114 && d[i + 2] === 97 && d[i + 3] === 105 && d[i + 4] === 108 && d[i + 5] === 101 && d[i + 6] === 114) {
5512
+ trailerIdx = i;
5513
+ break;
5514
+ }
5487
5515
  if (trailerIdx !== -1) try {
5488
5516
  const trailerObj = this.objectParser.parseObjectAt(trailerIdx + 7);
5489
5517
  if (trailerObj.kind === "dict") {
@@ -5524,6 +5552,48 @@ var XrefParser = class {
5524
5552
  * Determine whether the data at the given offset starts with the
5525
5553
  * "xref" keyword (indicating a traditional xref table).
5526
5554
  */
5555
+ /**
5556
+ * Check if the bytes at `pos` match the given byte values.
5557
+ */
5558
+ matchesBytes(pos, ...bytes) {
5559
+ if (pos + bytes.length > this.data.length) return false;
5560
+ for (let i = 0; i < bytes.length; i++) if (this.data[pos + i] !== bytes[i]) return false;
5561
+ return true;
5562
+ }
5563
+ /**
5564
+ * Parse a standard xref entry directly from bytes, avoiding TextDecoder
5565
+ * and regex. Returns null if the bytes don't form a valid entry.
5566
+ *
5567
+ * Standard format: `OOOOOOOOOO GGGGG f/n` (18 significant bytes)
5568
+ */
5569
+ parseXrefEntryDirect(pos) {
5570
+ const d = this.data;
5571
+ if (pos + 18 > d.length) return null;
5572
+ let offset = 0;
5573
+ for (let k = 0; k < 10; k++) {
5574
+ const b = d[pos + k];
5575
+ if (b < 48 || b > 57) return null;
5576
+ offset = offset * 10 + (b - 48);
5577
+ }
5578
+ if (d[pos + 10] !== 32) return null;
5579
+ let gen = 0;
5580
+ for (let k = 0; k < 5; k++) {
5581
+ const b = d[pos + 11 + k];
5582
+ if (b < 48 || b > 57) return null;
5583
+ gen = gen * 10 + (b - 48);
5584
+ }
5585
+ if (d[pos + 16] !== 32) return null;
5586
+ const marker = d[pos + 17];
5587
+ if (marker !== 110 && marker !== 102) return null;
5588
+ let nextPos = pos + 18;
5589
+ while (nextPos < d.length && (d[nextPos] === 32 || d[nextPos] === 13 || d[nextPos] === 10)) nextPos++;
5590
+ return {
5591
+ offset,
5592
+ gen,
5593
+ type: marker === 110 ? "in-use" : "free",
5594
+ nextPos
5595
+ };
5596
+ }
5527
5597
  isTraditionalXref(offset) {
5528
5598
  if (offset + 4 > this.data.length) return false;
5529
5599
  return this.data[offset] === 120 && this.data[offset + 1] === 114 && this.data[offset + 2] === 101 && this.data[offset + 3] === 102;
@@ -6237,15 +6307,76 @@ function padPassword(password) {
6237
6307
  return result;
6238
6308
  }
6239
6309
  /**
6240
- * Truncate a UTF-8 password to 127 bytes for R=5/R=6 (SASLprep would
6241
- * be ideal but we approximate with simple truncation as most PDF
6242
- * libraries do).
6310
+ * Prepare a password for R=5/R=6 (AES-256) using SASLprep (RFC 4013).
6311
+ *
6312
+ * ISO 32000-2 SS7.6.3.1 requires SASLprep normalization for passwords
6313
+ * used with encryption revision 5 and 6. The steps are:
6314
+ *
6315
+ * 1. Map: Convert non-ASCII space characters to U+0020, remove
6316
+ * "commonly mapped to nothing" characters (RFC 3454 B.1).
6317
+ * 2. Normalize: Apply Unicode NFKC normalization.
6318
+ * 3. Prohibit: Reject characters from RFC 3454 prohibited tables.
6319
+ * 4. Bidi: Check bidirectional string rules.
6320
+ *
6321
+ * The result is truncated to 127 UTF-8 bytes per the PDF spec.
6243
6322
  */
6244
6323
  function preparePasswordV5(password) {
6245
- const encoded = new TextEncoder().encode(password);
6324
+ const prepared = saslprep(password);
6325
+ const encoded = new TextEncoder().encode(prepared);
6246
6326
  return encoded.length > 127 ? encoded.subarray(0, 127) : encoded;
6247
6327
  }
6248
6328
  /**
6329
+ * Simplified SASLprep (RFC 4013) profile of stringprep (RFC 3454).
6330
+ *
6331
+ * Covers the mapping, normalization, and prohibited-character steps
6332
+ * needed for PDF password preparation. Bidi checking is omitted
6333
+ * since PDF passwords are typically LTR and the spec allows
6334
+ * implementations to skip it.
6335
+ *
6336
+ * @internal
6337
+ */
6338
+ function saslprep(input) {
6339
+ let mapped = "";
6340
+ for (const ch of input) {
6341
+ const cp = ch.codePointAt(0);
6342
+ if (isMappedToNothing(cp)) continue;
6343
+ if (isNonAsciiSpace(cp)) {
6344
+ mapped += " ";
6345
+ continue;
6346
+ }
6347
+ mapped += ch;
6348
+ }
6349
+ const normalized = mapped.normalize("NFKC");
6350
+ for (const ch of normalized) {
6351
+ const cp = ch.codePointAt(0);
6352
+ if (isProhibited(cp)) throw new Error(`Password contains prohibited character U+${cp.toString(16).toUpperCase().padStart(4, "0")} (SASLprep)`);
6353
+ }
6354
+ return normalized;
6355
+ }
6356
+ /** RFC 3454 Table B.1 — Commonly mapped to nothing. */
6357
+ function isMappedToNothing(cp) {
6358
+ 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;
6359
+ }
6360
+ /** RFC 3454 Table C.1.2 — Non-ASCII space characters. */
6361
+ function isNonAsciiSpace(cp) {
6362
+ 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;
6363
+ }
6364
+ /**
6365
+ * RFC 3454 prohibited tables (C.2.1, C.2.2, C.3, C.4, C.5, C.6, C.7, C.8, C.9).
6366
+ * Simplified to the ranges most likely to appear in passwords.
6367
+ */
6368
+ function isProhibited(cp) {
6369
+ if (cp <= 31 || cp === 127) return true;
6370
+ 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;
6371
+ if (cp >= 57344 && cp <= 63743 || cp >= 983040 && cp <= 1048573 || cp >= 1048576 && cp <= 1114109) return true;
6372
+ if (cp >= 64976 && cp <= 65007 || (cp & 65535) === 65534 || (cp & 65535) === 65535) return true;
6373
+ if (cp >= 55296 && cp <= 57343) return true;
6374
+ if (cp === 65529 || cp === 65530 || cp === 65531 || cp === 65532) return true;
6375
+ if (cp === 832 || cp === 833 || cp === 8206 || cp === 8207 || cp >= 8234 && cp <= 8238 || cp >= 8298 && cp <= 8303) return true;
6376
+ if (cp === 917505 || cp >= 917536 && cp <= 917631) return true;
6377
+ return false;
6378
+ }
6379
+ /**
6249
6380
  * Concatenate multiple Uint8Arrays into one.
6250
6381
  */
6251
6382
  function concat$1(...arrays) {
@@ -6321,10 +6452,12 @@ function computeOwnerPasswordValue(ownerPassword, userPassword, revision, keyLen
6321
6452
  if (revision >= 3) for (let i = 0; i < 50; i++) hash = md5(hash.subarray(0, keyByteLength));
6322
6453
  const rc4Key = hash.subarray(0, keyByteLength);
6323
6454
  let encrypted = rc4(rc4Key, padPassword(userPassword));
6324
- if (revision >= 3) for (let i = 1; i <= 19; i++) {
6455
+ if (revision >= 3) {
6325
6456
  const modKey = new Uint8Array(rc4Key.length);
6326
- for (let j = 0; j < rc4Key.length; j++) modKey[j] = (rc4Key[j] ^ i) & 255;
6327
- encrypted = rc4(modKey, encrypted);
6457
+ for (let i = 1; i <= 19; i++) {
6458
+ for (let j = 0; j < rc4Key.length; j++) modKey[j] = (rc4Key[j] ^ i) & 255;
6459
+ encrypted = rc4(modKey, encrypted);
6460
+ }
6328
6461
  }
6329
6462
  return encrypted;
6330
6463
  }
@@ -6354,10 +6487,12 @@ function computeUserPasswordR2(fileKey) {
6354
6487
  */
6355
6488
  function computeUserPasswordR3R4(fileKey, fileId) {
6356
6489
  let encrypted = rc4(fileKey, md5(concat$1(PASSWORD_PADDING, fileId)));
6357
- for (let i = 1; i <= 19; i++) {
6490
+ {
6358
6491
  const modKey = new Uint8Array(fileKey.length);
6359
- for (let j = 0; j < fileKey.length; j++) modKey[j] = (fileKey[j] ^ i) & 255;
6360
- encrypted = rc4(modKey, encrypted);
6492
+ for (let i = 1; i <= 19; i++) {
6493
+ for (let j = 0; j < fileKey.length; j++) modKey[j] = (fileKey[j] ^ i) & 255;
6494
+ encrypted = rc4(modKey, encrypted);
6495
+ }
6361
6496
  }
6362
6497
  const result = new Uint8Array(32);
6363
6498
  result.set(encrypted.subarray(0, 16));
@@ -6423,9 +6558,17 @@ async function algorithm2B(password, salt, uKey) {
6423
6558
  let K = await sha256(uKey ? concat$1(password, salt, uKey) : concat$1(password, salt));
6424
6559
  let round = 0;
6425
6560
  while (true) {
6426
- const K1block = uKey ? concat$1(password, K, uKey) : concat$1(password, K);
6427
- const K1 = new Uint8Array(K1block.length * 64);
6428
- for (let i = 0; i < 64; i++) K1.set(K1block, i * K1block.length);
6561
+ const pLen = password.length;
6562
+ const kLen = K.length;
6563
+ const uLen = uKey ? uKey.length : 0;
6564
+ const blockLen = pLen + kLen + uLen;
6565
+ const K1 = new Uint8Array(blockLen * 64);
6566
+ for (let i = 0; i < 64; i++) {
6567
+ const base = i * blockLen;
6568
+ K1.set(password, base);
6569
+ K1.set(K, base + pLen);
6570
+ if (uKey) K1.set(uKey, base + pLen + kLen);
6571
+ }
6429
6572
  const E = await aesEncryptCBCNoPad(K.subarray(0, 16), K.subarray(16, 32), K1);
6430
6573
  let bigModVal = 0;
6431
6574
  for (let i = 0; i < 16; i++) bigModVal = (bigModVal * 256 + E[i]) % 3;
@@ -6512,11 +6655,33 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
6512
6655
  }
6513
6656
  }
6514
6657
  /**
6658
+ * LRU cache for file encryption keys.
6659
+ *
6660
+ * Keyed on a hash derived from password + encryption dict parameters,
6661
+ * so re-opening the same PDF with the same password skips the expensive
6662
+ * key derivation (especially for R=6 which runs 64+ rounds of AES+SHA).
6663
+ */
6664
+ const fileKeyCache = /* @__PURE__ */ new Map();
6665
+ const FILE_KEY_CACHE_MAX = 32;
6666
+ /**
6667
+ * Build a cache key string from the inputs that uniquely identify
6668
+ * a key derivation result.
6669
+ */
6670
+ function buildCacheKey(password, dict, fileId) {
6671
+ const oHex = Array.from(dict.ownerKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6672
+ const uHex = Array.from(dict.userKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6673
+ const fHex = Array.from(fileId.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
6674
+ return `${dict.revision}:${dict.permissions}:${password}:${oHex}:${uHex}:${fHex}`;
6675
+ }
6676
+ /**
6515
6677
  * Compute the file encryption key from a password and encryption dict.
6516
6678
  *
6517
6679
  * Tries the password as both user and owner password. Returns the key
6518
6680
  * on the first successful match, or throws if neither works.
6519
6681
  *
6682
+ * Results are cached so that re-opening the same PDF with the same
6683
+ * password skips the expensive key derivation.
6684
+ *
6520
6685
  * @param password The password to try.
6521
6686
  * @param dict Encryption dictionary values.
6522
6687
  * @param fileId The first element of the /ID array (unused for R>=5).
@@ -6524,23 +6689,35 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
6524
6689
  * @throws If the password is incorrect.
6525
6690
  */
6526
6691
  async function computeFileEncryptionKey(password, dict, fileId) {
6692
+ const ck = buildCacheKey(password, dict, fileId);
6693
+ const cached = fileKeyCache.get(ck);
6694
+ if (cached) return cached.slice();
6695
+ let result;
6527
6696
  if (dict.revision >= 6) {
6528
6697
  const userKey = await computeEncryptionKeyR6(password, dict, false);
6529
- if (userKey) return userKey;
6530
- const ownerKey = await computeEncryptionKeyR6(password, dict, true);
6531
- if (ownerKey) return ownerKey;
6532
- throw new Error("Incorrect password for R=6 encryption");
6533
- }
6534
- if (dict.revision === 5) {
6698
+ if (userKey) result = userKey;
6699
+ else {
6700
+ const ownerKey = await computeEncryptionKeyR6(password, dict, true);
6701
+ if (ownerKey) result = ownerKey;
6702
+ else throw new Error("Incorrect password for R=6 encryption");
6703
+ }
6704
+ } else if (dict.revision === 5) {
6535
6705
  const userKey = await computeEncryptionKeyR5(password, dict, false);
6536
- if (userKey) return userKey;
6537
- const ownerKey = await computeEncryptionKeyR5(password, dict, true);
6538
- if (ownerKey) return ownerKey;
6539
- throw new Error("Incorrect password for R=5 encryption");
6540
- }
6541
- if (await verifyUserPassword(password, dict, fileId)) return computeEncryptionKeyR2R4(password, dict, fileId);
6542
- if (await verifyOwnerPassword(password, dict, fileId)) return computeEncryptionKeyR2R4Bytes(recoverUserKeyFromOwner(password, dict), dict, fileId);
6543
- throw new Error("Incorrect password");
6706
+ if (userKey) result = userKey;
6707
+ else {
6708
+ const ownerKey = await computeEncryptionKeyR5(password, dict, true);
6709
+ if (ownerKey) result = ownerKey;
6710
+ else throw new Error("Incorrect password for R=5 encryption");
6711
+ }
6712
+ } else if (await verifyUserPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4(password, dict, fileId);
6713
+ else if (await verifyOwnerPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4Bytes(recoverUserKeyFromOwner(password, dict), dict, fileId);
6714
+ else throw new Error("Incorrect password");
6715
+ if (fileKeyCache.size >= FILE_KEY_CACHE_MAX) {
6716
+ const firstKey = fileKeyCache.keys().next().value;
6717
+ if (firstKey !== void 0) fileKeyCache.delete(firstKey);
6718
+ }
6719
+ fileKeyCache.set(ck, result.slice());
6720
+ return result;
6544
6721
  }
6545
6722
  /**
6546
6723
  * Compute encryption key from raw password bytes (already padded/recovered).
@@ -6803,6 +6980,15 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
6803
6980
  perms;
6804
6981
  /** The file ID (first element of /ID array). */
6805
6982
  fileId;
6983
+ /**
6984
+ * Cache for per-object derived keys (V=1-4 only).
6985
+ * Key: `(objNum << 16) | genNum` — unique integer per object.
6986
+ * Value: the derived encryption key.
6987
+ *
6988
+ * Avoids recomputing MD5(fileKey + objNum + genNum [+ sAlT]) for
6989
+ * every string and stream in the same object.
6990
+ */
6991
+ objectKeyCache = /* @__PURE__ */ new Map();
6806
6992
  constructor(params) {
6807
6993
  this.fileKey = params.fileKey;
6808
6994
  this.version = params.version;
@@ -6955,6 +7141,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
6955
7141
  */
6956
7142
  deriveObjectKey(objNum, genNum) {
6957
7143
  if (this.version === 5) return this.fileKey;
7144
+ const cacheKey = objNum << 16 | genNum;
7145
+ const cached = this.objectKeyCache.get(cacheKey);
7146
+ if (cached) return cached;
6958
7147
  const extra = this.useAes ? 4 : 0;
6959
7148
  const input = new Uint8Array(this.fileKey.length + 5 + extra);
6960
7149
  input.set(this.fileKey, 0);
@@ -6972,7 +7161,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
6972
7161
  }
6973
7162
  const hash = md5(input);
6974
7163
  const keyLen = Math.min(this.keyLengthBits / 8 + 5, 16);
6975
- return hash.subarray(0, keyLen);
7164
+ const key = hash.subarray(0, keyLen);
7165
+ this.objectKeyCache.set(cacheKey, key);
7166
+ return key;
6976
7167
  }
6977
7168
  /**
6978
7169
  * Encrypt raw data for a specific object.
@@ -8577,7 +8768,17 @@ const NS_PDFAID = "http://www.aiim.org/pdfa/ns/id/";
8577
8768
  * @internal
8578
8769
  */
8579
8770
  function escapeXml(str) {
8580
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
8771
+ const parts = [];
8772
+ for (let i = 0; i < str.length; i++) {
8773
+ const c = str.charCodeAt(i);
8774
+ if (c === 38) parts.push("&amp;");
8775
+ else if (c === 60) parts.push("&lt;");
8776
+ else if (c === 62) parts.push("&gt;");
8777
+ else if (c === 34) parts.push("&quot;");
8778
+ else if (c === 39) parts.push("&apos;");
8779
+ else parts.push(str[i]);
8780
+ }
8781
+ return parts.join("");
8581
8782
  }
8582
8783
  /**
8583
8784
  * Format a Date as an ISO 8601 string for XMP.
@@ -9880,9 +10081,10 @@ function remapRef$1(sourceRef, context) {
9880
10081
  context.refMap.set(sourceRef.objectNumber, placeholderRef);
9881
10082
  return placeholderRef;
9882
10083
  }
10084
+ let streamHash;
9883
10085
  if (sourceObj.kind === "stream") {
9884
- const hash = hashBytes(sourceObj.data);
9885
- const dedup = context.hashMap.get(hash);
10086
+ streamHash = hashBytes(sourceObj.data);
10087
+ const dedup = context.hashMap.get(streamHash);
9886
10088
  if (dedup) {
9887
10089
  context.refMap.set(sourceRef.objectNumber, dedup);
9888
10090
  return dedup;
@@ -9892,10 +10094,7 @@ function remapRef$1(sourceRef, context) {
9892
10094
  context.refMap.set(sourceRef.objectNumber, targetRef);
9893
10095
  const clonedObj = deepClone$1(sourceObj, context);
9894
10096
  context.targetRegistry.assign(targetRef, clonedObj);
9895
- if (sourceObj.kind === "stream") {
9896
- const hash = hashBytes(sourceObj.data);
9897
- context.hashMap.set(hash, targetRef);
9898
- }
10097
+ if (streamHash !== void 0) context.hashMap.set(streamHash, targetRef);
9899
10098
  return targetRef;
9900
10099
  }
9901
10100
  /**
@@ -10126,23 +10325,73 @@ function findStringForward(data, needle, startOffset) {
10126
10325
  return -1;
10127
10326
  }
10128
10327
  /**
10129
- * Prepare a PDF for signing by appending a signature dictionary
10130
- * via incremental update.
10131
- *
10132
- * This function:
10133
- * 1. Appends a new signature field and value to the PDF
10134
- * 2. Inserts an empty `/Contents` placeholder of the specified size
10135
- * 3. Computes the `/ByteRange` that excludes the `/Contents` value
10328
+ * Build a PDF content stream for a visible signature appearance.
10136
10329
  *
10137
- * The resulting PDF bytes can be hashed (excluding the placeholder gap)
10138
- * and the hash can be signed.
10330
+ * Renders a bordered rectangle with optional background color and
10331
+ * text lines rendered in Helvetica.
10139
10332
  *
10140
- * @param pdfBytes The original PDF file bytes.
10141
- * @param signatureFieldName The name for the signature field.
10142
- * @param placeholderSize Size in bytes for the signature. Default 8192.
10143
- * @returns The prepared PDF and ByteRange info.
10333
+ * @internal
10144
10334
  */
10145
- function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192) {
10335
+ function buildSignatureAppearanceStream(options) {
10336
+ const [, , w, h] = options.rect;
10337
+ const fontSize = options.fontSize ?? 10;
10338
+ const borderWidth = options.borderWidth ?? 1;
10339
+ const borderColor = options.borderColor ?? [
10340
+ 0,
10341
+ 0,
10342
+ 0
10343
+ ];
10344
+ const bgColor = options.backgroundColor;
10345
+ const lines = options.textLines;
10346
+ const ops = [];
10347
+ ops.push("q");
10348
+ if (bgColor) {
10349
+ ops.push(`${n$5(bgColor[0])} ${n$5(bgColor[1])} ${n$5(bgColor[2])} rg`);
10350
+ ops.push(`0 0 ${n$5(w)} ${n$5(h)} re`);
10351
+ ops.push("f");
10352
+ }
10353
+ if (borderWidth > 0) {
10354
+ ops.push(`${n$5(borderColor[0])} ${n$5(borderColor[1])} ${n$5(borderColor[2])} RG`);
10355
+ ops.push(`${n$5(borderWidth)} w`);
10356
+ const bw2 = borderWidth / 2;
10357
+ ops.push(`${n$5(bw2)} ${n$5(bw2)} ${n$5(w - borderWidth)} ${n$5(h - borderWidth)} re`);
10358
+ ops.push("S");
10359
+ }
10360
+ if (lines.length > 0) {
10361
+ const margin = borderWidth + 4;
10362
+ const lineHeight = fontSize * 1.2;
10363
+ ops.push("BT");
10364
+ ops.push(`/F1 ${n$5(fontSize)} Tf`);
10365
+ ops.push("0 0 0 rg");
10366
+ const startY = h - margin - fontSize;
10367
+ for (let i = 0; i < lines.length; i++) {
10368
+ const y = startY - i * lineHeight;
10369
+ if (y < margin) break;
10370
+ ops.push(`${n$5(margin)} ${n$5(y)} Td`);
10371
+ ops.push(`(${escapePdfString(lines[i])}) Tj`);
10372
+ ops.push(`${n$5(-margin)} ${n$5(-y)} Td`);
10373
+ }
10374
+ ops.push("ET");
10375
+ }
10376
+ ops.push("Q");
10377
+ return ops.join("\n");
10378
+ }
10379
+ /**
10380
+ * Escape a string for use inside a PDF literal string `(...)`.
10381
+ * @internal
10382
+ */
10383
+ function escapePdfString(str) {
10384
+ return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
10385
+ }
10386
+ /**
10387
+ * Format a number for PDF operators (max 6 decimal places, no trailing zeros).
10388
+ * @internal
10389
+ */
10390
+ function n$5(value) {
10391
+ if (Number.isInteger(value)) return value.toString();
10392
+ return value.toFixed(6).replace(/\.?0+$/, "");
10393
+ }
10394
+ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192, appearance) {
10146
10395
  const pdfStr = decoder$4.decode(pdfBytes);
10147
10396
  const startxrefIdx = pdfStr.lastIndexOf("startxref");
10148
10397
  if (startxrefIdx === -1) throw new Error("Cannot find startxref in PDF — file may be corrupted");
@@ -10159,9 +10408,21 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
10159
10408
  const infoMatch = pdfStr.match(/\/Info\s+(\d+)\s+(\d+)\s+R/);
10160
10409
  const sigValueObjNum = originalSize;
10161
10410
  const sigFieldObjNum = originalSize + 1;
10162
- const newSize = originalSize + 2;
10411
+ let apStreamObjNum = -1;
10412
+ let newSize = originalSize + 2;
10413
+ if (appearance) {
10414
+ apStreamObjNum = newSize;
10415
+ newSize++;
10416
+ }
10163
10417
  const sigDictStr = buildSignatureDictString(placeholderSize, signatureFieldName);
10164
- const sigFieldDict = `<< /Type /Annot /Subtype /Widget /FT /Sig /T (${signatureFieldName}) /V ${sigValueObjNum} 0 R /F 132 /Rect [0 0 0 0] >>`;
10418
+ let rectStr = "0 0 0 0";
10419
+ if (appearance) {
10420
+ const [x, y, w, h] = appearance.rect;
10421
+ rectStr = `${x} ${y} ${x + w} ${y + h}`;
10422
+ }
10423
+ let sigFieldDict = `<< /Type /Annot /Subtype /Widget /FT /Sig /T (${signatureFieldName}) /V ${sigValueObjNum} 0 R /F 132 /Rect [${rectStr}]`;
10424
+ if (appearance && apStreamObjNum >= 0) sigFieldDict += ` /AP << /N ${apStreamObjNum} 0 R >>`;
10425
+ sigFieldDict += " >>";
10165
10426
  let appendix = "\n";
10166
10427
  const objOffsets = /* @__PURE__ */ new Map();
10167
10428
  const sigValueStart = pdfBytes.length + encoder$3.encode(appendix).length;
@@ -10174,11 +10435,28 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
10174
10435
  appendix += `${sigFieldObjNum} 0 obj\n`;
10175
10436
  appendix += sigFieldDict;
10176
10437
  appendix += `\nendobj\n`;
10438
+ let apStreamStart = -1;
10439
+ if (appearance && apStreamObjNum >= 0) {
10440
+ apStreamStart = pdfBytes.length + encoder$3.encode(appendix).length;
10441
+ objOffsets.set(apStreamObjNum, apStreamStart);
10442
+ const apContent = buildSignatureAppearanceStream(appearance);
10443
+ const [, , w, h] = appearance.rect;
10444
+ appendix += `${apStreamObjNum} 0 obj\n`;
10445
+ appendix += `<< /Type /XObject /Subtype /Form /BBox [0 0 ${w} ${h}]`;
10446
+ appendix += ` /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>`;
10447
+ appendix += ` /Length ${apContent.length} >>\n`;
10448
+ appendix += `stream\n`;
10449
+ appendix += apContent;
10450
+ appendix += `\nendstream\n`;
10451
+ appendix += `endobj\n`;
10452
+ }
10177
10453
  const xrefOffset = pdfBytes.length + encoder$3.encode(appendix).length;
10454
+ const objCount = appearance ? 3 : 2;
10178
10455
  appendix += "xref\n";
10179
- appendix += `${sigValueObjNum} 2\n`;
10456
+ appendix += `${sigValueObjNum} ${objCount}\n`;
10180
10457
  appendix += `${sigValueStart.toString().padStart(10, "0")} 00000 n \n`;
10181
10458
  appendix += `${sigFieldStart.toString().padStart(10, "0")} 00000 n \n`;
10459
+ if (appearance && apStreamStart >= 0) appendix += `${apStreamStart.toString().padStart(10, "0")} 00000 n \n`;
10182
10460
  appendix += "trailer\n";
10183
10461
  appendix += "<<\n";
10184
10462
  appendix += `/Size ${newSize}\n`;
@@ -10968,7 +11246,32 @@ function parsePkcs7ForCert(pkcs7Bytes) {
10968
11246
  */
10969
11247
  async function signPdf(pdfBytes, fieldName, options) {
10970
11248
  const hashAlgorithm = options.hashAlgorithm ?? "SHA-256";
10971
- const { preparedPdf, byteRange } = prepareForSigning(pdfBytes, fieldName, 8192);
11249
+ let prepareAppearance;
11250
+ if (options.appearance) {
11251
+ const ap = options.appearance;
11252
+ let textLines = ap.text;
11253
+ if (!textLines) {
11254
+ textLines = [];
11255
+ try {
11256
+ const { subjectCN } = extractIssuerAndSerial(options.certificate);
11257
+ if (subjectCN) textLines.push(`Signed by: ${subjectCN}`);
11258
+ } catch {
11259
+ textLines.push("Digitally Signed");
11260
+ }
11261
+ if (options.reason) textLines.push(`Reason: ${options.reason}`);
11262
+ if (options.location) textLines.push(`Location: ${options.location}`);
11263
+ textLines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString().substring(0, 10)}`);
11264
+ }
11265
+ prepareAppearance = {
11266
+ rect: ap.rect,
11267
+ textLines,
11268
+ fontSize: ap.fontSize,
11269
+ backgroundColor: ap.backgroundColor,
11270
+ borderColor: ap.borderColor,
11271
+ borderWidth: ap.borderWidth
11272
+ };
11273
+ }
11274
+ const { preparedPdf, byteRange } = prepareForSigning(pdfBytes, fieldName, 8192, prepareAppearance);
10972
11275
  return embedSignature(preparedPdf, await buildPkcs7Signature(await computeSignatureHash(preparedPdf, byteRange.byteRange, hashAlgorithm), {
10973
11276
  signerInfo: {
10974
11277
  certificate: options.certificate,
@@ -14474,7 +14777,6 @@ function read2DMode(reader) {
14474
14777
  if (bit === 1) {
14475
14778
  bit = reader.readBit();
14476
14779
  if (bit < 0) return Mode2D.EOL;
14477
- if (bit === 1) return Mode2D.HORIZONTAL;
14478
14780
  if (bit === 0) return Mode2D.VERTICAL_MINUS_1;
14479
14781
  return Mode2D.VERTICAL_PLUS_1;
14480
14782
  }
@@ -17605,9 +17907,28 @@ function hexValue(ch) {
17605
17907
  * `~>` marks end-of-data.
17606
17908
  */
17607
17909
  function decodeASCII85(data) {
17608
- const result = [];
17609
- const group = [];
17910
+ let out = new Uint8Array(Math.max(data.length * 4 / 5 | 0, 256));
17911
+ let outPos = 0;
17912
+ const group = new Uint8Array(5);
17913
+ let groupLen = 0;
17610
17914
  let i = 0;
17915
+ function ensureOut(needed) {
17916
+ if (outPos + needed > out.length) {
17917
+ const newBuf = new Uint8Array(Math.max(out.length * 2, outPos + needed));
17918
+ newBuf.set(out.subarray(0, outPos));
17919
+ out = newBuf;
17920
+ }
17921
+ }
17922
+ function decodeGroup(count) {
17923
+ for (let k = count; k < 5; k++) group[k] = 84;
17924
+ const val = group[0] * 85 * 85 * 85 * 85 + group[1] * 85 * 85 * 85 + group[2] * 85 * 85 + group[3] * 85 + group[4];
17925
+ const numBytes = count === 5 ? 4 : count - 1;
17926
+ ensureOut(numBytes);
17927
+ if (numBytes >= 1) out[outPos++] = val >>> 24 & 255;
17928
+ if (numBytes >= 2) out[outPos++] = val >>> 16 & 255;
17929
+ if (numBytes >= 3) out[outPos++] = val >>> 8 & 255;
17930
+ if (numBytes >= 4) out[outPos++] = val & 255;
17931
+ }
17611
17932
  while (i < data.length) {
17612
17933
  const ch = data[i];
17613
17934
  if (ch === 126) break;
@@ -17616,8 +17937,12 @@ function decodeASCII85(data) {
17616
17937
  continue;
17617
17938
  }
17618
17939
  if (ch === 122) {
17619
- if (group.length > 0) throw new Error("ASCII85Decode: \"z\" inside a group");
17620
- result.push(0, 0, 0, 0);
17940
+ if (groupLen > 0) throw new Error("ASCII85Decode: \"z\" inside a group");
17941
+ ensureOut(4);
17942
+ out[outPos++] = 0;
17943
+ out[outPos++] = 0;
17944
+ out[outPos++] = 0;
17945
+ out[outPos++] = 0;
17621
17946
  i++;
17622
17947
  continue;
17623
17948
  }
@@ -17625,24 +17950,15 @@ function decodeASCII85(data) {
17625
17950
  i++;
17626
17951
  continue;
17627
17952
  }
17628
- group.push(ch - 33);
17629
- if (group.length === 5) {
17630
- const val = group[0] * 85 * 85 * 85 * 85 + group[1] * 85 * 85 * 85 + group[2] * 85 * 85 + group[3] * 85 + group[4];
17631
- result.push(val >>> 24 & 255, val >>> 16 & 255, val >>> 8 & 255, val & 255);
17632
- group.length = 0;
17953
+ group[groupLen++] = ch - 33;
17954
+ if (groupLen === 5) {
17955
+ decodeGroup(5);
17956
+ groupLen = 0;
17633
17957
  }
17634
17958
  i++;
17635
17959
  }
17636
- if (group.length >= 2) {
17637
- const origLen = group.length;
17638
- while (group.length < 5) group.push(84);
17639
- const val = group[0] * 85 * 85 * 85 * 85 + group[1] * 85 * 85 * 85 + group[2] * 85 * 85 + group[3] * 85 + group[4];
17640
- const numBytes = origLen - 1;
17641
- if (numBytes >= 1) result.push(val >>> 24 & 255);
17642
- if (numBytes >= 2) result.push(val >>> 16 & 255);
17643
- if (numBytes >= 3) result.push(val >>> 8 & 255);
17644
- }
17645
- return new Uint8Array(result);
17960
+ if (groupLen >= 2) decodeGroup(groupLen);
17961
+ return out.subarray(0, outPos);
17646
17962
  }
17647
17963
  /**
17648
17964
  * Decode an LZWDecode stream.
@@ -17662,6 +17978,10 @@ function decodeLZW(data, parms) {
17662
17978
  }
17663
17979
  /**
17664
17980
  * Core LZW decompression.
17981
+ *
17982
+ * Uses a flat pooled buffer for the code table instead of per-entry
17983
+ * Uint8Array allocations, and a pre-allocated output buffer with
17984
+ * manual growth instead of `number[]` + push.
17665
17985
  */
17666
17986
  function lzwDecompress(data, earlyChange) {
17667
17987
  const CLEAR_TABLE = 256;
@@ -17686,71 +18006,88 @@ function lzwDecompress(data, earlyChange) {
17686
18006
  }
17687
18007
  return result;
17688
18008
  }
17689
- let table = [];
18009
+ let tableBuf = new Uint8Array(65536);
18010
+ const tableOff = new Int32Array(4096);
18011
+ const tableLen = new Int32Array(4096);
18012
+ let tableBufUsed = 256;
17690
18013
  let codeSize = 9;
17691
18014
  let nextCode = 258;
18015
+ for (let i = 0; i < 256; i++) {
18016
+ tableBuf[i] = i;
18017
+ tableOff[i] = i;
18018
+ tableLen[i] = 1;
18019
+ }
17692
18020
  function resetTable() {
17693
- table = [];
17694
- for (let i = 0; i < 256; i++) table[i] = new Uint8Array([i]);
17695
- table[256] = new Uint8Array(0);
17696
- table[257] = new Uint8Array(0);
18021
+ tableBufUsed = 256;
17697
18022
  nextCode = 258;
17698
18023
  codeSize = 9;
17699
18024
  }
17700
- resetTable();
17701
- const output = [];
17702
- let prevEntry = null;
18025
+ let out = new Uint8Array(Math.max(data.length * 3, 4096));
18026
+ let outPos = 0;
18027
+ function ensureOut(needed) {
18028
+ if (outPos + needed > out.length) {
18029
+ const newBuf = new Uint8Array(Math.max(out.length * 2, outPos + needed));
18030
+ newBuf.set(out.subarray(0, outPos));
18031
+ out = newBuf;
18032
+ }
18033
+ }
18034
+ function ensureTable(needed) {
18035
+ if (tableBufUsed + needed > tableBuf.length) {
18036
+ const newBuf = new Uint8Array(Math.max(tableBuf.length * 2, tableBufUsed + needed));
18037
+ newBuf.set(tableBuf.subarray(0, tableBufUsed));
18038
+ tableBuf = newBuf;
18039
+ }
18040
+ }
18041
+ function writeEntry(code) {
18042
+ const off = tableOff[code];
18043
+ const len = tableLen[code];
18044
+ ensureOut(len);
18045
+ out.set(tableBuf.subarray(off, off + len), outPos);
18046
+ outPos += len;
18047
+ }
18048
+ function addEntry(prevCode, firstByte) {
18049
+ const prevOff = tableOff[prevCode];
18050
+ const prevLen = tableLen[prevCode];
18051
+ const newLen = prevLen + 1;
18052
+ ensureTable(newLen);
18053
+ tableBuf.set(tableBuf.subarray(prevOff, prevOff + prevLen), tableBufUsed);
18054
+ tableBuf[tableBufUsed + prevLen] = firstByte;
18055
+ tableOff[nextCode] = tableBufUsed;
18056
+ tableLen[nextCode] = newLen;
18057
+ tableBufUsed += newLen;
18058
+ nextCode++;
18059
+ }
17703
18060
  let code = readBits(codeSize);
17704
18061
  if (code === CLEAR_TABLE) {
17705
18062
  resetTable();
17706
18063
  code = readBits(codeSize);
17707
18064
  }
17708
- if (code === EOD) return new Uint8Array(output);
17709
- let entry = table[code];
17710
- if (entry) {
17711
- for (const b of entry) output.push(b);
17712
- prevEntry = entry;
17713
- }
18065
+ if (code === EOD) return new Uint8Array(0);
18066
+ writeEntry(code);
18067
+ let prevCode = code;
17714
18068
  while (true) {
17715
18069
  code = readBits(codeSize);
17716
18070
  if (code === EOD) break;
17717
18071
  if (code === CLEAR_TABLE) {
17718
18072
  resetTable();
17719
- prevEntry = null;
17720
18073
  code = readBits(codeSize);
17721
18074
  if (code === EOD) break;
17722
- entry = table[code];
17723
- if (entry) {
17724
- for (const b of entry) output.push(b);
17725
- prevEntry = entry;
17726
- }
18075
+ writeEntry(code);
18076
+ prevCode = code;
17727
18077
  continue;
17728
18078
  }
17729
- if (code < nextCode && table[code]) {
17730
- entry = table[code];
17731
- for (const b of entry) output.push(b);
17732
- if (prevEntry) {
17733
- const newEntry = new Uint8Array(prevEntry.length + 1);
17734
- newEntry.set(prevEntry);
17735
- newEntry[prevEntry.length] = entry[0];
17736
- table[nextCode] = newEntry;
17737
- nextCode++;
17738
- }
18079
+ if (code < nextCode) {
18080
+ writeEntry(code);
18081
+ addEntry(prevCode, tableBuf[tableOff[code]]);
17739
18082
  } else {
17740
- if (!prevEntry) throw new Error("LZWDecode: invalid code sequence");
17741
- const newEntry = new Uint8Array(prevEntry.length + 1);
17742
- newEntry.set(prevEntry);
17743
- newEntry[prevEntry.length] = prevEntry[0];
17744
- for (const b of newEntry) output.push(b);
17745
- table[nextCode] = newEntry;
17746
- nextCode++;
17747
- entry = newEntry;
17748
- }
17749
- prevEntry = entry;
18083
+ addEntry(prevCode, tableBuf[tableOff[prevCode]]);
18084
+ writeEntry(nextCode - 1);
18085
+ }
18086
+ prevCode = code;
17750
18087
  const threshold = (1 << codeSize) - earlyChange;
17751
18088
  if (nextCode >= threshold && codeSize < 12) codeSize++;
17752
18089
  }
17753
- return new Uint8Array(output);
18090
+ return out.subarray(0, outPos);
17754
18091
  }
17755
18092
  /**
17756
18093
  * Decode a RunLengthDecode stream.
@@ -17882,7 +18219,6 @@ function embedPageAsFormXObject(page, sourceRegistry, targetRegistry, xObjectNam
17882
18219
  const pageWidth = page.width;
17883
18220
  const pageHeight = page.height;
17884
18221
  const contentChunks = [];
17885
- new TextDecoder();
17886
18222
  const originalRefs = page.getOriginalContentRefs();
17887
18223
  for (const ref of originalRefs) {
17888
18224
  const obj = sourceRegistry.resolve(ref);
@@ -18400,7 +18736,7 @@ var PdfDocument = class PdfDocument {
18400
18736
  async embedTrueTypeFont(fontData, options) {
18401
18737
  const embeddedFont = await embedFont(fontData);
18402
18738
  const metrics = embeddedFont.metrics;
18403
- const psName = options?.customName || metrics.postScriptName || metrics.familyName || "CustomFont";
18739
+ const psName = options?.customName ?? metrics.postScriptName ?? metrics.familyName ?? "CustomFont";
18404
18740
  const existing = this.embeddedFonts.get(`__ttf__${psName}`);
18405
18741
  if (existing) return existing;
18406
18742
  this.fontCounter++;
@@ -18515,7 +18851,7 @@ var PdfDocument = class PdfDocument {
18515
18851
  async embedCffFont(fontData, options) {
18516
18852
  const embeddedFont = await embedFont(fontData);
18517
18853
  const metrics = embeddedFont.metrics;
18518
- const psName = options?.customName || metrics.postScriptName || metrics.familyName || "CFFFont";
18854
+ const psName = options?.customName ?? metrics.postScriptName ?? metrics.familyName ?? "CFFFont";
18519
18855
  const existing = this.embeddedFonts.get(`__cff__${psName}`);
18520
18856
  if (existing) return existing;
18521
18857
  this.fontCounter++;
@@ -18747,6 +19083,30 @@ var PdfDocument = class PdfDocument {
18747
19083
  return imageRef;
18748
19084
  }
18749
19085
  /**
19086
+ * Embed an image, auto-detecting the format from file headers.
19087
+ *
19088
+ * Inspects the first bytes to determine whether the data is PNG or JPEG,
19089
+ * then delegates to {@link embedPng} or {@link embedJpeg} accordingly.
19090
+ *
19091
+ * @param imageData Raw image file bytes (PNG or JPEG).
19092
+ * @returns An {@link ImageRef} to pass to `page.drawImage()`.
19093
+ * @throws If the image format cannot be detected.
19094
+ *
19095
+ * @example
19096
+ * ```ts
19097
+ * const bytes = new Uint8Array(await readFile('photo.jpg'));
19098
+ * const image = await pdf.embedImage(bytes);
19099
+ * page.drawImage(image, { x: 50, y: 400, width: 200, height: 150 });
19100
+ * ```
19101
+ */
19102
+ async embedImage(imageData) {
19103
+ const data = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : imageData;
19104
+ if (data.length < 4) throw new Error("Image data too short to detect format");
19105
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return this.embedPng(data);
19106
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return this.embedJpeg(data);
19107
+ 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(" ")}.`);
19108
+ }
19109
+ /**
18750
19110
  * Embed pages from another PDF as Form XObjects.
18751
19111
  *
18752
19112
  * Each embedded page is turned into a self-contained Form XObject that
@@ -18805,7 +19165,7 @@ var PdfDocument = class PdfDocument {
18805
19165
  * @param pages Array of PdfPage instances to embed.
18806
19166
  * @returns Array of {@link EmbeddedPdfPage} handles, one per input page.
18807
19167
  */
18808
- async embedPages(pages) {
19168
+ embedPages(pages) {
18809
19169
  return pages.map((page) => this.embedPage(page));
18810
19170
  }
18811
19171
  /** Set the document title. */
@@ -20337,8 +20697,8 @@ function compressStream(stream, level) {
20337
20697
  * @returns The incremental save result.
20338
20698
  */
20339
20699
  async function saveDocumentIncremental(originalBytes, doc, options) {
20340
- const { buildDocumentStructure } = await Promise.resolve().then(() => require("./pdfCatalog-y_XG8Hq1.cjs")).then((n) => n.pdfCatalog_exports);
20341
- const { PdfPage: _PdfPage } = await Promise.resolve().then(() => require("./pdfPage-BebMv6fN.cjs")).then((n) => n.pdfPage_exports);
20700
+ const { buildDocumentStructure } = await Promise.resolve().then(() => require("./pdfCatalog-COKoYQ8C.cjs")).then((n) => n.pdfCatalog_exports);
20701
+ const { PdfPage: _PdfPage } = await Promise.resolve().then(() => require("./pdfPage-DBfdinTR.cjs")).then((n) => n.pdfPage_exports);
20342
20702
  const registry = doc.getRegistry();
20343
20703
  const structure = buildDocumentStructure(doc.getInternalPages().map((p) => p.finalize()), {
20344
20704
  producer: doc.getProducer(),
@@ -21647,6 +22007,261 @@ var PdfRedactAnnotation = class PdfRedactAnnotation extends require_pdfPage.PdfA
21647
22007
  }
21648
22008
  };
21649
22009
 
22010
+ //#endregion
22011
+ //#region src/annotation/types/popupAnnotation.ts
22012
+ /**
22013
+ * @module annotation/types/popupAnnotation
22014
+ *
22015
+ * Popup annotation — a floating window that displays the text of its
22016
+ * parent annotation (typically a text/sticky note annotation).
22017
+ *
22018
+ * Popup annotations have no appearance of their own; the PDF viewer
22019
+ * renders them as a resizable window near the parent annotation.
22020
+ *
22021
+ * Reference: PDF 1.7 spec, Section 12.5.6.14 (Popup Annotations).
22022
+ */
22023
+ /**
22024
+ * A popup annotation (subtype /Popup).
22025
+ *
22026
+ * Displays a floating window containing the text of its parent
22027
+ * annotation. The parent annotation references this popup via its
22028
+ * `/Popup` entry, and this popup references its parent via `/Parent`.
22029
+ */
22030
+ var PdfPopupAnnotation = class PdfPopupAnnotation extends require_pdfPage.PdfAnnotation {
22031
+ constructor(dict) {
22032
+ super("Popup", dict);
22033
+ }
22034
+ /**
22035
+ * Create a new popup annotation.
22036
+ *
22037
+ * @param options.open Whether the popup is initially open. Default: false.
22038
+ * @param options.parent Reference to the parent annotation (set after registration).
22039
+ */
22040
+ static create(options) {
22041
+ const annot = new PdfPopupAnnotation(require_pdfPage.buildAnnotationDict("Popup", options));
22042
+ if (options.open !== void 0) annot.setOpen(options.open);
22043
+ return annot;
22044
+ }
22045
+ /**
22046
+ * Create a PdfPopupAnnotation from an existing dictionary.
22047
+ */
22048
+ static fromDict(dict, _resolver) {
22049
+ return new PdfPopupAnnotation(dict);
22050
+ }
22051
+ /** Whether the popup is initially open. */
22052
+ isOpen() {
22053
+ const obj = this.dict.get("/Open");
22054
+ if (obj && obj.kind === "bool") return obj.value;
22055
+ return false;
22056
+ }
22057
+ /** Set the initial open state. */
22058
+ setOpen(open) {
22059
+ this.dict.set("/Open", require_pdfCatalog.PdfBool.of(open));
22060
+ }
22061
+ /**
22062
+ * Set the parent annotation reference.
22063
+ * The parent is the annotation whose text this popup displays.
22064
+ */
22065
+ setParent(parentRef) {
22066
+ this.dict.set("/Parent", parentRef);
22067
+ }
22068
+ /** Get the parent annotation reference, if set. */
22069
+ getParent() {
22070
+ const obj = this.dict.get("/Parent");
22071
+ if (obj && obj.kind === "ref") return obj;
22072
+ }
22073
+ };
22074
+
22075
+ //#endregion
22076
+ //#region src/annotation/types/caretAnnotation.ts
22077
+ /**
22078
+ * @module annotation/types/caretAnnotation
22079
+ *
22080
+ * Caret annotation — marks a text insertion point in the document.
22081
+ *
22082
+ * A caret annotation indicates where text should be inserted. It is
22083
+ * typically used in document review workflows to suggest additions.
22084
+ * The annotation renders as a caret (^) symbol at the specified
22085
+ * location.
22086
+ *
22087
+ * Reference: PDF 1.7 spec, Section 12.5.6.11 (Caret Annotations).
22088
+ */
22089
+ /**
22090
+ * A caret annotation (subtype /Caret).
22091
+ *
22092
+ * Marks an insertion point in the text. Used in review workflows
22093
+ * to indicate where new content should be added.
22094
+ */
22095
+ var PdfCaretAnnotation = class PdfCaretAnnotation extends require_pdfPage.PdfAnnotation {
22096
+ constructor(dict) {
22097
+ super("Caret", dict);
22098
+ }
22099
+ /**
22100
+ * Create a new caret annotation.
22101
+ *
22102
+ * @param options.symbol The caret symbol. Default: 'None'.
22103
+ * @param options.caretRect The inner rectangle (RD) that describes
22104
+ * the difference between the annotation rect and the actual caret
22105
+ * position. Format: [left, bottom, right, top] insets.
22106
+ */
22107
+ static create(options) {
22108
+ const annot = new PdfCaretAnnotation(require_pdfPage.buildAnnotationDict("Caret", options));
22109
+ if (options.symbol !== void 0) annot.setSymbol(options.symbol);
22110
+ if (options.caretRect !== void 0) annot.setCaretRect(options.caretRect);
22111
+ return annot;
22112
+ }
22113
+ /**
22114
+ * Create a PdfCaretAnnotation from an existing dictionary.
22115
+ */
22116
+ static fromDict(dict, _resolver) {
22117
+ return new PdfCaretAnnotation(dict);
22118
+ }
22119
+ /** Get the caret symbol. Defaults to 'None'. */
22120
+ getSymbol() {
22121
+ const obj = this.dict.get("/Sy");
22122
+ if (obj && obj.kind === "name") {
22123
+ if ((obj.value.startsWith("/") ? obj.value.slice(1) : obj.value) === "P") return "P";
22124
+ }
22125
+ return "None";
22126
+ }
22127
+ /** Set the caret symbol. */
22128
+ setSymbol(symbol) {
22129
+ this.dict.set("/Sy", require_pdfCatalog.PdfName.of(symbol));
22130
+ }
22131
+ /**
22132
+ * Get the inner rectangle differences (RD entry).
22133
+ * Returns [left, bottom, right, top] insets from the annotation rect.
22134
+ */
22135
+ getCaretRect() {
22136
+ const obj = this.dict.get("/RD");
22137
+ if (obj && obj.kind === "array" && obj.items.length === 4) return obj.items.map((item) => {
22138
+ if (item.kind === "number") return item.value;
22139
+ return 0;
22140
+ });
22141
+ }
22142
+ /** Set the inner rectangle differences (RD entry). */
22143
+ setCaretRect(rd) {
22144
+ this.dict.set("/RD", require_pdfCatalog.PdfArray.of(rd.map(require_pdfCatalog.PdfNumber.of)));
22145
+ }
22146
+ };
22147
+
22148
+ //#endregion
22149
+ //#region src/annotation/types/fileAttachmentAnnotation.ts
22150
+ /**
22151
+ * @module annotation/types/fileAttachmentAnnotation
22152
+ *
22153
+ * File attachment annotation — embeds a file as an inline annotation
22154
+ * on a page, displayed as a clickable icon.
22155
+ *
22156
+ * Unlike document-level attachments (via `attachFile()`), file attachment
22157
+ * annotations are positioned on a specific page and rendered as an icon
22158
+ * that users can click to open or save the embedded file.
22159
+ *
22160
+ * Reference: PDF 1.7 spec, Section 12.5.6.15 (File Attachment Annotations).
22161
+ */
22162
+ /**
22163
+ * A file attachment annotation (subtype /FileAttachment).
22164
+ *
22165
+ * Embeds a file directly in the annotation, rendered as a clickable
22166
+ * icon on the page. When the user clicks the icon, the PDF viewer
22167
+ * allows them to open or save the embedded file.
22168
+ */
22169
+ var PdfFileAttachmentAnnotation = class PdfFileAttachmentAnnotation extends require_pdfPage.PdfAnnotation {
22170
+ /** The raw file data to embed. */
22171
+ fileData;
22172
+ /** The filename to display. */
22173
+ fileName;
22174
+ /** Optional MIME type. */
22175
+ mimeType;
22176
+ /** Optional file description. */
22177
+ fileDescription;
22178
+ constructor(dict) {
22179
+ super("FileAttachment", dict);
22180
+ }
22181
+ /**
22182
+ * Create a new file attachment annotation.
22183
+ *
22184
+ * @param options.file The file data to embed.
22185
+ * @param options.fileName The filename (e.g., 'invoice.xml').
22186
+ * @param options.mimeType Optional MIME type (e.g., 'application/xml').
22187
+ * @param options.description Optional description of the file.
22188
+ * @param options.icon Icon to display. Default: 'GraphPushPin'.
22189
+ */
22190
+ static create(options) {
22191
+ const annot = new PdfFileAttachmentAnnotation(require_pdfPage.buildAnnotationDict("FileAttachment", options));
22192
+ annot.fileData = options.file;
22193
+ annot.fileName = options.fileName;
22194
+ annot.mimeType = options.mimeType;
22195
+ annot.fileDescription = options.description;
22196
+ if (options.icon !== void 0) annot.setIcon(options.icon);
22197
+ return annot;
22198
+ }
22199
+ /**
22200
+ * Create a PdfFileAttachmentAnnotation from an existing dictionary.
22201
+ */
22202
+ static fromDict(dict, _resolver) {
22203
+ return new PdfFileAttachmentAnnotation(dict);
22204
+ }
22205
+ /** Get the icon name. Defaults to 'GraphPushPin'. */
22206
+ getIcon() {
22207
+ const obj = this.dict.get("/Name");
22208
+ if (obj && obj.kind === "name") {
22209
+ const val = obj.value.startsWith("/") ? obj.value.slice(1) : obj.value;
22210
+ if ([
22211
+ "GraphPushPin",
22212
+ "PaperclipTag",
22213
+ "Paperclip",
22214
+ "Tag"
22215
+ ].includes(val)) return val;
22216
+ }
22217
+ return "GraphPushPin";
22218
+ }
22219
+ /** Set the icon name. */
22220
+ setIcon(icon) {
22221
+ this.dict.set("/Name", require_pdfCatalog.PdfName.of(icon));
22222
+ }
22223
+ /** Get the filename, if set. */
22224
+ getFileName() {
22225
+ const fs = this.dict.get("/FS");
22226
+ if (fs && fs.kind === "dict") {
22227
+ const uf = fs.get("/UF");
22228
+ if (uf && uf.kind === "string") return uf.value;
22229
+ const f = fs.get("/F");
22230
+ if (f && f.kind === "string") return f.value;
22231
+ }
22232
+ return this.fileName;
22233
+ }
22234
+ /**
22235
+ * Build the file specification dictionary and register the embedded
22236
+ * file stream. Call this before serializing the annotation.
22237
+ *
22238
+ * @param registry The document's object registry.
22239
+ * @returns The annotation dict with `/FS` referencing the file.
22240
+ */
22241
+ buildFileSpec(registry) {
22242
+ if (!this.fileData || !this.fileName) return this.dict;
22243
+ const efStreamDict = new require_pdfCatalog.PdfDict();
22244
+ efStreamDict.set("/Type", require_pdfCatalog.PdfName.of("EmbeddedFile"));
22245
+ if (this.mimeType) efStreamDict.set("/Subtype", require_pdfCatalog.PdfName.of(this.mimeType.replace("/", "#2F")));
22246
+ const params = new require_pdfCatalog.PdfDict();
22247
+ params.set("/Size", require_pdfCatalog.PdfNumber.of(this.fileData.length));
22248
+ efStreamDict.set("/Params", params);
22249
+ const efStream = new require_pdfCatalog.PdfStream(efStreamDict, this.fileData);
22250
+ const efRef = registry.register(efStream);
22251
+ const efDict = new require_pdfCatalog.PdfDict();
22252
+ efDict.set("/F", efRef);
22253
+ const fsDict = new require_pdfCatalog.PdfDict();
22254
+ fsDict.set("/Type", require_pdfCatalog.PdfName.of("Filespec"));
22255
+ fsDict.set("/F", require_pdfCatalog.PdfString.literal(this.fileName));
22256
+ fsDict.set("/UF", require_pdfCatalog.PdfString.literal(this.fileName));
22257
+ fsDict.set("/EF", efDict);
22258
+ if (this.fileDescription) fsDict.set("/Desc", require_pdfCatalog.PdfString.literal(this.fileDescription));
22259
+ const fsRef = registry.register(fsDict);
22260
+ this.dict.set("/FS", fsRef);
22261
+ return this.dict;
22262
+ }
22263
+ };
22264
+
21650
22265
  //#endregion
21651
22266
  //#region src/parser/textExtractor.ts
21652
22267
  /**
@@ -22445,6 +23060,19 @@ var TokenType = /* @__PURE__ */ function(TokenType) {
22445
23060
  return TokenType;
22446
23061
  }(TokenType || {});
22447
23062
  /**
23063
+ * `hexVal[b]` is the numeric value (0-15) of a hex character, or -1 if
23064
+ * the byte is not a valid hex digit.
23065
+ */
23066
+ const hexVal = /* @__PURE__ */ (() => {
23067
+ const t = new Int8Array(256).fill(-1);
23068
+ for (let i = 0; i <= 9; i++) t[48 + i] = i;
23069
+ for (let i = 0; i < 6; i++) {
23070
+ t[65 + i] = 10 + i;
23071
+ t[97 + i] = 10 + i;
23072
+ }
23073
+ return t;
23074
+ })();
23075
+ /**
22448
23076
  * Combined lexer + parser for PDF content streams.
22449
23077
  *
22450
23078
  * Content streams are simpler than full PDF object syntax — there are no
@@ -22563,20 +23191,22 @@ var ContentStreamLexer = class {
22563
23191
  }
22564
23192
  const dataStart = this.pos;
22565
23193
  let dataEnd = this.pos;
22566
- while (this.pos < this.data.length) {
22567
- if (this.isWhitespace(this.data[this.pos])) {
22568
- const wsPos = this.pos;
22569
- let probe = wsPos + 1;
22570
- if (probe + 1 < this.data.length && this.data[probe] === 69 && this.data[probe + 1] === 73) {
22571
- const afterEI = probe + 2;
22572
- if (afterEI >= this.data.length || this.isWhitespace(this.data[afterEI])) {
22573
- dataEnd = wsPos;
22574
- this.pos = afterEI;
22575
- break;
22576
- }
23194
+ let searchFrom = this.pos;
23195
+ while (searchFrom < this.data.length) {
23196
+ const eIdx = this.data.indexOf(69, searchFrom);
23197
+ if (eIdx === -1 || eIdx + 1 >= this.data.length) {
23198
+ this.pos = this.data.length;
23199
+ break;
23200
+ }
23201
+ if (eIdx > dataStart && this.isWhitespace(this.data[eIdx - 1]) && this.data[eIdx + 1] === 73) {
23202
+ const afterEI = eIdx + 2;
23203
+ if (afterEI >= this.data.length || this.isWhitespace(this.data[afterEI])) {
23204
+ dataEnd = eIdx - 1;
23205
+ this.pos = afterEI;
23206
+ break;
22577
23207
  }
22578
23208
  }
22579
- this.pos++;
23209
+ searchFrom = eIdx + 1;
22580
23210
  }
22581
23211
  return {
22582
23212
  dict,
@@ -22644,7 +23274,7 @@ var ContentStreamLexer = class {
22644
23274
  */
22645
23275
  readLiteralString() {
22646
23276
  this.pos++;
22647
- let result = "";
23277
+ const parts = [];
22648
23278
  let depth = 1;
22649
23279
  while (this.pos < this.data.length && depth > 0) {
22650
23280
  const ch = this.data[this.pos];
@@ -22654,35 +23284,35 @@ var ContentStreamLexer = class {
22654
23284
  const esc = this.data[this.pos];
22655
23285
  switch (esc) {
22656
23286
  case 110:
22657
- result += "\n";
23287
+ parts.push("\n");
22658
23288
  this.pos++;
22659
23289
  break;
22660
23290
  case 114:
22661
- result += "\r";
23291
+ parts.push("\r");
22662
23292
  this.pos++;
22663
23293
  break;
22664
23294
  case 116:
22665
- result += " ";
23295
+ parts.push(" ");
22666
23296
  this.pos++;
22667
23297
  break;
22668
23298
  case 98:
22669
- result += "\b";
23299
+ parts.push("\b");
22670
23300
  this.pos++;
22671
23301
  break;
22672
23302
  case 102:
22673
- result += "\f";
23303
+ parts.push("\f");
22674
23304
  this.pos++;
22675
23305
  break;
22676
23306
  case 40:
22677
- result += "(";
23307
+ parts.push("(");
22678
23308
  this.pos++;
22679
23309
  break;
22680
23310
  case 41:
22681
- result += ")";
23311
+ parts.push(")");
22682
23312
  this.pos++;
22683
23313
  break;
22684
23314
  case 92:
22685
- result += "\\";
23315
+ parts.push("\\");
22686
23316
  this.pos++;
22687
23317
  break;
22688
23318
  case 10:
@@ -22710,29 +23340,29 @@ var ContentStreamLexer = class {
22710
23340
  }
22711
23341
  }
22712
23342
  }
22713
- result += String.fromCharCode(octal & 255);
23343
+ parts.push(String.fromCharCode(octal & 255));
22714
23344
  } else {
22715
- result += String.fromCharCode(esc);
23345
+ parts.push(String.fromCharCode(esc));
22716
23346
  this.pos++;
22717
23347
  }
22718
23348
  break;
22719
23349
  }
22720
23350
  } else if (ch === 40) {
22721
23351
  depth++;
22722
- result += "(";
23352
+ parts.push("(");
22723
23353
  this.pos++;
22724
23354
  } else if (ch === 41) {
22725
23355
  depth--;
22726
- if (depth > 0) result += ")";
23356
+ if (depth > 0) parts.push(")");
22727
23357
  this.pos++;
22728
23358
  } else {
22729
- result += String.fromCharCode(ch);
23359
+ parts.push(String.fromCharCode(ch));
22730
23360
  this.pos++;
22731
23361
  }
22732
23362
  }
22733
23363
  return {
22734
23364
  type: TokenType.String,
22735
- value: result
23365
+ value: parts.join("")
22736
23366
  };
22737
23367
  }
22738
23368
  /**
@@ -22740,7 +23370,8 @@ var ContentStreamLexer = class {
22740
23370
  */
22741
23371
  readHexString() {
22742
23372
  this.pos++;
22743
- let hex = "";
23373
+ const bytes = [];
23374
+ let hi = -1;
22744
23375
  while (this.pos < this.data.length) {
22745
23376
  const ch = this.data[this.pos];
22746
23377
  if (ch === 62) {
@@ -22751,18 +23382,22 @@ var ContentStreamLexer = class {
22751
23382
  this.pos++;
22752
23383
  continue;
22753
23384
  }
22754
- hex += String.fromCharCode(ch);
23385
+ const v = hexVal[ch];
23386
+ if (v === -1) {
23387
+ this.pos++;
23388
+ continue;
23389
+ }
23390
+ if (hi === -1) hi = v;
23391
+ else {
23392
+ bytes.push(hi << 4 | v);
23393
+ hi = -1;
23394
+ }
22755
23395
  this.pos++;
22756
23396
  }
22757
- if (hex.length % 2 !== 0) hex += "0";
22758
- let result = "";
22759
- for (let i = 0; i < hex.length; i += 2) {
22760
- const byte = parseInt(hex.substring(i, i + 2), 16);
22761
- if (!isNaN(byte)) result += String.fromCharCode(byte);
22762
- }
23397
+ if (hi !== -1) bytes.push(hi << 4);
22763
23398
  return {
22764
23399
  type: TokenType.HexString,
22765
- value: result
23400
+ value: String.fromCharCode.apply(null, bytes)
22766
23401
  };
22767
23402
  }
22768
23403
  /**
@@ -22770,27 +23405,25 @@ var ContentStreamLexer = class {
22770
23405
  */
22771
23406
  readName() {
22772
23407
  this.pos++;
22773
- let name = "/";
23408
+ const parts = ["/"];
22774
23409
  while (this.pos < this.data.length) {
22775
23410
  const ch = this.data[this.pos];
22776
23411
  if (this.isWhitespace(ch) || this.isDelimiter(ch)) break;
22777
23412
  if (ch === 35 && this.pos + 2 < this.data.length) {
22778
- const hi = this.data[this.pos + 1];
22779
- const lo = this.data[this.pos + 2];
22780
- const hex = String.fromCharCode(hi) + String.fromCharCode(lo);
22781
- const code = parseInt(hex, 16);
22782
- if (!isNaN(code)) {
22783
- name += String.fromCharCode(code);
23413
+ const hi = hexVal[this.data[this.pos + 1]];
23414
+ const lo = hexVal[this.data[this.pos + 2]];
23415
+ if (hi !== -1 && lo !== -1) {
23416
+ parts.push(String.fromCharCode(hi << 4 | lo));
22784
23417
  this.pos += 3;
22785
23418
  continue;
22786
23419
  }
22787
23420
  }
22788
- name += String.fromCharCode(ch);
23421
+ parts.push(String.fromCharCode(ch));
22789
23422
  this.pos++;
22790
23423
  }
22791
23424
  return {
22792
23425
  type: TokenType.Name,
22793
- value: require_pdfCatalog.PdfName.of(name)
23426
+ value: require_pdfCatalog.PdfName.of(parts.join(""))
22794
23427
  };
22795
23428
  }
22796
23429
  /**
@@ -22884,9 +23517,7 @@ var ContentStreamLexer = class {
22884
23517
  * Decode a slice of the data as ASCII text.
22885
23518
  */
22886
23519
  decodeAscii(start, end) {
22887
- let s = "";
22888
- for (let i = start; i < end; i++) s += String.fromCharCode(this.data[i]);
22889
- return s;
23520
+ return String.fromCharCode.apply(null, this.data.subarray(start, end));
22890
23521
  }
22891
23522
  };
22892
23523
 
@@ -23714,6 +24345,339 @@ function addTrailerId(data) {
23714
24345
  return concatBytes(...parts);
23715
24346
  }
23716
24347
 
24348
+ //#endregion
24349
+ //#region src/assets/image/imageOptimize.ts
24350
+ /**
24351
+ * Downscale an image to fit within the specified dimensions.
24352
+ *
24353
+ * If the image is already smaller than the target dimensions, it is
24354
+ * returned unchanged.
24355
+ *
24356
+ * @param image - The raw image pixel data.
24357
+ * @param options - Downscaling options (target dimensions, algorithm).
24358
+ * @returns The downscaled image, or the original if no scaling needed.
24359
+ *
24360
+ * @example
24361
+ * ```ts
24362
+ * const result = downscaleImage(rawImage, {
24363
+ * maxWidth: 1024,
24364
+ * maxHeight: 768,
24365
+ * algorithm: 'bilinear',
24366
+ * });
24367
+ * ```
24368
+ */
24369
+ function downscaleImage(image, options = {}) {
24370
+ const target = computeTargetDimensions(image.width, image.height, options);
24371
+ if (target.width >= image.width && target.height >= image.height) return image;
24372
+ switch (options.algorithm ?? "bilinear") {
24373
+ case "nearest": return resampleNearest(image, target.width, target.height);
24374
+ case "bilinear": return resampleBilinear(image, target.width, target.height);
24375
+ case "lanczos": return resampleLanczos(image, target.width, target.height);
24376
+ default: return resampleBilinear(image, target.width, target.height);
24377
+ }
24378
+ }
24379
+ /**
24380
+ * Recompress raw image pixel data using the specified format.
24381
+ *
24382
+ * @param image - The raw image pixel data.
24383
+ * @param options - Recompression options (format, quality).
24384
+ * @returns The compressed image data.
24385
+ *
24386
+ * @example
24387
+ * ```ts
24388
+ * const result = await recompressImage(rawImage, {
24389
+ * format: 'deflate',
24390
+ * compressionLevel: 9,
24391
+ * });
24392
+ * ```
24393
+ */
24394
+ async function recompressImage(image, options = {}) {
24395
+ switch (options.format ?? "deflate") {
24396
+ case "deflate": return recompressDeflate(image, options.compressionLevel ?? 6);
24397
+ case "jpeg": return recompressJpeg(image, options.quality ?? 85);
24398
+ default: return {
24399
+ data: image.pixels,
24400
+ width: image.width,
24401
+ height: image.height,
24402
+ channels: image.channels,
24403
+ format: "raw",
24404
+ wasOptimized: false
24405
+ };
24406
+ }
24407
+ }
24408
+ /**
24409
+ * Run the full image optimization pipeline: downscale then recompress.
24410
+ *
24411
+ * @param image - The raw image pixel data.
24412
+ * @param options - Combined optimization options.
24413
+ * @returns The optimized result.
24414
+ */
24415
+ async function optimizeImage(image, options = {}) {
24416
+ if (options.skipBelowBytes && image.pixels.length < options.skipBelowBytes) return {
24417
+ data: image.pixels,
24418
+ width: image.width,
24419
+ height: image.height,
24420
+ channels: image.channels,
24421
+ format: "raw",
24422
+ wasOptimized: false
24423
+ };
24424
+ return recompressImage(downscaleImage(image, options), options);
24425
+ }
24426
+ /**
24427
+ * Compute target dimensions from options, preserving aspect ratio.
24428
+ * @internal
24429
+ */
24430
+ function computeTargetDimensions(srcWidth, srcHeight, options) {
24431
+ let targetWidth = srcWidth;
24432
+ let targetHeight = srcHeight;
24433
+ if (options.targetDpi && options.printWidth && options.printHeight) {
24434
+ const printWidthInches = options.printWidth / 72;
24435
+ const printHeightInches = options.printHeight / 72;
24436
+ const dpiWidth = Math.round(printWidthInches * options.targetDpi);
24437
+ const dpiHeight = Math.round(printHeightInches * options.targetDpi);
24438
+ targetWidth = Math.min(targetWidth, dpiWidth);
24439
+ targetHeight = Math.min(targetHeight, dpiHeight);
24440
+ }
24441
+ if (options.maxWidth && targetWidth > options.maxWidth) {
24442
+ const scale = options.maxWidth / targetWidth;
24443
+ targetWidth = options.maxWidth;
24444
+ targetHeight = Math.round(targetHeight * scale);
24445
+ }
24446
+ if (options.maxHeight && targetHeight > options.maxHeight) {
24447
+ const scale = options.maxHeight / targetHeight;
24448
+ targetHeight = options.maxHeight;
24449
+ targetWidth = Math.round(targetWidth * scale);
24450
+ }
24451
+ targetWidth = Math.max(1, targetWidth);
24452
+ targetHeight = Math.max(1, targetHeight);
24453
+ return {
24454
+ width: targetWidth,
24455
+ height: targetHeight
24456
+ };
24457
+ }
24458
+ /**
24459
+ * Nearest-neighbor resampling.
24460
+ * @internal
24461
+ */
24462
+ function resampleNearest(src, dstWidth, dstHeight) {
24463
+ const channels = src.channels;
24464
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24465
+ const xRatio = src.width / dstWidth;
24466
+ const yRatio = src.height / dstHeight;
24467
+ for (let y = 0; y < dstHeight; y++) {
24468
+ const srcY = Math.min(Math.floor(y * yRatio), src.height - 1);
24469
+ for (let x = 0; x < dstWidth; x++) {
24470
+ const srcX = Math.min(Math.floor(x * xRatio), src.width - 1);
24471
+ const srcIdx = (srcY * src.width + srcX) * channels;
24472
+ const dstIdx = (y * dstWidth + x) * channels;
24473
+ for (let c = 0; c < channels; c++) dst[dstIdx + c] = src.pixels[srcIdx + c];
24474
+ }
24475
+ }
24476
+ return {
24477
+ pixels: dst,
24478
+ width: dstWidth,
24479
+ height: dstHeight,
24480
+ channels,
24481
+ bitsPerChannel: src.bitsPerChannel
24482
+ };
24483
+ }
24484
+ /**
24485
+ * Bilinear interpolation resampling.
24486
+ * @internal
24487
+ */
24488
+ function resampleBilinear(src, dstWidth, dstHeight) {
24489
+ const channels = src.channels;
24490
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24491
+ const xRatio = (src.width - 1) / Math.max(1, dstWidth - 1);
24492
+ const yRatio = (src.height - 1) / Math.max(1, dstHeight - 1);
24493
+ for (let y = 0; y < dstHeight; y++) {
24494
+ const srcYf = y * yRatio;
24495
+ const srcY0 = Math.floor(srcYf);
24496
+ const srcY1 = Math.min(srcY0 + 1, src.height - 1);
24497
+ const yFrac = srcYf - srcY0;
24498
+ for (let x = 0; x < dstWidth; x++) {
24499
+ const srcXf = x * xRatio;
24500
+ const srcX0 = Math.floor(srcXf);
24501
+ const srcX1 = Math.min(srcX0 + 1, src.width - 1);
24502
+ const xFrac = srcXf - srcX0;
24503
+ const dstIdx = (y * dstWidth + x) * channels;
24504
+ for (let c = 0; c < channels; c++) {
24505
+ const topLeft = src.pixels[(srcY0 * src.width + srcX0) * channels + c];
24506
+ const topRight = src.pixels[(srcY0 * src.width + srcX1) * channels + c];
24507
+ const bottomLeft = src.pixels[(srcY1 * src.width + srcX0) * channels + c];
24508
+ const bottomRight = src.pixels[(srcY1 * src.width + srcX1) * channels + c];
24509
+ const top = topLeft + (topRight - topLeft) * xFrac;
24510
+ const value = top + (bottomLeft + (bottomRight - bottomLeft) * xFrac - top) * yFrac;
24511
+ dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, value)));
24512
+ }
24513
+ }
24514
+ }
24515
+ return {
24516
+ pixels: dst,
24517
+ width: dstWidth,
24518
+ height: dstHeight,
24519
+ channels,
24520
+ bitsPerChannel: src.bitsPerChannel
24521
+ };
24522
+ }
24523
+ /**
24524
+ * Lanczos kernel function.
24525
+ *
24526
+ * Computes the Lanczos windowed sinc value for a given distance `x`
24527
+ * and window size `a`. For Lanczos-3, `a = 3`.
24528
+ *
24529
+ * @param x - Distance from the center sample.
24530
+ * @param a - Window radius (3 for Lanczos-3).
24531
+ * @returns The kernel weight.
24532
+ * @internal
24533
+ */
24534
+ function lanczos(x, a = 3) {
24535
+ if (x === 0) return 1;
24536
+ if (Math.abs(x) >= a) return 0;
24537
+ const pix = Math.PI * x;
24538
+ return Math.sin(pix) / pix * (Math.sin(pix / a) / (pix / a));
24539
+ }
24540
+ /**
24541
+ * Lanczos-3 resampling.
24542
+ *
24543
+ * Uses a 6-tap (a=3) windowed sinc filter in both dimensions for
24544
+ * high-quality downscaling. This is the best quality option but
24545
+ * also the slowest.
24546
+ *
24547
+ * @internal
24548
+ */
24549
+ function resampleLanczos(src, dstWidth, dstHeight) {
24550
+ const channels = src.channels;
24551
+ const a = 3;
24552
+ const dst = new Uint8Array(dstWidth * dstHeight * channels);
24553
+ const xRatio = src.width / dstWidth;
24554
+ const yRatio = src.height / dstHeight;
24555
+ for (let y = 0; y < dstHeight; y++) {
24556
+ const srcYf = (y + .5) * yRatio - .5;
24557
+ for (let x = 0; x < dstWidth; x++) {
24558
+ const srcXf = (x + .5) * xRatio - .5;
24559
+ const dstIdx = (y * dstWidth + x) * channels;
24560
+ const sum = new Float64Array(channels);
24561
+ let weightSum = 0;
24562
+ const yStart = Math.floor(srcYf) - a + 1;
24563
+ const yEnd = Math.floor(srcYf) + a;
24564
+ const xStart = Math.floor(srcXf) - a + 1;
24565
+ const xEnd = Math.floor(srcXf) + a;
24566
+ for (let sy = yStart; sy <= yEnd; sy++) {
24567
+ const wy = lanczos(srcYf - sy, a);
24568
+ if (wy === 0) continue;
24569
+ const clampedY = Math.max(0, Math.min(src.height - 1, sy));
24570
+ for (let sx = xStart; sx <= xEnd; sx++) {
24571
+ const wx = lanczos(srcXf - sx, a);
24572
+ if (wx === 0) continue;
24573
+ const w = wx * wy;
24574
+ const clampedX = Math.max(0, Math.min(src.width - 1, sx));
24575
+ const srcIdx = (clampedY * src.width + clampedX) * channels;
24576
+ for (let c = 0; c < channels; c++) sum[c] = (sum[c] ?? 0) + src.pixels[srcIdx + c] * w;
24577
+ weightSum += w;
24578
+ }
24579
+ }
24580
+ if (weightSum > 0) for (let c = 0; c < channels; c++) dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, sum[c] / weightSum)));
24581
+ }
24582
+ }
24583
+ return {
24584
+ pixels: dst,
24585
+ width: dstWidth,
24586
+ height: dstHeight,
24587
+ channels,
24588
+ bitsPerChannel: src.bitsPerChannel
24589
+ };
24590
+ }
24591
+ /**
24592
+ * Recompress image data using deflate (for PDF FlateDecode).
24593
+ * @internal
24594
+ */
24595
+ async function recompressDeflate(image, level) {
24596
+ if (typeof CompressionStream !== "undefined") {
24597
+ const cs = new CompressionStream("deflate");
24598
+ const writer = cs.writable.getWriter();
24599
+ const reader = cs.readable.getReader();
24600
+ const chunks = [];
24601
+ writer.write(new Uint8Array(image.pixels)).catch(() => {});
24602
+ writer.close().catch(() => {});
24603
+ while (true) {
24604
+ const { done, value } = await reader.read();
24605
+ if (done) break;
24606
+ chunks.push(value);
24607
+ }
24608
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
24609
+ const result = new Uint8Array(totalLength);
24610
+ let pos = 0;
24611
+ for (const chunk of chunks) {
24612
+ result.set(chunk, pos);
24613
+ pos += chunk.length;
24614
+ }
24615
+ return {
24616
+ data: result,
24617
+ width: image.width,
24618
+ height: image.height,
24619
+ channels: image.channels,
24620
+ format: "deflate",
24621
+ wasOptimized: true
24622
+ };
24623
+ }
24624
+ try {
24625
+ const { deflateSync } = await import("fflate");
24626
+ return {
24627
+ data: deflateSync(image.pixels, { level }),
24628
+ width: image.width,
24629
+ height: image.height,
24630
+ channels: image.channels,
24631
+ format: "deflate",
24632
+ wasOptimized: true
24633
+ };
24634
+ } catch {
24635
+ return {
24636
+ data: image.pixels,
24637
+ width: image.width,
24638
+ height: image.height,
24639
+ channels: image.channels,
24640
+ format: "raw",
24641
+ wasOptimized: false
24642
+ };
24643
+ }
24644
+ }
24645
+ /**
24646
+ * Recompress image data as JPEG.
24647
+ * @internal
24648
+ */
24649
+ /**
24650
+ * Recompress image data as JPEG.
24651
+ *
24652
+ * JPEG encoding in pure JS is complex (DCT, Huffman coding, quantization).
24653
+ * A full implementation requires either:
24654
+ *
24655
+ * 1. **WASM-based encoder** (preferred) -- compile libjpeg-turbo or mozjpeg
24656
+ * to WASM, feed raw pixels, get JPEG bytes back.
24657
+ * 2. **Canvas API** (browser-only fallback) -- use `OffscreenCanvas` with
24658
+ * `convertToBlob({ type: 'image/jpeg', quality })`.
24659
+ * 3. **Pure JS encoder** (last resort) -- very slow but works everywhere.
24660
+ *
24661
+ * Until a WASM encoder module is bundled, this function returns the input
24662
+ * data unchanged. The caller receives `format: 'raw'` and
24663
+ * `wasOptimized: false` to indicate that JPEG encoding was not applied.
24664
+ *
24665
+ * @param image - The raw image pixel data.
24666
+ * @param quality - JPEG quality 1-100 (reserved for future use).
24667
+ * @returns The input data as-is, marked as un-optimized.
24668
+ * @internal
24669
+ */
24670
+ async function recompressJpeg(image, quality) {
24671
+ return {
24672
+ data: image.pixels,
24673
+ width: image.width,
24674
+ height: image.height,
24675
+ channels: image.channels,
24676
+ format: "raw",
24677
+ wasOptimized: false
24678
+ };
24679
+ }
24680
+
23717
24681
  //#endregion
23718
24682
  //#region src/errors.ts
23719
24683
  /**
@@ -23921,6 +24885,7 @@ exports.PdfAnnotation = require_pdfPage.PdfAnnotation;
23921
24885
  exports.PdfArray = require_pdfCatalog.PdfArray;
23922
24886
  exports.PdfBool = require_pdfCatalog.PdfBool;
23923
24887
  exports.PdfButtonField = PdfButtonField;
24888
+ exports.PdfCaretAnnotation = PdfCaretAnnotation;
23924
24889
  exports.PdfCheckboxField = PdfCheckboxField;
23925
24890
  exports.PdfCircleAnnotation = PdfCircleAnnotation;
23926
24891
  exports.PdfDict = require_pdfCatalog.PdfDict;
@@ -23928,6 +24893,7 @@ exports.PdfDocument = PdfDocument;
23928
24893
  exports.PdfDropdownField = PdfDropdownField;
23929
24894
  exports.PdfEncryptionHandler = PdfEncryptionHandler;
23930
24895
  exports.PdfField = PdfField;
24896
+ exports.PdfFileAttachmentAnnotation = PdfFileAttachmentAnnotation;
23931
24897
  exports.PdfForm = PdfForm;
23932
24898
  exports.PdfFreeTextAnnotation = PdfFreeTextAnnotation;
23933
24899
  exports.PdfHighlightAnnotation = PdfHighlightAnnotation;
@@ -23947,6 +24913,7 @@ exports.PdfPage = require_pdfPage.PdfPage;
23947
24913
  exports.PdfParseError = PdfParseError;
23948
24914
  exports.PdfPolyLineAnnotation = PdfPolyLineAnnotation;
23949
24915
  exports.PdfPolygonAnnotation = PdfPolygonAnnotation;
24916
+ exports.PdfPopupAnnotation = PdfPopupAnnotation;
23950
24917
  exports.PdfRadioGroup = PdfRadioGroup;
23951
24918
  exports.PdfRedactAnnotation = PdfRedactAnnotation;
23952
24919
  exports.PdfRef = require_pdfCatalog.PdfRef;
@@ -24033,6 +25000,7 @@ exports.decodePermissions = decodePermissions;
24033
25000
  exports.decodeStream = decodeStream;
24034
25001
  exports.degrees = require_pdfPage.degrees;
24035
25002
  exports.degreesToRadians = require_pdfPage.degreesToRadians;
25003
+ exports.downscaleImage = downscaleImage;
24036
25004
  exports.drawImageWithMatrix = require_pdfPage.drawImageWithMatrix;
24037
25005
  exports.drawImageXObject = require_pdfPage.drawImageXObject;
24038
25006
  exports.drawObject = require_pdfPage.drawXObject;
@@ -24110,6 +25078,7 @@ exports.moveTextOp = require_pdfPage.moveText;
24110
25078
  exports.moveTextSetLeading = require_pdfPage.moveTextSetLeading;
24111
25079
  exports.moveToOp = require_pdfPage.moveTo;
24112
25080
  exports.nextLineOp = require_pdfPage.nextLine;
25081
+ exports.optimizeImage = optimizeImage;
24113
25082
  exports.parseContentStream = parseContentStream;
24114
25083
  exports.parseSvg = require_pdfPage.parseSvg;
24115
25084
  exports.parseSvgColor = require_pdfPage.parseSvgColor;
@@ -24125,6 +25094,7 @@ exports.radialGradient = require_pdfPage.radialGradient;
24125
25094
  exports.radians = require_pdfPage.radians;
24126
25095
  exports.radiansToDegrees = require_pdfPage.radiansToDegrees;
24127
25096
  exports.rc4 = rc4;
25097
+ exports.recompressImage = recompressImage;
24128
25098
  exports.rectangleOp = require_pdfPage.rectangle;
24129
25099
  exports.removePage = removePage;
24130
25100
  exports.removePages = removePages;