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/README.md +13 -9
- package/dist/index.cjs +868 -37
- package/dist/index.d.cts +404 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +404 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +863 -38
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6307,15 +6307,76 @@ function padPassword(password) {
|
|
|
6307
6307
|
return result;
|
|
6308
6308
|
}
|
|
6309
6309
|
/**
|
|
6310
|
-
*
|
|
6311
|
-
*
|
|
6312
|
-
*
|
|
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.
|
|
6313
6322
|
*/
|
|
6314
6323
|
function preparePasswordV5(password) {
|
|
6315
|
-
const
|
|
6324
|
+
const prepared = saslprep(password);
|
|
6325
|
+
const encoded = new TextEncoder().encode(prepared);
|
|
6316
6326
|
return encoded.length > 127 ? encoded.subarray(0, 127) : encoded;
|
|
6317
6327
|
}
|
|
6318
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
|
+
/**
|
|
6319
6380
|
* Concatenate multiple Uint8Arrays into one.
|
|
6320
6381
|
*/
|
|
6321
6382
|
function concat$1(...arrays) {
|
|
@@ -6594,11 +6655,33 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
|
|
|
6594
6655
|
}
|
|
6595
6656
|
}
|
|
6596
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
|
+
/**
|
|
6597
6677
|
* Compute the file encryption key from a password and encryption dict.
|
|
6598
6678
|
*
|
|
6599
6679
|
* Tries the password as both user and owner password. Returns the key
|
|
6600
6680
|
* on the first successful match, or throws if neither works.
|
|
6601
6681
|
*
|
|
6682
|
+
* Results are cached so that re-opening the same PDF with the same
|
|
6683
|
+
* password skips the expensive key derivation.
|
|
6684
|
+
*
|
|
6602
6685
|
* @param password The password to try.
|
|
6603
6686
|
* @param dict Encryption dictionary values.
|
|
6604
6687
|
* @param fileId The first element of the /ID array (unused for R>=5).
|
|
@@ -6606,23 +6689,35 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
|
|
|
6606
6689
|
* @throws If the password is incorrect.
|
|
6607
6690
|
*/
|
|
6608
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;
|
|
6609
6696
|
if (dict.revision >= 6) {
|
|
6610
6697
|
const userKey = await computeEncryptionKeyR6(password, dict, false);
|
|
6611
|
-
if (userKey)
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
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) {
|
|
6617
6705
|
const userKey = await computeEncryptionKeyR5(password, dict, false);
|
|
6618
|
-
if (userKey)
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
if (await
|
|
6625
|
-
|
|
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;
|
|
6626
6721
|
}
|
|
6627
6722
|
/**
|
|
6628
6723
|
* Compute encryption key from raw password bytes (already padded/recovered).
|
|
@@ -6885,6 +6980,15 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
6885
6980
|
perms;
|
|
6886
6981
|
/** The file ID (first element of /ID array). */
|
|
6887
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();
|
|
6888
6992
|
constructor(params) {
|
|
6889
6993
|
this.fileKey = params.fileKey;
|
|
6890
6994
|
this.version = params.version;
|
|
@@ -7037,6 +7141,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
7037
7141
|
*/
|
|
7038
7142
|
deriveObjectKey(objNum, genNum) {
|
|
7039
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;
|
|
7040
7147
|
const extra = this.useAes ? 4 : 0;
|
|
7041
7148
|
const input = new Uint8Array(this.fileKey.length + 5 + extra);
|
|
7042
7149
|
input.set(this.fileKey, 0);
|
|
@@ -7054,7 +7161,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
7054
7161
|
}
|
|
7055
7162
|
const hash = md5(input);
|
|
7056
7163
|
const keyLen = Math.min(this.keyLengthBits / 8 + 5, 16);
|
|
7057
|
-
|
|
7164
|
+
const key = hash.subarray(0, keyLen);
|
|
7165
|
+
this.objectKeyCache.set(cacheKey, key);
|
|
7166
|
+
return key;
|
|
7058
7167
|
}
|
|
7059
7168
|
/**
|
|
7060
7169
|
* Encrypt raw data for a specific object.
|
|
@@ -10216,23 +10325,73 @@ function findStringForward(data, needle, startOffset) {
|
|
|
10216
10325
|
return -1;
|
|
10217
10326
|
}
|
|
10218
10327
|
/**
|
|
10219
|
-
*
|
|
10220
|
-
* via incremental update.
|
|
10328
|
+
* Build a PDF content stream for a visible signature appearance.
|
|
10221
10329
|
*
|
|
10222
|
-
*
|
|
10223
|
-
*
|
|
10224
|
-
* 2. Inserts an empty `/Contents` placeholder of the specified size
|
|
10225
|
-
* 3. Computes the `/ByteRange` that excludes the `/Contents` value
|
|
10330
|
+
* Renders a bordered rectangle with optional background color and
|
|
10331
|
+
* text lines rendered in Helvetica.
|
|
10226
10332
|
*
|
|
10227
|
-
*
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10333
|
+
* @internal
|
|
10334
|
+
*/
|
|
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
|
|
10234
10389
|
*/
|
|
10235
|
-
function
|
|
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) {
|
|
10236
10395
|
const pdfStr = decoder$4.decode(pdfBytes);
|
|
10237
10396
|
const startxrefIdx = pdfStr.lastIndexOf("startxref");
|
|
10238
10397
|
if (startxrefIdx === -1) throw new Error("Cannot find startxref in PDF — file may be corrupted");
|
|
@@ -10249,9 +10408,21 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
|
|
|
10249
10408
|
const infoMatch = pdfStr.match(/\/Info\s+(\d+)\s+(\d+)\s+R/);
|
|
10250
10409
|
const sigValueObjNum = originalSize;
|
|
10251
10410
|
const sigFieldObjNum = originalSize + 1;
|
|
10252
|
-
|
|
10411
|
+
let apStreamObjNum = -1;
|
|
10412
|
+
let newSize = originalSize + 2;
|
|
10413
|
+
if (appearance) {
|
|
10414
|
+
apStreamObjNum = newSize;
|
|
10415
|
+
newSize++;
|
|
10416
|
+
}
|
|
10253
10417
|
const sigDictStr = buildSignatureDictString(placeholderSize, signatureFieldName);
|
|
10254
|
-
|
|
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 += " >>";
|
|
10255
10426
|
let appendix = "\n";
|
|
10256
10427
|
const objOffsets = /* @__PURE__ */ new Map();
|
|
10257
10428
|
const sigValueStart = pdfBytes.length + encoder$3.encode(appendix).length;
|
|
@@ -10264,11 +10435,28 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
|
|
|
10264
10435
|
appendix += `${sigFieldObjNum} 0 obj\n`;
|
|
10265
10436
|
appendix += sigFieldDict;
|
|
10266
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
|
+
}
|
|
10267
10453
|
const xrefOffset = pdfBytes.length + encoder$3.encode(appendix).length;
|
|
10454
|
+
const objCount = appearance ? 3 : 2;
|
|
10268
10455
|
appendix += "xref\n";
|
|
10269
|
-
appendix += `${sigValueObjNum}
|
|
10456
|
+
appendix += `${sigValueObjNum} ${objCount}\n`;
|
|
10270
10457
|
appendix += `${sigValueStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10271
10458
|
appendix += `${sigFieldStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10459
|
+
if (appearance && apStreamStart >= 0) appendix += `${apStreamStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10272
10460
|
appendix += "trailer\n";
|
|
10273
10461
|
appendix += "<<\n";
|
|
10274
10462
|
appendix += `/Size ${newSize}\n`;
|
|
@@ -11058,7 +11246,32 @@ function parsePkcs7ForCert(pkcs7Bytes) {
|
|
|
11058
11246
|
*/
|
|
11059
11247
|
async function signPdf(pdfBytes, fieldName, options) {
|
|
11060
11248
|
const hashAlgorithm = options.hashAlgorithm ?? "SHA-256";
|
|
11061
|
-
|
|
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);
|
|
11062
11275
|
return embedSignature(preparedPdf, await buildPkcs7Signature(await computeSignatureHash(preparedPdf, byteRange.byteRange, hashAlgorithm), {
|
|
11063
11276
|
signerInfo: {
|
|
11064
11277
|
certificate: options.certificate,
|
|
@@ -18870,6 +19083,30 @@ var PdfDocument = class PdfDocument {
|
|
|
18870
19083
|
return imageRef;
|
|
18871
19084
|
}
|
|
18872
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
|
+
/**
|
|
18873
19110
|
* Embed pages from another PDF as Form XObjects.
|
|
18874
19111
|
*
|
|
18875
19112
|
* Each embedded page is turned into a self-contained Form XObject that
|
|
@@ -21770,6 +22007,261 @@ var PdfRedactAnnotation = class PdfRedactAnnotation extends require_pdfPage.PdfA
|
|
|
21770
22007
|
}
|
|
21771
22008
|
};
|
|
21772
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
|
+
|
|
21773
22265
|
//#endregion
|
|
21774
22266
|
//#region src/parser/textExtractor.ts
|
|
21775
22267
|
/**
|
|
@@ -23853,6 +24345,339 @@ function addTrailerId(data) {
|
|
|
23853
24345
|
return concatBytes(...parts);
|
|
23854
24346
|
}
|
|
23855
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
|
+
|
|
23856
24681
|
//#endregion
|
|
23857
24682
|
//#region src/errors.ts
|
|
23858
24683
|
/**
|
|
@@ -24060,6 +24885,7 @@ exports.PdfAnnotation = require_pdfPage.PdfAnnotation;
|
|
|
24060
24885
|
exports.PdfArray = require_pdfCatalog.PdfArray;
|
|
24061
24886
|
exports.PdfBool = require_pdfCatalog.PdfBool;
|
|
24062
24887
|
exports.PdfButtonField = PdfButtonField;
|
|
24888
|
+
exports.PdfCaretAnnotation = PdfCaretAnnotation;
|
|
24063
24889
|
exports.PdfCheckboxField = PdfCheckboxField;
|
|
24064
24890
|
exports.PdfCircleAnnotation = PdfCircleAnnotation;
|
|
24065
24891
|
exports.PdfDict = require_pdfCatalog.PdfDict;
|
|
@@ -24067,6 +24893,7 @@ exports.PdfDocument = PdfDocument;
|
|
|
24067
24893
|
exports.PdfDropdownField = PdfDropdownField;
|
|
24068
24894
|
exports.PdfEncryptionHandler = PdfEncryptionHandler;
|
|
24069
24895
|
exports.PdfField = PdfField;
|
|
24896
|
+
exports.PdfFileAttachmentAnnotation = PdfFileAttachmentAnnotation;
|
|
24070
24897
|
exports.PdfForm = PdfForm;
|
|
24071
24898
|
exports.PdfFreeTextAnnotation = PdfFreeTextAnnotation;
|
|
24072
24899
|
exports.PdfHighlightAnnotation = PdfHighlightAnnotation;
|
|
@@ -24086,6 +24913,7 @@ exports.PdfPage = require_pdfPage.PdfPage;
|
|
|
24086
24913
|
exports.PdfParseError = PdfParseError;
|
|
24087
24914
|
exports.PdfPolyLineAnnotation = PdfPolyLineAnnotation;
|
|
24088
24915
|
exports.PdfPolygonAnnotation = PdfPolygonAnnotation;
|
|
24916
|
+
exports.PdfPopupAnnotation = PdfPopupAnnotation;
|
|
24089
24917
|
exports.PdfRadioGroup = PdfRadioGroup;
|
|
24090
24918
|
exports.PdfRedactAnnotation = PdfRedactAnnotation;
|
|
24091
24919
|
exports.PdfRef = require_pdfCatalog.PdfRef;
|
|
@@ -24172,6 +25000,7 @@ exports.decodePermissions = decodePermissions;
|
|
|
24172
25000
|
exports.decodeStream = decodeStream;
|
|
24173
25001
|
exports.degrees = require_pdfPage.degrees;
|
|
24174
25002
|
exports.degreesToRadians = require_pdfPage.degreesToRadians;
|
|
25003
|
+
exports.downscaleImage = downscaleImage;
|
|
24175
25004
|
exports.drawImageWithMatrix = require_pdfPage.drawImageWithMatrix;
|
|
24176
25005
|
exports.drawImageXObject = require_pdfPage.drawImageXObject;
|
|
24177
25006
|
exports.drawObject = require_pdfPage.drawXObject;
|
|
@@ -24249,6 +25078,7 @@ exports.moveTextOp = require_pdfPage.moveText;
|
|
|
24249
25078
|
exports.moveTextSetLeading = require_pdfPage.moveTextSetLeading;
|
|
24250
25079
|
exports.moveToOp = require_pdfPage.moveTo;
|
|
24251
25080
|
exports.nextLineOp = require_pdfPage.nextLine;
|
|
25081
|
+
exports.optimizeImage = optimizeImage;
|
|
24252
25082
|
exports.parseContentStream = parseContentStream;
|
|
24253
25083
|
exports.parseSvg = require_pdfPage.parseSvg;
|
|
24254
25084
|
exports.parseSvgColor = require_pdfPage.parseSvgColor;
|
|
@@ -24264,6 +25094,7 @@ exports.radialGradient = require_pdfPage.radialGradient;
|
|
|
24264
25094
|
exports.radians = require_pdfPage.radians;
|
|
24265
25095
|
exports.radiansToDegrees = require_pdfPage.radiansToDegrees;
|
|
24266
25096
|
exports.rc4 = rc4;
|
|
25097
|
+
exports.recompressImage = recompressImage;
|
|
24267
25098
|
exports.rectangleOp = require_pdfPage.rectangle;
|
|
24268
25099
|
exports.removePage = removePage;
|
|
24269
25100
|
exports.removePages = removePages;
|