modern-pdf-lib 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -11
- package/dist/bridge-C7U4E7St.mjs +103 -0
- package/dist/bridge-DUcJFVsk.cjs +132 -0
- package/dist/index.cjs +1737 -91
- package/dist/index.d.cts +878 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +878 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1719 -92
- package/dist/{libdeflateWasm-DlHgU5oy.mjs → libdeflateWasm-82loOtIV.mjs} +2 -2
- package/dist/{libdeflateWasm-OkNoqBnO.cjs → libdeflateWasm-Enus0G1k.cjs} +2 -2
- package/dist/{loader-CQfoGFp9.mjs → loader-1VJXLlMZ.mjs} +3 -2
- package/dist/{loader-_fqS-TmT.cjs → loader-CKlBOHma.cjs} +3 -2
- package/dist/{pngEmbed-OYyOe_W0.cjs → pngEmbed-10m4CfBU.cjs} +2 -2
- package/dist/{pngEmbed-DTOqgEUC.mjs → pngEmbed-gaJ9S2Dk.mjs} +2 -2
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
const require_pdfPage = require('./pdfPage-DBfdinTR.cjs');
|
|
3
3
|
const require_pdfCatalog = require('./pdfCatalog-COKoYQ8C.cjs');
|
|
4
|
-
const require_libdeflateWasm = require('./libdeflateWasm-
|
|
4
|
+
const require_libdeflateWasm = require('./libdeflateWasm-Enus0G1k.cjs');
|
|
5
5
|
const require_fontSubset = require('./fontSubset-pFc8Dueu.cjs');
|
|
6
|
-
const require_pngEmbed = require('./pngEmbed-
|
|
6
|
+
const require_pngEmbed = require('./pngEmbed-10m4CfBU.cjs');
|
|
7
7
|
const require_fflateAdapter = require('./fflateAdapter-AHC_S3cb.cjs');
|
|
8
|
+
const require_bridge = require('./bridge-DUcJFVsk.cjs');
|
|
8
9
|
let fflate = require("fflate");
|
|
9
10
|
|
|
10
11
|
//#region src/core/pdfWriter.ts
|
|
@@ -4827,10 +4828,10 @@ async function tryLoadLibdeflate() {
|
|
|
4827
4828
|
if (libdeflateAttempted) return libdeflateEngine;
|
|
4828
4829
|
libdeflateAttempted = true;
|
|
4829
4830
|
try {
|
|
4830
|
-
const { LibdeflateWasm: LibdeflateCtor, initDeflateWasm } = await Promise.resolve().then(() => require("./libdeflateWasm-
|
|
4831
|
+
const { LibdeflateWasm: LibdeflateCtor, initDeflateWasm } = await Promise.resolve().then(() => require("./libdeflateWasm-Enus0G1k.cjs")).then((n) => n.libdeflateWasm_exports);
|
|
4831
4832
|
let customBytes;
|
|
4832
4833
|
try {
|
|
4833
|
-
const { getWasmLoaderConfig } = await Promise.resolve().then(() => require("./loader-
|
|
4834
|
+
const { getWasmLoaderConfig } = await Promise.resolve().then(() => require("./loader-CKlBOHma.cjs"));
|
|
4834
4835
|
customBytes = getWasmLoaderConfig().moduleBytes?.["libdeflate"];
|
|
4835
4836
|
} catch {}
|
|
4836
4837
|
await initDeflateWasm(customBytes);
|
|
@@ -6307,15 +6308,76 @@ function padPassword(password) {
|
|
|
6307
6308
|
return result;
|
|
6308
6309
|
}
|
|
6309
6310
|
/**
|
|
6310
|
-
*
|
|
6311
|
-
*
|
|
6312
|
-
*
|
|
6311
|
+
* Prepare a password for R=5/R=6 (AES-256) using SASLprep (RFC 4013).
|
|
6312
|
+
*
|
|
6313
|
+
* ISO 32000-2 SS7.6.3.1 requires SASLprep normalization for passwords
|
|
6314
|
+
* used with encryption revision 5 and 6. The steps are:
|
|
6315
|
+
*
|
|
6316
|
+
* 1. Map: Convert non-ASCII space characters to U+0020, remove
|
|
6317
|
+
* "commonly mapped to nothing" characters (RFC 3454 B.1).
|
|
6318
|
+
* 2. Normalize: Apply Unicode NFKC normalization.
|
|
6319
|
+
* 3. Prohibit: Reject characters from RFC 3454 prohibited tables.
|
|
6320
|
+
* 4. Bidi: Check bidirectional string rules.
|
|
6321
|
+
*
|
|
6322
|
+
* The result is truncated to 127 UTF-8 bytes per the PDF spec.
|
|
6313
6323
|
*/
|
|
6314
6324
|
function preparePasswordV5(password) {
|
|
6315
|
-
const
|
|
6325
|
+
const prepared = saslprep(password);
|
|
6326
|
+
const encoded = new TextEncoder().encode(prepared);
|
|
6316
6327
|
return encoded.length > 127 ? encoded.subarray(0, 127) : encoded;
|
|
6317
6328
|
}
|
|
6318
6329
|
/**
|
|
6330
|
+
* Simplified SASLprep (RFC 4013) profile of stringprep (RFC 3454).
|
|
6331
|
+
*
|
|
6332
|
+
* Covers the mapping, normalization, and prohibited-character steps
|
|
6333
|
+
* needed for PDF password preparation. Bidi checking is omitted
|
|
6334
|
+
* since PDF passwords are typically LTR and the spec allows
|
|
6335
|
+
* implementations to skip it.
|
|
6336
|
+
*
|
|
6337
|
+
* @internal
|
|
6338
|
+
*/
|
|
6339
|
+
function saslprep(input) {
|
|
6340
|
+
let mapped = "";
|
|
6341
|
+
for (const ch of input) {
|
|
6342
|
+
const cp = ch.codePointAt(0);
|
|
6343
|
+
if (isMappedToNothing(cp)) continue;
|
|
6344
|
+
if (isNonAsciiSpace(cp)) {
|
|
6345
|
+
mapped += " ";
|
|
6346
|
+
continue;
|
|
6347
|
+
}
|
|
6348
|
+
mapped += ch;
|
|
6349
|
+
}
|
|
6350
|
+
const normalized = mapped.normalize("NFKC");
|
|
6351
|
+
for (const ch of normalized) {
|
|
6352
|
+
const cp = ch.codePointAt(0);
|
|
6353
|
+
if (isProhibited(cp)) throw new Error(`Password contains prohibited character U+${cp.toString(16).toUpperCase().padStart(4, "0")} (SASLprep)`);
|
|
6354
|
+
}
|
|
6355
|
+
return normalized;
|
|
6356
|
+
}
|
|
6357
|
+
/** RFC 3454 Table B.1 — Commonly mapped to nothing. */
|
|
6358
|
+
function isMappedToNothing(cp) {
|
|
6359
|
+
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;
|
|
6360
|
+
}
|
|
6361
|
+
/** RFC 3454 Table C.1.2 — Non-ASCII space characters. */
|
|
6362
|
+
function isNonAsciiSpace(cp) {
|
|
6363
|
+
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;
|
|
6364
|
+
}
|
|
6365
|
+
/**
|
|
6366
|
+
* RFC 3454 prohibited tables (C.2.1, C.2.2, C.3, C.4, C.5, C.6, C.7, C.8, C.9).
|
|
6367
|
+
* Simplified to the ranges most likely to appear in passwords.
|
|
6368
|
+
*/
|
|
6369
|
+
function isProhibited(cp) {
|
|
6370
|
+
if (cp <= 31 || cp === 127) return true;
|
|
6371
|
+
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;
|
|
6372
|
+
if (cp >= 57344 && cp <= 63743 || cp >= 983040 && cp <= 1048573 || cp >= 1048576 && cp <= 1114109) return true;
|
|
6373
|
+
if (cp >= 64976 && cp <= 65007 || (cp & 65535) === 65534 || (cp & 65535) === 65535) return true;
|
|
6374
|
+
if (cp >= 55296 && cp <= 57343) return true;
|
|
6375
|
+
if (cp === 65529 || cp === 65530 || cp === 65531 || cp === 65532) return true;
|
|
6376
|
+
if (cp === 832 || cp === 833 || cp === 8206 || cp === 8207 || cp >= 8234 && cp <= 8238 || cp >= 8298 && cp <= 8303) return true;
|
|
6377
|
+
if (cp === 917505 || cp >= 917536 && cp <= 917631) return true;
|
|
6378
|
+
return false;
|
|
6379
|
+
}
|
|
6380
|
+
/**
|
|
6319
6381
|
* Concatenate multiple Uint8Arrays into one.
|
|
6320
6382
|
*/
|
|
6321
6383
|
function concat$1(...arrays) {
|
|
@@ -6594,11 +6656,33 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
|
|
|
6594
6656
|
}
|
|
6595
6657
|
}
|
|
6596
6658
|
/**
|
|
6659
|
+
* LRU cache for file encryption keys.
|
|
6660
|
+
*
|
|
6661
|
+
* Keyed on a hash derived from password + encryption dict parameters,
|
|
6662
|
+
* so re-opening the same PDF with the same password skips the expensive
|
|
6663
|
+
* key derivation (especially for R=6 which runs 64+ rounds of AES+SHA).
|
|
6664
|
+
*/
|
|
6665
|
+
const fileKeyCache = /* @__PURE__ */ new Map();
|
|
6666
|
+
const FILE_KEY_CACHE_MAX = 32;
|
|
6667
|
+
/**
|
|
6668
|
+
* Build a cache key string from the inputs that uniquely identify
|
|
6669
|
+
* a key derivation result.
|
|
6670
|
+
*/
|
|
6671
|
+
function buildCacheKey(password, dict, fileId) {
|
|
6672
|
+
const oHex = Array.from(dict.ownerKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6673
|
+
const uHex = Array.from(dict.userKey.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6674
|
+
const fHex = Array.from(fileId.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6675
|
+
return `${dict.revision}:${dict.permissions}:${password}:${oHex}:${uHex}:${fHex}`;
|
|
6676
|
+
}
|
|
6677
|
+
/**
|
|
6597
6678
|
* Compute the file encryption key from a password and encryption dict.
|
|
6598
6679
|
*
|
|
6599
6680
|
* Tries the password as both user and owner password. Returns the key
|
|
6600
6681
|
* on the first successful match, or throws if neither works.
|
|
6601
6682
|
*
|
|
6683
|
+
* Results are cached so that re-opening the same PDF with the same
|
|
6684
|
+
* password skips the expensive key derivation.
|
|
6685
|
+
*
|
|
6602
6686
|
* @param password The password to try.
|
|
6603
6687
|
* @param dict Encryption dictionary values.
|
|
6604
6688
|
* @param fileId The first element of the /ID array (unused for R>=5).
|
|
@@ -6606,23 +6690,35 @@ async function computeEncryptionKeyR6(password, dict, isOwner) {
|
|
|
6606
6690
|
* @throws If the password is incorrect.
|
|
6607
6691
|
*/
|
|
6608
6692
|
async function computeFileEncryptionKey(password, dict, fileId) {
|
|
6693
|
+
const ck = buildCacheKey(password, dict, fileId);
|
|
6694
|
+
const cached = fileKeyCache.get(ck);
|
|
6695
|
+
if (cached) return cached.slice();
|
|
6696
|
+
let result;
|
|
6609
6697
|
if (dict.revision >= 6) {
|
|
6610
6698
|
const userKey = await computeEncryptionKeyR6(password, dict, false);
|
|
6611
|
-
if (userKey)
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6699
|
+
if (userKey) result = userKey;
|
|
6700
|
+
else {
|
|
6701
|
+
const ownerKey = await computeEncryptionKeyR6(password, dict, true);
|
|
6702
|
+
if (ownerKey) result = ownerKey;
|
|
6703
|
+
else throw new Error("Incorrect password for R=6 encryption");
|
|
6704
|
+
}
|
|
6705
|
+
} else if (dict.revision === 5) {
|
|
6617
6706
|
const userKey = await computeEncryptionKeyR5(password, dict, false);
|
|
6618
|
-
if (userKey)
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
if (await
|
|
6625
|
-
|
|
6707
|
+
if (userKey) result = userKey;
|
|
6708
|
+
else {
|
|
6709
|
+
const ownerKey = await computeEncryptionKeyR5(password, dict, true);
|
|
6710
|
+
if (ownerKey) result = ownerKey;
|
|
6711
|
+
else throw new Error("Incorrect password for R=5 encryption");
|
|
6712
|
+
}
|
|
6713
|
+
} else if (await verifyUserPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4(password, dict, fileId);
|
|
6714
|
+
else if (await verifyOwnerPassword(password, dict, fileId)) result = computeEncryptionKeyR2R4Bytes(recoverUserKeyFromOwner(password, dict), dict, fileId);
|
|
6715
|
+
else throw new Error("Incorrect password");
|
|
6716
|
+
if (fileKeyCache.size >= FILE_KEY_CACHE_MAX) {
|
|
6717
|
+
const firstKey = fileKeyCache.keys().next().value;
|
|
6718
|
+
if (firstKey !== void 0) fileKeyCache.delete(firstKey);
|
|
6719
|
+
}
|
|
6720
|
+
fileKeyCache.set(ck, result.slice());
|
|
6721
|
+
return result;
|
|
6626
6722
|
}
|
|
6627
6723
|
/**
|
|
6628
6724
|
* Compute encryption key from raw password bytes (already padded/recovered).
|
|
@@ -6885,6 +6981,15 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
6885
6981
|
perms;
|
|
6886
6982
|
/** The file ID (first element of /ID array). */
|
|
6887
6983
|
fileId;
|
|
6984
|
+
/**
|
|
6985
|
+
* Cache for per-object derived keys (V=1-4 only).
|
|
6986
|
+
* Key: `(objNum << 16) | genNum` — unique integer per object.
|
|
6987
|
+
* Value: the derived encryption key.
|
|
6988
|
+
*
|
|
6989
|
+
* Avoids recomputing MD5(fileKey + objNum + genNum [+ sAlT]) for
|
|
6990
|
+
* every string and stream in the same object.
|
|
6991
|
+
*/
|
|
6992
|
+
objectKeyCache = /* @__PURE__ */ new Map();
|
|
6888
6993
|
constructor(params) {
|
|
6889
6994
|
this.fileKey = params.fileKey;
|
|
6890
6995
|
this.version = params.version;
|
|
@@ -7037,6 +7142,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
7037
7142
|
*/
|
|
7038
7143
|
deriveObjectKey(objNum, genNum) {
|
|
7039
7144
|
if (this.version === 5) return this.fileKey;
|
|
7145
|
+
const cacheKey = objNum << 16 | genNum;
|
|
7146
|
+
const cached = this.objectKeyCache.get(cacheKey);
|
|
7147
|
+
if (cached) return cached;
|
|
7040
7148
|
const extra = this.useAes ? 4 : 0;
|
|
7041
7149
|
const input = new Uint8Array(this.fileKey.length + 5 + extra);
|
|
7042
7150
|
input.set(this.fileKey, 0);
|
|
@@ -7054,7 +7162,9 @@ var PdfEncryptionHandler = class PdfEncryptionHandler {
|
|
|
7054
7162
|
}
|
|
7055
7163
|
const hash = md5(input);
|
|
7056
7164
|
const keyLen = Math.min(this.keyLengthBits / 8 + 5, 16);
|
|
7057
|
-
|
|
7165
|
+
const key = hash.subarray(0, keyLen);
|
|
7166
|
+
this.objectKeyCache.set(cacheKey, key);
|
|
7167
|
+
return key;
|
|
7058
7168
|
}
|
|
7059
7169
|
/**
|
|
7060
7170
|
* Encrypt raw data for a specific object.
|
|
@@ -9882,7 +9992,7 @@ function isAccessible(issues) {
|
|
|
9882
9992
|
* @param data The bytes to hash.
|
|
9883
9993
|
* @returns A hex string hash.
|
|
9884
9994
|
*/
|
|
9885
|
-
function hashBytes(data) {
|
|
9995
|
+
function hashBytes$1(data) {
|
|
9886
9996
|
let hash = 2166136261;
|
|
9887
9997
|
for (let i = 0; i < data.length; i++) {
|
|
9888
9998
|
hash ^= data[i];
|
|
@@ -9974,7 +10084,7 @@ function remapRef$1(sourceRef, context) {
|
|
|
9974
10084
|
}
|
|
9975
10085
|
let streamHash;
|
|
9976
10086
|
if (sourceObj.kind === "stream") {
|
|
9977
|
-
streamHash = hashBytes(sourceObj.data);
|
|
10087
|
+
streamHash = hashBytes$1(sourceObj.data);
|
|
9978
10088
|
const dedup = context.hashMap.get(streamHash);
|
|
9979
10089
|
if (dedup) {
|
|
9980
10090
|
context.refMap.set(sourceRef.objectNumber, dedup);
|
|
@@ -10216,23 +10326,73 @@ function findStringForward(data, needle, startOffset) {
|
|
|
10216
10326
|
return -1;
|
|
10217
10327
|
}
|
|
10218
10328
|
/**
|
|
10219
|
-
*
|
|
10220
|
-
* via incremental update.
|
|
10221
|
-
*
|
|
10222
|
-
* This function:
|
|
10223
|
-
* 1. Appends a new signature field and value to the PDF
|
|
10224
|
-
* 2. Inserts an empty `/Contents` placeholder of the specified size
|
|
10225
|
-
* 3. Computes the `/ByteRange` that excludes the `/Contents` value
|
|
10329
|
+
* Build a PDF content stream for a visible signature appearance.
|
|
10226
10330
|
*
|
|
10227
|
-
*
|
|
10228
|
-
*
|
|
10331
|
+
* Renders a bordered rectangle with optional background color and
|
|
10332
|
+
* text lines rendered in Helvetica.
|
|
10229
10333
|
*
|
|
10230
|
-
* @
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10334
|
+
* @internal
|
|
10335
|
+
*/
|
|
10336
|
+
function buildSignatureAppearanceStream(options) {
|
|
10337
|
+
const [, , w, h] = options.rect;
|
|
10338
|
+
const fontSize = options.fontSize ?? 10;
|
|
10339
|
+
const borderWidth = options.borderWidth ?? 1;
|
|
10340
|
+
const borderColor = options.borderColor ?? [
|
|
10341
|
+
0,
|
|
10342
|
+
0,
|
|
10343
|
+
0
|
|
10344
|
+
];
|
|
10345
|
+
const bgColor = options.backgroundColor;
|
|
10346
|
+
const lines = options.textLines;
|
|
10347
|
+
const ops = [];
|
|
10348
|
+
ops.push("q");
|
|
10349
|
+
if (bgColor) {
|
|
10350
|
+
ops.push(`${n$5(bgColor[0])} ${n$5(bgColor[1])} ${n$5(bgColor[2])} rg`);
|
|
10351
|
+
ops.push(`0 0 ${n$5(w)} ${n$5(h)} re`);
|
|
10352
|
+
ops.push("f");
|
|
10353
|
+
}
|
|
10354
|
+
if (borderWidth > 0) {
|
|
10355
|
+
ops.push(`${n$5(borderColor[0])} ${n$5(borderColor[1])} ${n$5(borderColor[2])} RG`);
|
|
10356
|
+
ops.push(`${n$5(borderWidth)} w`);
|
|
10357
|
+
const bw2 = borderWidth / 2;
|
|
10358
|
+
ops.push(`${n$5(bw2)} ${n$5(bw2)} ${n$5(w - borderWidth)} ${n$5(h - borderWidth)} re`);
|
|
10359
|
+
ops.push("S");
|
|
10360
|
+
}
|
|
10361
|
+
if (lines.length > 0) {
|
|
10362
|
+
const margin = borderWidth + 4;
|
|
10363
|
+
const lineHeight = fontSize * 1.2;
|
|
10364
|
+
ops.push("BT");
|
|
10365
|
+
ops.push(`/F1 ${n$5(fontSize)} Tf`);
|
|
10366
|
+
ops.push("0 0 0 rg");
|
|
10367
|
+
const startY = h - margin - fontSize;
|
|
10368
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10369
|
+
const y = startY - i * lineHeight;
|
|
10370
|
+
if (y < margin) break;
|
|
10371
|
+
ops.push(`${n$5(margin)} ${n$5(y)} Td`);
|
|
10372
|
+
ops.push(`(${escapePdfString(lines[i])}) Tj`);
|
|
10373
|
+
ops.push(`${n$5(-margin)} ${n$5(-y)} Td`);
|
|
10374
|
+
}
|
|
10375
|
+
ops.push("ET");
|
|
10376
|
+
}
|
|
10377
|
+
ops.push("Q");
|
|
10378
|
+
return ops.join("\n");
|
|
10379
|
+
}
|
|
10380
|
+
/**
|
|
10381
|
+
* Escape a string for use inside a PDF literal string `(...)`.
|
|
10382
|
+
* @internal
|
|
10383
|
+
*/
|
|
10384
|
+
function escapePdfString(str) {
|
|
10385
|
+
return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
10386
|
+
}
|
|
10387
|
+
/**
|
|
10388
|
+
* Format a number for PDF operators (max 6 decimal places, no trailing zeros).
|
|
10389
|
+
* @internal
|
|
10234
10390
|
*/
|
|
10235
|
-
function
|
|
10391
|
+
function n$5(value) {
|
|
10392
|
+
if (Number.isInteger(value)) return value.toString();
|
|
10393
|
+
return value.toFixed(6).replace(/\.?0+$/, "");
|
|
10394
|
+
}
|
|
10395
|
+
function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192, appearance) {
|
|
10236
10396
|
const pdfStr = decoder$4.decode(pdfBytes);
|
|
10237
10397
|
const startxrefIdx = pdfStr.lastIndexOf("startxref");
|
|
10238
10398
|
if (startxrefIdx === -1) throw new Error("Cannot find startxref in PDF — file may be corrupted");
|
|
@@ -10249,9 +10409,21 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
|
|
|
10249
10409
|
const infoMatch = pdfStr.match(/\/Info\s+(\d+)\s+(\d+)\s+R/);
|
|
10250
10410
|
const sigValueObjNum = originalSize;
|
|
10251
10411
|
const sigFieldObjNum = originalSize + 1;
|
|
10252
|
-
|
|
10412
|
+
let apStreamObjNum = -1;
|
|
10413
|
+
let newSize = originalSize + 2;
|
|
10414
|
+
if (appearance) {
|
|
10415
|
+
apStreamObjNum = newSize;
|
|
10416
|
+
newSize++;
|
|
10417
|
+
}
|
|
10253
10418
|
const sigDictStr = buildSignatureDictString(placeholderSize, signatureFieldName);
|
|
10254
|
-
|
|
10419
|
+
let rectStr = "0 0 0 0";
|
|
10420
|
+
if (appearance) {
|
|
10421
|
+
const [x, y, w, h] = appearance.rect;
|
|
10422
|
+
rectStr = `${x} ${y} ${x + w} ${y + h}`;
|
|
10423
|
+
}
|
|
10424
|
+
let sigFieldDict = `<< /Type /Annot /Subtype /Widget /FT /Sig /T (${signatureFieldName}) /V ${sigValueObjNum} 0 R /F 132 /Rect [${rectStr}]`;
|
|
10425
|
+
if (appearance && apStreamObjNum >= 0) sigFieldDict += ` /AP << /N ${apStreamObjNum} 0 R >>`;
|
|
10426
|
+
sigFieldDict += " >>";
|
|
10255
10427
|
let appendix = "\n";
|
|
10256
10428
|
const objOffsets = /* @__PURE__ */ new Map();
|
|
10257
10429
|
const sigValueStart = pdfBytes.length + encoder$3.encode(appendix).length;
|
|
@@ -10264,11 +10436,28 @@ function prepareForSigning(pdfBytes, signatureFieldName, placeholderSize = 8192)
|
|
|
10264
10436
|
appendix += `${sigFieldObjNum} 0 obj\n`;
|
|
10265
10437
|
appendix += sigFieldDict;
|
|
10266
10438
|
appendix += `\nendobj\n`;
|
|
10439
|
+
let apStreamStart = -1;
|
|
10440
|
+
if (appearance && apStreamObjNum >= 0) {
|
|
10441
|
+
apStreamStart = pdfBytes.length + encoder$3.encode(appendix).length;
|
|
10442
|
+
objOffsets.set(apStreamObjNum, apStreamStart);
|
|
10443
|
+
const apContent = buildSignatureAppearanceStream(appearance);
|
|
10444
|
+
const [, , w, h] = appearance.rect;
|
|
10445
|
+
appendix += `${apStreamObjNum} 0 obj\n`;
|
|
10446
|
+
appendix += `<< /Type /XObject /Subtype /Form /BBox [0 0 ${w} ${h}]`;
|
|
10447
|
+
appendix += ` /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>`;
|
|
10448
|
+
appendix += ` /Length ${apContent.length} >>\n`;
|
|
10449
|
+
appendix += `stream\n`;
|
|
10450
|
+
appendix += apContent;
|
|
10451
|
+
appendix += `\nendstream\n`;
|
|
10452
|
+
appendix += `endobj\n`;
|
|
10453
|
+
}
|
|
10267
10454
|
const xrefOffset = pdfBytes.length + encoder$3.encode(appendix).length;
|
|
10455
|
+
const objCount = appearance ? 3 : 2;
|
|
10268
10456
|
appendix += "xref\n";
|
|
10269
|
-
appendix += `${sigValueObjNum}
|
|
10457
|
+
appendix += `${sigValueObjNum} ${objCount}\n`;
|
|
10270
10458
|
appendix += `${sigValueStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10271
10459
|
appendix += `${sigFieldStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10460
|
+
if (appearance && apStreamStart >= 0) appendix += `${apStreamStart.toString().padStart(10, "0")} 00000 n \n`;
|
|
10272
10461
|
appendix += "trailer\n";
|
|
10273
10462
|
appendix += "<<\n";
|
|
10274
10463
|
appendix += `/Size ${newSize}\n`;
|
|
@@ -11058,7 +11247,32 @@ function parsePkcs7ForCert(pkcs7Bytes) {
|
|
|
11058
11247
|
*/
|
|
11059
11248
|
async function signPdf(pdfBytes, fieldName, options) {
|
|
11060
11249
|
const hashAlgorithm = options.hashAlgorithm ?? "SHA-256";
|
|
11061
|
-
|
|
11250
|
+
let prepareAppearance;
|
|
11251
|
+
if (options.appearance) {
|
|
11252
|
+
const ap = options.appearance;
|
|
11253
|
+
let textLines = ap.text;
|
|
11254
|
+
if (!textLines) {
|
|
11255
|
+
textLines = [];
|
|
11256
|
+
try {
|
|
11257
|
+
const { subjectCN } = extractIssuerAndSerial(options.certificate);
|
|
11258
|
+
if (subjectCN) textLines.push(`Signed by: ${subjectCN}`);
|
|
11259
|
+
} catch {
|
|
11260
|
+
textLines.push("Digitally Signed");
|
|
11261
|
+
}
|
|
11262
|
+
if (options.reason) textLines.push(`Reason: ${options.reason}`);
|
|
11263
|
+
if (options.location) textLines.push(`Location: ${options.location}`);
|
|
11264
|
+
textLines.push(`Date: ${(/* @__PURE__ */ new Date()).toISOString().substring(0, 10)}`);
|
|
11265
|
+
}
|
|
11266
|
+
prepareAppearance = {
|
|
11267
|
+
rect: ap.rect,
|
|
11268
|
+
textLines,
|
|
11269
|
+
fontSize: ap.fontSize,
|
|
11270
|
+
backgroundColor: ap.backgroundColor,
|
|
11271
|
+
borderColor: ap.borderColor,
|
|
11272
|
+
borderWidth: ap.borderWidth
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
11275
|
+
const { preparedPdf, byteRange } = prepareForSigning(pdfBytes, fieldName, 8192, prepareAppearance);
|
|
11062
11276
|
return embedSignature(preparedPdf, await buildPkcs7Signature(await computeSignatureHash(preparedPdf, byteRange.byteRange, hashAlgorithm), {
|
|
11063
11277
|
signerInfo: {
|
|
11064
11278
|
certificate: options.certificate,
|
|
@@ -18870,6 +19084,30 @@ var PdfDocument = class PdfDocument {
|
|
|
18870
19084
|
return imageRef;
|
|
18871
19085
|
}
|
|
18872
19086
|
/**
|
|
19087
|
+
* Embed an image, auto-detecting the format from file headers.
|
|
19088
|
+
*
|
|
19089
|
+
* Inspects the first bytes to determine whether the data is PNG or JPEG,
|
|
19090
|
+
* then delegates to {@link embedPng} or {@link embedJpeg} accordingly.
|
|
19091
|
+
*
|
|
19092
|
+
* @param imageData Raw image file bytes (PNG or JPEG).
|
|
19093
|
+
* @returns An {@link ImageRef} to pass to `page.drawImage()`.
|
|
19094
|
+
* @throws If the image format cannot be detected.
|
|
19095
|
+
*
|
|
19096
|
+
* @example
|
|
19097
|
+
* ```ts
|
|
19098
|
+
* const bytes = new Uint8Array(await readFile('photo.jpg'));
|
|
19099
|
+
* const image = await pdf.embedImage(bytes);
|
|
19100
|
+
* page.drawImage(image, { x: 50, y: 400, width: 200, height: 150 });
|
|
19101
|
+
* ```
|
|
19102
|
+
*/
|
|
19103
|
+
async embedImage(imageData) {
|
|
19104
|
+
const data = imageData instanceof ArrayBuffer ? new Uint8Array(imageData) : imageData;
|
|
19105
|
+
if (data.length < 4) throw new Error("Image data too short to detect format");
|
|
19106
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return this.embedPng(data);
|
|
19107
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return this.embedJpeg(data);
|
|
19108
|
+
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(" ")}.`);
|
|
19109
|
+
}
|
|
19110
|
+
/**
|
|
18873
19111
|
* Embed pages from another PDF as Form XObjects.
|
|
18874
19112
|
*
|
|
18875
19113
|
* Each embedded page is turned into a self-contained Form XObject that
|
|
@@ -21770,6 +22008,261 @@ var PdfRedactAnnotation = class PdfRedactAnnotation extends require_pdfPage.PdfA
|
|
|
21770
22008
|
}
|
|
21771
22009
|
};
|
|
21772
22010
|
|
|
22011
|
+
//#endregion
|
|
22012
|
+
//#region src/annotation/types/popupAnnotation.ts
|
|
22013
|
+
/**
|
|
22014
|
+
* @module annotation/types/popupAnnotation
|
|
22015
|
+
*
|
|
22016
|
+
* Popup annotation — a floating window that displays the text of its
|
|
22017
|
+
* parent annotation (typically a text/sticky note annotation).
|
|
22018
|
+
*
|
|
22019
|
+
* Popup annotations have no appearance of their own; the PDF viewer
|
|
22020
|
+
* renders them as a resizable window near the parent annotation.
|
|
22021
|
+
*
|
|
22022
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.14 (Popup Annotations).
|
|
22023
|
+
*/
|
|
22024
|
+
/**
|
|
22025
|
+
* A popup annotation (subtype /Popup).
|
|
22026
|
+
*
|
|
22027
|
+
* Displays a floating window containing the text of its parent
|
|
22028
|
+
* annotation. The parent annotation references this popup via its
|
|
22029
|
+
* `/Popup` entry, and this popup references its parent via `/Parent`.
|
|
22030
|
+
*/
|
|
22031
|
+
var PdfPopupAnnotation = class PdfPopupAnnotation extends require_pdfPage.PdfAnnotation {
|
|
22032
|
+
constructor(dict) {
|
|
22033
|
+
super("Popup", dict);
|
|
22034
|
+
}
|
|
22035
|
+
/**
|
|
22036
|
+
* Create a new popup annotation.
|
|
22037
|
+
*
|
|
22038
|
+
* @param options.open Whether the popup is initially open. Default: false.
|
|
22039
|
+
* @param options.parent Reference to the parent annotation (set after registration).
|
|
22040
|
+
*/
|
|
22041
|
+
static create(options) {
|
|
22042
|
+
const annot = new PdfPopupAnnotation(require_pdfPage.buildAnnotationDict("Popup", options));
|
|
22043
|
+
if (options.open !== void 0) annot.setOpen(options.open);
|
|
22044
|
+
return annot;
|
|
22045
|
+
}
|
|
22046
|
+
/**
|
|
22047
|
+
* Create a PdfPopupAnnotation from an existing dictionary.
|
|
22048
|
+
*/
|
|
22049
|
+
static fromDict(dict, _resolver) {
|
|
22050
|
+
return new PdfPopupAnnotation(dict);
|
|
22051
|
+
}
|
|
22052
|
+
/** Whether the popup is initially open. */
|
|
22053
|
+
isOpen() {
|
|
22054
|
+
const obj = this.dict.get("/Open");
|
|
22055
|
+
if (obj && obj.kind === "bool") return obj.value;
|
|
22056
|
+
return false;
|
|
22057
|
+
}
|
|
22058
|
+
/** Set the initial open state. */
|
|
22059
|
+
setOpen(open) {
|
|
22060
|
+
this.dict.set("/Open", require_pdfCatalog.PdfBool.of(open));
|
|
22061
|
+
}
|
|
22062
|
+
/**
|
|
22063
|
+
* Set the parent annotation reference.
|
|
22064
|
+
* The parent is the annotation whose text this popup displays.
|
|
22065
|
+
*/
|
|
22066
|
+
setParent(parentRef) {
|
|
22067
|
+
this.dict.set("/Parent", parentRef);
|
|
22068
|
+
}
|
|
22069
|
+
/** Get the parent annotation reference, if set. */
|
|
22070
|
+
getParent() {
|
|
22071
|
+
const obj = this.dict.get("/Parent");
|
|
22072
|
+
if (obj && obj.kind === "ref") return obj;
|
|
22073
|
+
}
|
|
22074
|
+
};
|
|
22075
|
+
|
|
22076
|
+
//#endregion
|
|
22077
|
+
//#region src/annotation/types/caretAnnotation.ts
|
|
22078
|
+
/**
|
|
22079
|
+
* @module annotation/types/caretAnnotation
|
|
22080
|
+
*
|
|
22081
|
+
* Caret annotation — marks a text insertion point in the document.
|
|
22082
|
+
*
|
|
22083
|
+
* A caret annotation indicates where text should be inserted. It is
|
|
22084
|
+
* typically used in document review workflows to suggest additions.
|
|
22085
|
+
* The annotation renders as a caret (^) symbol at the specified
|
|
22086
|
+
* location.
|
|
22087
|
+
*
|
|
22088
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.11 (Caret Annotations).
|
|
22089
|
+
*/
|
|
22090
|
+
/**
|
|
22091
|
+
* A caret annotation (subtype /Caret).
|
|
22092
|
+
*
|
|
22093
|
+
* Marks an insertion point in the text. Used in review workflows
|
|
22094
|
+
* to indicate where new content should be added.
|
|
22095
|
+
*/
|
|
22096
|
+
var PdfCaretAnnotation = class PdfCaretAnnotation extends require_pdfPage.PdfAnnotation {
|
|
22097
|
+
constructor(dict) {
|
|
22098
|
+
super("Caret", dict);
|
|
22099
|
+
}
|
|
22100
|
+
/**
|
|
22101
|
+
* Create a new caret annotation.
|
|
22102
|
+
*
|
|
22103
|
+
* @param options.symbol The caret symbol. Default: 'None'.
|
|
22104
|
+
* @param options.caretRect The inner rectangle (RD) that describes
|
|
22105
|
+
* the difference between the annotation rect and the actual caret
|
|
22106
|
+
* position. Format: [left, bottom, right, top] insets.
|
|
22107
|
+
*/
|
|
22108
|
+
static create(options) {
|
|
22109
|
+
const annot = new PdfCaretAnnotation(require_pdfPage.buildAnnotationDict("Caret", options));
|
|
22110
|
+
if (options.symbol !== void 0) annot.setSymbol(options.symbol);
|
|
22111
|
+
if (options.caretRect !== void 0) annot.setCaretRect(options.caretRect);
|
|
22112
|
+
return annot;
|
|
22113
|
+
}
|
|
22114
|
+
/**
|
|
22115
|
+
* Create a PdfCaretAnnotation from an existing dictionary.
|
|
22116
|
+
*/
|
|
22117
|
+
static fromDict(dict, _resolver) {
|
|
22118
|
+
return new PdfCaretAnnotation(dict);
|
|
22119
|
+
}
|
|
22120
|
+
/** Get the caret symbol. Defaults to 'None'. */
|
|
22121
|
+
getSymbol() {
|
|
22122
|
+
const obj = this.dict.get("/Sy");
|
|
22123
|
+
if (obj && obj.kind === "name") {
|
|
22124
|
+
if ((obj.value.startsWith("/") ? obj.value.slice(1) : obj.value) === "P") return "P";
|
|
22125
|
+
}
|
|
22126
|
+
return "None";
|
|
22127
|
+
}
|
|
22128
|
+
/** Set the caret symbol. */
|
|
22129
|
+
setSymbol(symbol) {
|
|
22130
|
+
this.dict.set("/Sy", require_pdfCatalog.PdfName.of(symbol));
|
|
22131
|
+
}
|
|
22132
|
+
/**
|
|
22133
|
+
* Get the inner rectangle differences (RD entry).
|
|
22134
|
+
* Returns [left, bottom, right, top] insets from the annotation rect.
|
|
22135
|
+
*/
|
|
22136
|
+
getCaretRect() {
|
|
22137
|
+
const obj = this.dict.get("/RD");
|
|
22138
|
+
if (obj && obj.kind === "array" && obj.items.length === 4) return obj.items.map((item) => {
|
|
22139
|
+
if (item.kind === "number") return item.value;
|
|
22140
|
+
return 0;
|
|
22141
|
+
});
|
|
22142
|
+
}
|
|
22143
|
+
/** Set the inner rectangle differences (RD entry). */
|
|
22144
|
+
setCaretRect(rd) {
|
|
22145
|
+
this.dict.set("/RD", require_pdfCatalog.PdfArray.of(rd.map(require_pdfCatalog.PdfNumber.of)));
|
|
22146
|
+
}
|
|
22147
|
+
};
|
|
22148
|
+
|
|
22149
|
+
//#endregion
|
|
22150
|
+
//#region src/annotation/types/fileAttachmentAnnotation.ts
|
|
22151
|
+
/**
|
|
22152
|
+
* @module annotation/types/fileAttachmentAnnotation
|
|
22153
|
+
*
|
|
22154
|
+
* File attachment annotation — embeds a file as an inline annotation
|
|
22155
|
+
* on a page, displayed as a clickable icon.
|
|
22156
|
+
*
|
|
22157
|
+
* Unlike document-level attachments (via `attachFile()`), file attachment
|
|
22158
|
+
* annotations are positioned on a specific page and rendered as an icon
|
|
22159
|
+
* that users can click to open or save the embedded file.
|
|
22160
|
+
*
|
|
22161
|
+
* Reference: PDF 1.7 spec, Section 12.5.6.15 (File Attachment Annotations).
|
|
22162
|
+
*/
|
|
22163
|
+
/**
|
|
22164
|
+
* A file attachment annotation (subtype /FileAttachment).
|
|
22165
|
+
*
|
|
22166
|
+
* Embeds a file directly in the annotation, rendered as a clickable
|
|
22167
|
+
* icon on the page. When the user clicks the icon, the PDF viewer
|
|
22168
|
+
* allows them to open or save the embedded file.
|
|
22169
|
+
*/
|
|
22170
|
+
var PdfFileAttachmentAnnotation = class PdfFileAttachmentAnnotation extends require_pdfPage.PdfAnnotation {
|
|
22171
|
+
/** The raw file data to embed. */
|
|
22172
|
+
fileData;
|
|
22173
|
+
/** The filename to display. */
|
|
22174
|
+
fileName;
|
|
22175
|
+
/** Optional MIME type. */
|
|
22176
|
+
mimeType;
|
|
22177
|
+
/** Optional file description. */
|
|
22178
|
+
fileDescription;
|
|
22179
|
+
constructor(dict) {
|
|
22180
|
+
super("FileAttachment", dict);
|
|
22181
|
+
}
|
|
22182
|
+
/**
|
|
22183
|
+
* Create a new file attachment annotation.
|
|
22184
|
+
*
|
|
22185
|
+
* @param options.file The file data to embed.
|
|
22186
|
+
* @param options.fileName The filename (e.g., 'invoice.xml').
|
|
22187
|
+
* @param options.mimeType Optional MIME type (e.g., 'application/xml').
|
|
22188
|
+
* @param options.description Optional description of the file.
|
|
22189
|
+
* @param options.icon Icon to display. Default: 'GraphPushPin'.
|
|
22190
|
+
*/
|
|
22191
|
+
static create(options) {
|
|
22192
|
+
const annot = new PdfFileAttachmentAnnotation(require_pdfPage.buildAnnotationDict("FileAttachment", options));
|
|
22193
|
+
annot.fileData = options.file;
|
|
22194
|
+
annot.fileName = options.fileName;
|
|
22195
|
+
annot.mimeType = options.mimeType;
|
|
22196
|
+
annot.fileDescription = options.description;
|
|
22197
|
+
if (options.icon !== void 0) annot.setIcon(options.icon);
|
|
22198
|
+
return annot;
|
|
22199
|
+
}
|
|
22200
|
+
/**
|
|
22201
|
+
* Create a PdfFileAttachmentAnnotation from an existing dictionary.
|
|
22202
|
+
*/
|
|
22203
|
+
static fromDict(dict, _resolver) {
|
|
22204
|
+
return new PdfFileAttachmentAnnotation(dict);
|
|
22205
|
+
}
|
|
22206
|
+
/** Get the icon name. Defaults to 'GraphPushPin'. */
|
|
22207
|
+
getIcon() {
|
|
22208
|
+
const obj = this.dict.get("/Name");
|
|
22209
|
+
if (obj && obj.kind === "name") {
|
|
22210
|
+
const val = obj.value.startsWith("/") ? obj.value.slice(1) : obj.value;
|
|
22211
|
+
if ([
|
|
22212
|
+
"GraphPushPin",
|
|
22213
|
+
"PaperclipTag",
|
|
22214
|
+
"Paperclip",
|
|
22215
|
+
"Tag"
|
|
22216
|
+
].includes(val)) return val;
|
|
22217
|
+
}
|
|
22218
|
+
return "GraphPushPin";
|
|
22219
|
+
}
|
|
22220
|
+
/** Set the icon name. */
|
|
22221
|
+
setIcon(icon) {
|
|
22222
|
+
this.dict.set("/Name", require_pdfCatalog.PdfName.of(icon));
|
|
22223
|
+
}
|
|
22224
|
+
/** Get the filename, if set. */
|
|
22225
|
+
getFileName() {
|
|
22226
|
+
const fs = this.dict.get("/FS");
|
|
22227
|
+
if (fs && fs.kind === "dict") {
|
|
22228
|
+
const uf = fs.get("/UF");
|
|
22229
|
+
if (uf && uf.kind === "string") return uf.value;
|
|
22230
|
+
const f = fs.get("/F");
|
|
22231
|
+
if (f && f.kind === "string") return f.value;
|
|
22232
|
+
}
|
|
22233
|
+
return this.fileName;
|
|
22234
|
+
}
|
|
22235
|
+
/**
|
|
22236
|
+
* Build the file specification dictionary and register the embedded
|
|
22237
|
+
* file stream. Call this before serializing the annotation.
|
|
22238
|
+
*
|
|
22239
|
+
* @param registry The document's object registry.
|
|
22240
|
+
* @returns The annotation dict with `/FS` referencing the file.
|
|
22241
|
+
*/
|
|
22242
|
+
buildFileSpec(registry) {
|
|
22243
|
+
if (!this.fileData || !this.fileName) return this.dict;
|
|
22244
|
+
const efStreamDict = new require_pdfCatalog.PdfDict();
|
|
22245
|
+
efStreamDict.set("/Type", require_pdfCatalog.PdfName.of("EmbeddedFile"));
|
|
22246
|
+
if (this.mimeType) efStreamDict.set("/Subtype", require_pdfCatalog.PdfName.of(this.mimeType.replace("/", "#2F")));
|
|
22247
|
+
const params = new require_pdfCatalog.PdfDict();
|
|
22248
|
+
params.set("/Size", require_pdfCatalog.PdfNumber.of(this.fileData.length));
|
|
22249
|
+
efStreamDict.set("/Params", params);
|
|
22250
|
+
const efStream = new require_pdfCatalog.PdfStream(efStreamDict, this.fileData);
|
|
22251
|
+
const efRef = registry.register(efStream);
|
|
22252
|
+
const efDict = new require_pdfCatalog.PdfDict();
|
|
22253
|
+
efDict.set("/F", efRef);
|
|
22254
|
+
const fsDict = new require_pdfCatalog.PdfDict();
|
|
22255
|
+
fsDict.set("/Type", require_pdfCatalog.PdfName.of("Filespec"));
|
|
22256
|
+
fsDict.set("/F", require_pdfCatalog.PdfString.literal(this.fileName));
|
|
22257
|
+
fsDict.set("/UF", require_pdfCatalog.PdfString.literal(this.fileName));
|
|
22258
|
+
fsDict.set("/EF", efDict);
|
|
22259
|
+
if (this.fileDescription) fsDict.set("/Desc", require_pdfCatalog.PdfString.literal(this.fileDescription));
|
|
22260
|
+
const fsRef = registry.register(fsDict);
|
|
22261
|
+
this.dict.set("/FS", fsRef);
|
|
22262
|
+
return this.dict;
|
|
22263
|
+
}
|
|
22264
|
+
};
|
|
22265
|
+
|
|
21773
22266
|
//#endregion
|
|
21774
22267
|
//#region src/parser/textExtractor.ts
|
|
21775
22268
|
/**
|
|
@@ -23854,66 +24347,1197 @@ function addTrailerId(data) {
|
|
|
23854
24347
|
}
|
|
23855
24348
|
|
|
23856
24349
|
//#endregion
|
|
23857
|
-
//#region src/
|
|
24350
|
+
//#region src/assets/image/imageOptimize.ts
|
|
23858
24351
|
/**
|
|
23859
|
-
*
|
|
24352
|
+
* Downscale an image to fit within the specified dimensions.
|
|
23860
24353
|
*
|
|
23861
|
-
*
|
|
23862
|
-
*
|
|
23863
|
-
* `name` so callers can use `instanceof` checks or `error.name` comparisons.
|
|
24354
|
+
* If the image is already smaller than the target dimensions, it is
|
|
24355
|
+
* returned unchanged.
|
|
23864
24356
|
*
|
|
23865
|
-
*
|
|
23866
|
-
*
|
|
24357
|
+
* @param image - The raw image pixel data.
|
|
24358
|
+
* @param options - Downscaling options (target dimensions, algorithm).
|
|
24359
|
+
* @returns The downscaled image, or the original if no scaling needed.
|
|
23867
24360
|
*
|
|
23868
|
-
*
|
|
23869
|
-
|
|
23870
|
-
|
|
23871
|
-
*
|
|
23872
|
-
*
|
|
24361
|
+
* @example
|
|
24362
|
+
* ```ts
|
|
24363
|
+
* const result = downscaleImage(rawImage, {
|
|
24364
|
+
* maxWidth: 1024,
|
|
24365
|
+
* maxHeight: 768,
|
|
24366
|
+
* algorithm: 'bilinear',
|
|
24367
|
+
* });
|
|
24368
|
+
* ```
|
|
23873
24369
|
*/
|
|
23874
|
-
|
|
23875
|
-
|
|
23876
|
-
|
|
23877
|
-
|
|
24370
|
+
function downscaleImage(image, options = {}) {
|
|
24371
|
+
const target = computeTargetDimensions$1(image.width, image.height, options);
|
|
24372
|
+
if (target.width >= image.width && target.height >= image.height) return image;
|
|
24373
|
+
switch (options.algorithm ?? "bilinear") {
|
|
24374
|
+
case "nearest": return resampleNearest(image, target.width, target.height);
|
|
24375
|
+
case "bilinear": return resampleBilinear(image, target.width, target.height);
|
|
24376
|
+
case "lanczos": return resampleLanczos(image, target.width, target.height);
|
|
24377
|
+
default: return resampleBilinear(image, target.width, target.height);
|
|
23878
24378
|
}
|
|
23879
|
-
}
|
|
24379
|
+
}
|
|
23880
24380
|
/**
|
|
23881
|
-
*
|
|
23882
|
-
*
|
|
24381
|
+
* Recompress raw image pixel data using the specified format.
|
|
24382
|
+
*
|
|
24383
|
+
* @param image - The raw image pixel data.
|
|
24384
|
+
* @param options - Recompression options (format, quality).
|
|
24385
|
+
* @returns The compressed image data.
|
|
24386
|
+
*
|
|
24387
|
+
* @example
|
|
24388
|
+
* ```ts
|
|
24389
|
+
* const result = await recompressImage(rawImage, {
|
|
24390
|
+
* format: 'deflate',
|
|
24391
|
+
* compressionLevel: 9,
|
|
24392
|
+
* });
|
|
24393
|
+
* ```
|
|
23883
24394
|
*/
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
24395
|
+
async function recompressImage(image, options = {}) {
|
|
24396
|
+
switch (options.format ?? "deflate") {
|
|
24397
|
+
case "deflate": return recompressDeflate(image, options.compressionLevel ?? 6);
|
|
24398
|
+
case "jpeg": return recompressJpeg(image, options.quality ?? 85, options.progressive ?? false, options.chromaSubsampling ?? "4:2:0");
|
|
24399
|
+
default: return {
|
|
24400
|
+
data: image.pixels,
|
|
24401
|
+
width: image.width,
|
|
24402
|
+
height: image.height,
|
|
24403
|
+
channels: image.channels,
|
|
24404
|
+
format: "raw",
|
|
24405
|
+
wasOptimized: false
|
|
24406
|
+
};
|
|
23888
24407
|
}
|
|
23889
|
-
}
|
|
24408
|
+
}
|
|
23890
24409
|
/**
|
|
23891
|
-
*
|
|
23892
|
-
*
|
|
24410
|
+
* Run the full image optimization pipeline: downscale then recompress.
|
|
24411
|
+
*
|
|
24412
|
+
* @param image - The raw image pixel data.
|
|
24413
|
+
* @param options - Combined optimization options.
|
|
24414
|
+
* @returns The optimized result.
|
|
23893
24415
|
*/
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
24416
|
+
async function optimizeImage(image, options = {}) {
|
|
24417
|
+
if (options.skipBelowBytes && image.pixels.length < options.skipBelowBytes) return {
|
|
24418
|
+
data: image.pixels,
|
|
24419
|
+
width: image.width,
|
|
24420
|
+
height: image.height,
|
|
24421
|
+
channels: image.channels,
|
|
24422
|
+
format: "raw",
|
|
24423
|
+
wasOptimized: false
|
|
24424
|
+
};
|
|
24425
|
+
return recompressImage(downscaleImage(image, options), options);
|
|
24426
|
+
}
|
|
23900
24427
|
/**
|
|
23901
|
-
*
|
|
24428
|
+
* Standard JPEG luminance quantization table (Table K.1 from JPEG spec)
|
|
24429
|
+
* at quality 50, scaled to quality 100 = all-ones.
|
|
24430
|
+
* @internal
|
|
23902
24431
|
*/
|
|
23903
|
-
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
24432
|
+
const STANDARD_LUMINANCE_QT = [
|
|
24433
|
+
16,
|
|
24434
|
+
11,
|
|
24435
|
+
10,
|
|
24436
|
+
16,
|
|
24437
|
+
24,
|
|
24438
|
+
40,
|
|
24439
|
+
51,
|
|
24440
|
+
61,
|
|
24441
|
+
12,
|
|
24442
|
+
12,
|
|
24443
|
+
14,
|
|
24444
|
+
19,
|
|
24445
|
+
26,
|
|
24446
|
+
58,
|
|
24447
|
+
60,
|
|
24448
|
+
55,
|
|
24449
|
+
14,
|
|
24450
|
+
13,
|
|
24451
|
+
16,
|
|
24452
|
+
24,
|
|
24453
|
+
40,
|
|
24454
|
+
57,
|
|
24455
|
+
69,
|
|
24456
|
+
56,
|
|
24457
|
+
14,
|
|
24458
|
+
17,
|
|
24459
|
+
22,
|
|
24460
|
+
29,
|
|
24461
|
+
51,
|
|
24462
|
+
87,
|
|
24463
|
+
80,
|
|
24464
|
+
62,
|
|
24465
|
+
18,
|
|
24466
|
+
22,
|
|
24467
|
+
37,
|
|
24468
|
+
56,
|
|
24469
|
+
68,
|
|
24470
|
+
109,
|
|
24471
|
+
103,
|
|
24472
|
+
77,
|
|
24473
|
+
24,
|
|
24474
|
+
35,
|
|
24475
|
+
55,
|
|
24476
|
+
64,
|
|
24477
|
+
81,
|
|
24478
|
+
104,
|
|
24479
|
+
113,
|
|
24480
|
+
92,
|
|
24481
|
+
49,
|
|
24482
|
+
64,
|
|
24483
|
+
78,
|
|
24484
|
+
87,
|
|
24485
|
+
103,
|
|
24486
|
+
121,
|
|
24487
|
+
120,
|
|
24488
|
+
101,
|
|
24489
|
+
72,
|
|
24490
|
+
92,
|
|
24491
|
+
95,
|
|
24492
|
+
98,
|
|
24493
|
+
112,
|
|
24494
|
+
100,
|
|
24495
|
+
103,
|
|
24496
|
+
99
|
|
24497
|
+
];
|
|
23909
24498
|
/**
|
|
23910
|
-
*
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
|
|
24499
|
+
* Estimate the JPEG quality level (1–100) from the quantization tables
|
|
24500
|
+
* embedded in a JPEG file.
|
|
24501
|
+
*
|
|
24502
|
+
* Parses the DQT (Define Quantization Table, marker 0xFFDB) segments
|
|
24503
|
+
* from the raw JPEG bytes and compares the table values against the
|
|
24504
|
+
* standard JPEG luminance quantization table to estimate the quality
|
|
24505
|
+
* factor that was used during encoding.
|
|
24506
|
+
*
|
|
24507
|
+
* If no DQT marker is found, returns `undefined`.
|
|
24508
|
+
*
|
|
24509
|
+
* @param jpegBytes - Raw JPEG file bytes.
|
|
24510
|
+
* @returns Estimated quality 1–100, or `undefined` if no DQT is found.
|
|
24511
|
+
*
|
|
24512
|
+
* @example
|
|
24513
|
+
* ```ts
|
|
24514
|
+
* import { estimateJpegQuality } from 'modern-pdf-lib';
|
|
24515
|
+
*
|
|
24516
|
+
* const quality = estimateJpegQuality(jpegBytes);
|
|
24517
|
+
* if (quality !== undefined) {
|
|
24518
|
+
* console.log(`Estimated JPEG quality: ${quality}`);
|
|
24519
|
+
* }
|
|
24520
|
+
* ```
|
|
24521
|
+
*/
|
|
24522
|
+
function estimateJpegQuality(jpegBytes) {
|
|
24523
|
+
if (jpegBytes.length < 2 || jpegBytes[0] !== 255 || jpegBytes[1] !== 216) return;
|
|
24524
|
+
let offset = 2;
|
|
24525
|
+
let bestTable;
|
|
24526
|
+
while (offset < jpegBytes.length - 1) {
|
|
24527
|
+
if (jpegBytes[offset] !== 255) {
|
|
24528
|
+
offset++;
|
|
24529
|
+
continue;
|
|
24530
|
+
}
|
|
24531
|
+
const marker = jpegBytes[offset + 1];
|
|
24532
|
+
if (marker === 255) {
|
|
24533
|
+
offset++;
|
|
24534
|
+
continue;
|
|
24535
|
+
}
|
|
24536
|
+
if (marker === 0 || marker === 1 || marker >= 208 && marker <= 217) {
|
|
24537
|
+
offset += 2;
|
|
24538
|
+
continue;
|
|
24539
|
+
}
|
|
24540
|
+
if (marker === 218) break;
|
|
24541
|
+
if (offset + 3 >= jpegBytes.length) break;
|
|
24542
|
+
const segLen = jpegBytes[offset + 2] << 8 | jpegBytes[offset + 3];
|
|
24543
|
+
if (marker === 219) {
|
|
24544
|
+
let pos = offset + 4;
|
|
24545
|
+
const segEnd = offset + 2 + segLen;
|
|
24546
|
+
while (pos < segEnd && pos + 1 < jpegBytes.length) {
|
|
24547
|
+
const pqTq = jpegBytes[pos];
|
|
24548
|
+
const precision = pqTq >> 4 & 15;
|
|
24549
|
+
const tableId = pqTq & 15;
|
|
24550
|
+
pos++;
|
|
24551
|
+
const tableSize = 64 * (precision === 0 ? 1 : 2);
|
|
24552
|
+
if (pos + tableSize > jpegBytes.length) break;
|
|
24553
|
+
const table = [];
|
|
24554
|
+
for (let i = 0; i < 64; i++) if (precision === 0) table.push(jpegBytes[pos + i]);
|
|
24555
|
+
else table.push(jpegBytes[pos + i * 2] << 8 | jpegBytes[pos + i * 2 + 1]);
|
|
24556
|
+
pos += tableSize;
|
|
24557
|
+
if (tableId === 0 || !bestTable) bestTable = table;
|
|
24558
|
+
}
|
|
24559
|
+
}
|
|
24560
|
+
offset += 2 + segLen;
|
|
24561
|
+
}
|
|
24562
|
+
if (!bestTable) return void 0;
|
|
24563
|
+
let totalRatio = 0;
|
|
24564
|
+
let count = 0;
|
|
24565
|
+
for (let i = 0; i < 64; i++) {
|
|
24566
|
+
const std = STANDARD_LUMINANCE_QT[i];
|
|
24567
|
+
const actual = bestTable[i];
|
|
24568
|
+
if (std === 0 || actual === 0) continue;
|
|
24569
|
+
const scaleFactor = actual * 100 / std;
|
|
24570
|
+
totalRatio += scaleFactor;
|
|
24571
|
+
count++;
|
|
24572
|
+
}
|
|
24573
|
+
if (count === 0) return void 0;
|
|
24574
|
+
const avgScale = totalRatio / count;
|
|
24575
|
+
let quality;
|
|
24576
|
+
if (avgScale < 100) quality = (200 - avgScale) / 2;
|
|
24577
|
+
else quality = 5e3 / avgScale;
|
|
24578
|
+
return Math.max(1, Math.min(100, Math.round(quality)));
|
|
24579
|
+
}
|
|
24580
|
+
/**
|
|
24581
|
+
* Compute target dimensions from options, preserving aspect ratio.
|
|
24582
|
+
* @internal
|
|
24583
|
+
*/
|
|
24584
|
+
function computeTargetDimensions$1(srcWidth, srcHeight, options) {
|
|
24585
|
+
let targetWidth = srcWidth;
|
|
24586
|
+
let targetHeight = srcHeight;
|
|
24587
|
+
if (options.targetDpi && options.printWidth && options.printHeight) {
|
|
24588
|
+
const printWidthInches = options.printWidth / 72;
|
|
24589
|
+
const printHeightInches = options.printHeight / 72;
|
|
24590
|
+
const dpiWidth = Math.round(printWidthInches * options.targetDpi);
|
|
24591
|
+
const dpiHeight = Math.round(printHeightInches * options.targetDpi);
|
|
24592
|
+
targetWidth = Math.min(targetWidth, dpiWidth);
|
|
24593
|
+
targetHeight = Math.min(targetHeight, dpiHeight);
|
|
24594
|
+
}
|
|
24595
|
+
if (options.maxWidth && targetWidth > options.maxWidth) {
|
|
24596
|
+
const scale = options.maxWidth / targetWidth;
|
|
24597
|
+
targetWidth = options.maxWidth;
|
|
24598
|
+
targetHeight = Math.round(targetHeight * scale);
|
|
24599
|
+
}
|
|
24600
|
+
if (options.maxHeight && targetHeight > options.maxHeight) {
|
|
24601
|
+
const scale = options.maxHeight / targetHeight;
|
|
24602
|
+
targetHeight = options.maxHeight;
|
|
24603
|
+
targetWidth = Math.round(targetWidth * scale);
|
|
24604
|
+
}
|
|
24605
|
+
targetWidth = Math.max(1, targetWidth);
|
|
24606
|
+
targetHeight = Math.max(1, targetHeight);
|
|
24607
|
+
return {
|
|
24608
|
+
width: targetWidth,
|
|
24609
|
+
height: targetHeight
|
|
24610
|
+
};
|
|
24611
|
+
}
|
|
24612
|
+
/**
|
|
24613
|
+
* Nearest-neighbor resampling.
|
|
24614
|
+
* @internal
|
|
24615
|
+
*/
|
|
24616
|
+
function resampleNearest(src, dstWidth, dstHeight) {
|
|
24617
|
+
const channels = src.channels;
|
|
24618
|
+
const dst = new Uint8Array(dstWidth * dstHeight * channels);
|
|
24619
|
+
const xRatio = src.width / dstWidth;
|
|
24620
|
+
const yRatio = src.height / dstHeight;
|
|
24621
|
+
for (let y = 0; y < dstHeight; y++) {
|
|
24622
|
+
const srcY = Math.min(Math.floor(y * yRatio), src.height - 1);
|
|
24623
|
+
for (let x = 0; x < dstWidth; x++) {
|
|
24624
|
+
const srcX = Math.min(Math.floor(x * xRatio), src.width - 1);
|
|
24625
|
+
const srcIdx = (srcY * src.width + srcX) * channels;
|
|
24626
|
+
const dstIdx = (y * dstWidth + x) * channels;
|
|
24627
|
+
for (let c = 0; c < channels; c++) dst[dstIdx + c] = src.pixels[srcIdx + c];
|
|
24628
|
+
}
|
|
24629
|
+
}
|
|
24630
|
+
return {
|
|
24631
|
+
pixels: dst,
|
|
24632
|
+
width: dstWidth,
|
|
24633
|
+
height: dstHeight,
|
|
24634
|
+
channels,
|
|
24635
|
+
bitsPerChannel: src.bitsPerChannel
|
|
24636
|
+
};
|
|
24637
|
+
}
|
|
24638
|
+
/**
|
|
24639
|
+
* Bilinear interpolation resampling.
|
|
24640
|
+
* @internal
|
|
24641
|
+
*/
|
|
24642
|
+
function resampleBilinear(src, dstWidth, dstHeight) {
|
|
24643
|
+
const channels = src.channels;
|
|
24644
|
+
const dst = new Uint8Array(dstWidth * dstHeight * channels);
|
|
24645
|
+
const xRatio = (src.width - 1) / Math.max(1, dstWidth - 1);
|
|
24646
|
+
const yRatio = (src.height - 1) / Math.max(1, dstHeight - 1);
|
|
24647
|
+
for (let y = 0; y < dstHeight; y++) {
|
|
24648
|
+
const srcYf = y * yRatio;
|
|
24649
|
+
const srcY0 = Math.floor(srcYf);
|
|
24650
|
+
const srcY1 = Math.min(srcY0 + 1, src.height - 1);
|
|
24651
|
+
const yFrac = srcYf - srcY0;
|
|
24652
|
+
for (let x = 0; x < dstWidth; x++) {
|
|
24653
|
+
const srcXf = x * xRatio;
|
|
24654
|
+
const srcX0 = Math.floor(srcXf);
|
|
24655
|
+
const srcX1 = Math.min(srcX0 + 1, src.width - 1);
|
|
24656
|
+
const xFrac = srcXf - srcX0;
|
|
24657
|
+
const dstIdx = (y * dstWidth + x) * channels;
|
|
24658
|
+
for (let c = 0; c < channels; c++) {
|
|
24659
|
+
const topLeft = src.pixels[(srcY0 * src.width + srcX0) * channels + c];
|
|
24660
|
+
const topRight = src.pixels[(srcY0 * src.width + srcX1) * channels + c];
|
|
24661
|
+
const bottomLeft = src.pixels[(srcY1 * src.width + srcX0) * channels + c];
|
|
24662
|
+
const bottomRight = src.pixels[(srcY1 * src.width + srcX1) * channels + c];
|
|
24663
|
+
const top = topLeft + (topRight - topLeft) * xFrac;
|
|
24664
|
+
const value = top + (bottomLeft + (bottomRight - bottomLeft) * xFrac - top) * yFrac;
|
|
24665
|
+
dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, value)));
|
|
24666
|
+
}
|
|
24667
|
+
}
|
|
24668
|
+
}
|
|
24669
|
+
return {
|
|
24670
|
+
pixels: dst,
|
|
24671
|
+
width: dstWidth,
|
|
24672
|
+
height: dstHeight,
|
|
24673
|
+
channels,
|
|
24674
|
+
bitsPerChannel: src.bitsPerChannel
|
|
24675
|
+
};
|
|
24676
|
+
}
|
|
24677
|
+
/**
|
|
24678
|
+
* Lanczos kernel function.
|
|
24679
|
+
*
|
|
24680
|
+
* Computes the Lanczos windowed sinc value for a given distance `x`
|
|
24681
|
+
* and window size `a`. For Lanczos-3, `a = 3`.
|
|
24682
|
+
*
|
|
24683
|
+
* @param x - Distance from the center sample.
|
|
24684
|
+
* @param a - Window radius (3 for Lanczos-3).
|
|
24685
|
+
* @returns The kernel weight.
|
|
24686
|
+
* @internal
|
|
24687
|
+
*/
|
|
24688
|
+
function lanczos(x, a = 3) {
|
|
24689
|
+
if (x === 0) return 1;
|
|
24690
|
+
if (Math.abs(x) >= a) return 0;
|
|
24691
|
+
const pix = Math.PI * x;
|
|
24692
|
+
return Math.sin(pix) / pix * (Math.sin(pix / a) / (pix / a));
|
|
24693
|
+
}
|
|
24694
|
+
/**
|
|
24695
|
+
* Lanczos-3 resampling.
|
|
24696
|
+
*
|
|
24697
|
+
* Uses a 6-tap (a=3) windowed sinc filter in both dimensions for
|
|
24698
|
+
* high-quality downscaling. This is the best quality option but
|
|
24699
|
+
* also the slowest.
|
|
24700
|
+
*
|
|
24701
|
+
* @internal
|
|
24702
|
+
*/
|
|
24703
|
+
function resampleLanczos(src, dstWidth, dstHeight) {
|
|
24704
|
+
const channels = src.channels;
|
|
24705
|
+
const a = 3;
|
|
24706
|
+
const dst = new Uint8Array(dstWidth * dstHeight * channels);
|
|
24707
|
+
const xRatio = src.width / dstWidth;
|
|
24708
|
+
const yRatio = src.height / dstHeight;
|
|
24709
|
+
for (let y = 0; y < dstHeight; y++) {
|
|
24710
|
+
const srcYf = (y + .5) * yRatio - .5;
|
|
24711
|
+
for (let x = 0; x < dstWidth; x++) {
|
|
24712
|
+
const srcXf = (x + .5) * xRatio - .5;
|
|
24713
|
+
const dstIdx = (y * dstWidth + x) * channels;
|
|
24714
|
+
const sum = new Float64Array(channels);
|
|
24715
|
+
let weightSum = 0;
|
|
24716
|
+
const yStart = Math.floor(srcYf) - a + 1;
|
|
24717
|
+
const yEnd = Math.floor(srcYf) + a;
|
|
24718
|
+
const xStart = Math.floor(srcXf) - a + 1;
|
|
24719
|
+
const xEnd = Math.floor(srcXf) + a;
|
|
24720
|
+
for (let sy = yStart; sy <= yEnd; sy++) {
|
|
24721
|
+
const wy = lanczos(srcYf - sy, a);
|
|
24722
|
+
if (wy === 0) continue;
|
|
24723
|
+
const clampedY = Math.max(0, Math.min(src.height - 1, sy));
|
|
24724
|
+
for (let sx = xStart; sx <= xEnd; sx++) {
|
|
24725
|
+
const wx = lanczos(srcXf - sx, a);
|
|
24726
|
+
if (wx === 0) continue;
|
|
24727
|
+
const w = wx * wy;
|
|
24728
|
+
const clampedX = Math.max(0, Math.min(src.width - 1, sx));
|
|
24729
|
+
const srcIdx = (clampedY * src.width + clampedX) * channels;
|
|
24730
|
+
for (let c = 0; c < channels; c++) sum[c] = (sum[c] ?? 0) + src.pixels[srcIdx + c] * w;
|
|
24731
|
+
weightSum += w;
|
|
24732
|
+
}
|
|
24733
|
+
}
|
|
24734
|
+
if (weightSum > 0) for (let c = 0; c < channels; c++) dst[dstIdx + c] = Math.round(Math.max(0, Math.min(255, sum[c] / weightSum)));
|
|
24735
|
+
}
|
|
24736
|
+
}
|
|
24737
|
+
return {
|
|
24738
|
+
pixels: dst,
|
|
24739
|
+
width: dstWidth,
|
|
24740
|
+
height: dstHeight,
|
|
24741
|
+
channels,
|
|
24742
|
+
bitsPerChannel: src.bitsPerChannel
|
|
24743
|
+
};
|
|
24744
|
+
}
|
|
24745
|
+
/**
|
|
24746
|
+
* Recompress image data using deflate (for PDF FlateDecode).
|
|
24747
|
+
* @internal
|
|
24748
|
+
*/
|
|
24749
|
+
async function recompressDeflate(image, level) {
|
|
24750
|
+
if (typeof CompressionStream !== "undefined") {
|
|
24751
|
+
const cs = new CompressionStream("deflate");
|
|
24752
|
+
const writer = cs.writable.getWriter();
|
|
24753
|
+
const reader = cs.readable.getReader();
|
|
24754
|
+
const chunks = [];
|
|
24755
|
+
writer.write(new Uint8Array(image.pixels)).catch(() => {});
|
|
24756
|
+
writer.close().catch(() => {});
|
|
24757
|
+
while (true) {
|
|
24758
|
+
const { done, value } = await reader.read();
|
|
24759
|
+
if (done) break;
|
|
24760
|
+
chunks.push(value);
|
|
24761
|
+
}
|
|
24762
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
24763
|
+
const result = new Uint8Array(totalLength);
|
|
24764
|
+
let pos = 0;
|
|
24765
|
+
for (const chunk of chunks) {
|
|
24766
|
+
result.set(chunk, pos);
|
|
24767
|
+
pos += chunk.length;
|
|
24768
|
+
}
|
|
24769
|
+
return {
|
|
24770
|
+
data: result,
|
|
24771
|
+
width: image.width,
|
|
24772
|
+
height: image.height,
|
|
24773
|
+
channels: image.channels,
|
|
24774
|
+
format: "deflate",
|
|
24775
|
+
wasOptimized: true
|
|
24776
|
+
};
|
|
24777
|
+
}
|
|
24778
|
+
try {
|
|
24779
|
+
const { deflateSync } = await import("fflate");
|
|
24780
|
+
return {
|
|
24781
|
+
data: deflateSync(image.pixels, { level }),
|
|
24782
|
+
width: image.width,
|
|
24783
|
+
height: image.height,
|
|
24784
|
+
channels: image.channels,
|
|
24785
|
+
format: "deflate",
|
|
24786
|
+
wasOptimized: true
|
|
24787
|
+
};
|
|
24788
|
+
} catch {
|
|
24789
|
+
return {
|
|
24790
|
+
data: image.pixels,
|
|
24791
|
+
width: image.width,
|
|
24792
|
+
height: image.height,
|
|
24793
|
+
channels: image.channels,
|
|
24794
|
+
format: "raw",
|
|
24795
|
+
wasOptimized: false
|
|
24796
|
+
};
|
|
24797
|
+
}
|
|
24798
|
+
}
|
|
24799
|
+
/**
|
|
24800
|
+
* Recompress image data as JPEG.
|
|
24801
|
+
*
|
|
24802
|
+
* Uses the JPEG WASM encoder when available (initialized via
|
|
24803
|
+
* `initJpegWasm()` or `initWasm({ jpeg: true })`). When WASM is not
|
|
24804
|
+
* loaded, returns the input data unchanged with `wasOptimized: false`.
|
|
24805
|
+
*
|
|
24806
|
+
* @param image - The raw image pixel data.
|
|
24807
|
+
* @param quality - JPEG quality 1–100.
|
|
24808
|
+
* @param progressive - Encode as progressive JPEG (default: false).
|
|
24809
|
+
* @param chromaSubsampling - Chroma subsampling mode (default: '4:2:0').
|
|
24810
|
+
* @returns The JPEG-encoded result, or raw data if WASM is unavailable.
|
|
24811
|
+
* @internal
|
|
24812
|
+
*/
|
|
24813
|
+
async function recompressJpeg(image, quality, progressive = false, chromaSubsampling = "4:2:0") {
|
|
24814
|
+
const { encodeJpegWasm, isJpegWasmReady } = await Promise.resolve().then(() => require("./bridge-DUcJFVsk.cjs")).then((n) => n.bridge_exports);
|
|
24815
|
+
if (isJpegWasmReady()) {
|
|
24816
|
+
let pixels = image.pixels;
|
|
24817
|
+
let channels = image.channels;
|
|
24818
|
+
if (image.channels === 4 && image.colorSpace === "cmyk") {
|
|
24819
|
+
pixels = convertCmykToRgb(image.pixels, image.width, image.height);
|
|
24820
|
+
channels = 3;
|
|
24821
|
+
}
|
|
24822
|
+
const jpegBytes = encodeJpegWasm(pixels, image.width, image.height, channels, quality, progressive, chromaSubsampling);
|
|
24823
|
+
if (jpegBytes) return {
|
|
24824
|
+
data: jpegBytes,
|
|
24825
|
+
width: image.width,
|
|
24826
|
+
height: image.height,
|
|
24827
|
+
channels,
|
|
24828
|
+
format: "jpeg",
|
|
24829
|
+
wasOptimized: true
|
|
24830
|
+
};
|
|
24831
|
+
}
|
|
24832
|
+
return {
|
|
24833
|
+
data: image.pixels,
|
|
24834
|
+
width: image.width,
|
|
24835
|
+
height: image.height,
|
|
24836
|
+
channels: image.channels,
|
|
24837
|
+
format: "raw",
|
|
24838
|
+
wasOptimized: false
|
|
24839
|
+
};
|
|
24840
|
+
}
|
|
24841
|
+
/**
|
|
24842
|
+
* Convert CMYK pixel data to RGB.
|
|
24843
|
+
*
|
|
24844
|
+
* Uses the standard CMYK→RGB formula (inverted CMYK, Adobe convention):
|
|
24845
|
+
* ```
|
|
24846
|
+
* R = 255 × (1 − C/255) × (1 − K/255)
|
|
24847
|
+
* G = 255 × (1 − M/255) × (1 − K/255)
|
|
24848
|
+
* B = 255 × (1 − Y/255) × (1 − K/255)
|
|
24849
|
+
* ```
|
|
24850
|
+
*
|
|
24851
|
+
* @param pixels - CMYK pixel data (4 bytes per pixel, row-major).
|
|
24852
|
+
* @param width - Image width.
|
|
24853
|
+
* @param height - Image height.
|
|
24854
|
+
* @returns RGB pixel data (3 bytes per pixel).
|
|
24855
|
+
* @internal
|
|
24856
|
+
*/
|
|
24857
|
+
function convertCmykToRgb(pixels, width, height) {
|
|
24858
|
+
const pixelCount = width * height;
|
|
24859
|
+
const rgb = new Uint8Array(pixelCount * 3);
|
|
24860
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
24861
|
+
const c = pixels[i * 4] / 255;
|
|
24862
|
+
const m = pixels[i * 4 + 1] / 255;
|
|
24863
|
+
const y = pixels[i * 4 + 2] / 255;
|
|
24864
|
+
const k = pixels[i * 4 + 3] / 255;
|
|
24865
|
+
rgb[i * 3] = Math.round(255 * (1 - c) * (1 - k));
|
|
24866
|
+
rgb[i * 3 + 1] = Math.round(255 * (1 - m) * (1 - k));
|
|
24867
|
+
rgb[i * 3 + 2] = Math.round(255 * (1 - y) * (1 - k));
|
|
24868
|
+
}
|
|
24869
|
+
return rgb;
|
|
24870
|
+
}
|
|
24871
|
+
|
|
24872
|
+
//#endregion
|
|
24873
|
+
//#region src/assets/image/imageExtract.ts
|
|
24874
|
+
/**
|
|
24875
|
+
* Resolve the color space name from a `/ColorSpace` entry.
|
|
24876
|
+
* Handles both simple names (`/DeviceRGB`) and array forms
|
|
24877
|
+
* (`[/ICCBased ...]`, `[/Indexed /DeviceRGB ...]`).
|
|
24878
|
+
* @internal
|
|
24879
|
+
*/
|
|
24880
|
+
function resolveColorSpace(csEntry, registry) {
|
|
24881
|
+
if (!csEntry) return "DeviceRGB";
|
|
24882
|
+
if (csEntry.kind === "ref") {
|
|
24883
|
+
const resolved = registry.resolve(csEntry);
|
|
24884
|
+
if (!resolved) return "DeviceRGB";
|
|
24885
|
+
return resolveColorSpace(resolved, registry);
|
|
24886
|
+
}
|
|
24887
|
+
if (csEntry.kind === "name") return csEntry.value.replace(/^\//, "");
|
|
24888
|
+
if (csEntry.kind === "array") {
|
|
24889
|
+
const arr = csEntry;
|
|
24890
|
+
const first = arr.items[0];
|
|
24891
|
+
if (first && first.kind === "name") {
|
|
24892
|
+
const csName = first.value.replace(/^\//, "");
|
|
24893
|
+
if (csName === "ICCBased") {
|
|
24894
|
+
const profileRef = arr.items[1];
|
|
24895
|
+
if (profileRef && profileRef.kind === "ref") {
|
|
24896
|
+
const profile = registry.resolve(profileRef);
|
|
24897
|
+
if (profile && profile.kind === "stream") {
|
|
24898
|
+
const n = profile.dict.get("/N");
|
|
24899
|
+
if (n && n.kind === "number") {
|
|
24900
|
+
const channels = n.value;
|
|
24901
|
+
if (channels === 1) return "DeviceGray";
|
|
24902
|
+
if (channels === 3) return "DeviceRGB";
|
|
24903
|
+
if (channels === 4) return "DeviceCMYK";
|
|
24904
|
+
}
|
|
24905
|
+
}
|
|
24906
|
+
}
|
|
24907
|
+
return "DeviceRGB";
|
|
24908
|
+
}
|
|
24909
|
+
if (csName === "Indexed") return "Indexed";
|
|
24910
|
+
return csName;
|
|
24911
|
+
}
|
|
24912
|
+
}
|
|
24913
|
+
return "DeviceRGB";
|
|
24914
|
+
}
|
|
24915
|
+
/**
|
|
24916
|
+
* Determine the number of channels from a color space name.
|
|
24917
|
+
* @internal
|
|
24918
|
+
*/
|
|
24919
|
+
function channelsFromColorSpace(colorSpace) {
|
|
24920
|
+
switch (colorSpace) {
|
|
24921
|
+
case "DeviceGray":
|
|
24922
|
+
case "CalGray": return 1;
|
|
24923
|
+
case "DeviceCMYK": return 4;
|
|
24924
|
+
case "Indexed": return 1;
|
|
24925
|
+
default: return 3;
|
|
24926
|
+
}
|
|
24927
|
+
}
|
|
24928
|
+
/**
|
|
24929
|
+
* Extract all image XObjects from a PDF document.
|
|
24930
|
+
*
|
|
24931
|
+
* Walks every page's `/Resources /XObject` dictionary and collects
|
|
24932
|
+
* metadata for each image XObject found.
|
|
24933
|
+
*
|
|
24934
|
+
* @param doc - A parsed `PdfDocument`.
|
|
24935
|
+
* @returns An array of `ImageInfo` objects, one per image XObject.
|
|
24936
|
+
*
|
|
24937
|
+
* @example
|
|
24938
|
+
* ```ts
|
|
24939
|
+
* import { loadPdf, extractImages } from 'modern-pdf-lib';
|
|
24940
|
+
*
|
|
24941
|
+
* const doc = await loadPdf(pdfBytes);
|
|
24942
|
+
* const images = extractImages(doc);
|
|
24943
|
+
*
|
|
24944
|
+
* for (const img of images) {
|
|
24945
|
+
* console.log(`${img.name}: ${img.width}x${img.height} ${img.colorSpace} (${img.compressedSize} bytes)`);
|
|
24946
|
+
* }
|
|
24947
|
+
* ```
|
|
24948
|
+
*/
|
|
24949
|
+
function extractImages(doc) {
|
|
24950
|
+
const images = [];
|
|
24951
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
24952
|
+
const pages = doc.getPages();
|
|
24953
|
+
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
|
24954
|
+
const page = pages[pageIndex];
|
|
24955
|
+
const resources = page.getOriginalResources();
|
|
24956
|
+
if (!resources) continue;
|
|
24957
|
+
let xObjDict;
|
|
24958
|
+
const xObjEntry = resources.get("/XObject");
|
|
24959
|
+
if (!xObjEntry) continue;
|
|
24960
|
+
if (xObjEntry.kind === "dict") xObjDict = xObjEntry;
|
|
24961
|
+
else if (xObjEntry.kind === "ref") {
|
|
24962
|
+
const resolved = page.getRegistry().resolve(xObjEntry);
|
|
24963
|
+
if (resolved && resolved.kind === "dict") xObjDict = resolved;
|
|
24964
|
+
}
|
|
24965
|
+
if (!xObjDict) continue;
|
|
24966
|
+
const registry = page.getRegistry();
|
|
24967
|
+
for (const [name, value] of xObjDict) {
|
|
24968
|
+
let ref;
|
|
24969
|
+
let stream;
|
|
24970
|
+
if (value.kind === "ref") {
|
|
24971
|
+
ref = value;
|
|
24972
|
+
if (seenRefs.has(ref.objectNumber)) continue;
|
|
24973
|
+
const resolved = registry.resolve(ref);
|
|
24974
|
+
if (resolved && resolved.kind === "stream") stream = resolved;
|
|
24975
|
+
} else if (value.kind === "stream") stream = value;
|
|
24976
|
+
if (!stream || !ref) continue;
|
|
24977
|
+
const subtype = stream.dict.get("/Subtype");
|
|
24978
|
+
if (!subtype || subtype.kind !== "name") continue;
|
|
24979
|
+
if (subtype.value !== "/Image") continue;
|
|
24980
|
+
const widthObj = stream.dict.get("/Width");
|
|
24981
|
+
const heightObj = stream.dict.get("/Height");
|
|
24982
|
+
const bpcObj = stream.dict.get("/BitsPerComponent");
|
|
24983
|
+
const width = widthObj && widthObj.kind === "number" ? widthObj.value : 0;
|
|
24984
|
+
const height = heightObj && heightObj.kind === "number" ? heightObj.value : 0;
|
|
24985
|
+
const bitsPerComponent = bpcObj && bpcObj.kind === "number" ? bpcObj.value : 8;
|
|
24986
|
+
const colorSpace = resolveColorSpace(stream.dict.get("/ColorSpace"), registry);
|
|
24987
|
+
const channels = channelsFromColorSpace(colorSpace);
|
|
24988
|
+
const { filters } = getStreamFilters(stream.dict);
|
|
24989
|
+
seenRefs.add(ref.objectNumber);
|
|
24990
|
+
images.push({
|
|
24991
|
+
stream,
|
|
24992
|
+
ref,
|
|
24993
|
+
name,
|
|
24994
|
+
pageIndex,
|
|
24995
|
+
width,
|
|
24996
|
+
height,
|
|
24997
|
+
bitsPerComponent,
|
|
24998
|
+
colorSpace,
|
|
24999
|
+
channels,
|
|
25000
|
+
filters,
|
|
25001
|
+
compressedSize: stream.data.length
|
|
25002
|
+
});
|
|
25003
|
+
}
|
|
25004
|
+
}
|
|
25005
|
+
return images;
|
|
25006
|
+
}
|
|
25007
|
+
/**
|
|
25008
|
+
* Decode image stream data into raw pixels.
|
|
25009
|
+
*
|
|
25010
|
+
* For DCTDecode (JPEG) streams, returns the raw JPEG bytes (not decoded
|
|
25011
|
+
* to pixels) since JPEG decoding requires the WASM module.
|
|
25012
|
+
*
|
|
25013
|
+
* For FlateDecode and other filters, fully decodes the stream.
|
|
25014
|
+
*
|
|
25015
|
+
* @param imageInfo - An `ImageInfo` from `extractImages()`.
|
|
25016
|
+
* @returns The decoded stream data.
|
|
25017
|
+
*/
|
|
25018
|
+
function decodeImageStream(imageInfo) {
|
|
25019
|
+
if (imageInfo.filters.length === 0) return imageInfo.stream.data;
|
|
25020
|
+
return decodeStream(imageInfo.stream.data, imageInfo.filters, null);
|
|
25021
|
+
}
|
|
25022
|
+
|
|
25023
|
+
//#endregion
|
|
25024
|
+
//#region src/assets/image/grayscaleDetect.ts
|
|
25025
|
+
/**
|
|
25026
|
+
* @module assets/image/grayscaleDetect
|
|
25027
|
+
*
|
|
25028
|
+
* Grayscale detection and conversion for image optimization.
|
|
25029
|
+
*
|
|
25030
|
+
* Detects RGB images where all pixels are effectively grayscale
|
|
25031
|
+
* (R ≈ G ≈ B) and converts them to single-channel grayscale,
|
|
25032
|
+
* reducing data size by ~66%.
|
|
25033
|
+
*
|
|
25034
|
+
* No Buffer — uses Uint8Array exclusively.
|
|
25035
|
+
*/
|
|
25036
|
+
/**
|
|
25037
|
+
* Check whether an RGB/RGBA image is effectively grayscale.
|
|
25038
|
+
*
|
|
25039
|
+
* Scans all pixels and checks if R, G, and B channels are within
|
|
25040
|
+
* `tolerance` of each other. If ≥99% of pixels pass, the image
|
|
25041
|
+
* is considered grayscale.
|
|
25042
|
+
*
|
|
25043
|
+
* @param pixels - Raw pixel data (row-major, channel-interleaved).
|
|
25044
|
+
* @param width - Image width in pixels.
|
|
25045
|
+
* @param height - Image height in pixels.
|
|
25046
|
+
* @param channels - Number of channels: 3 (RGB) or 4 (RGBA).
|
|
25047
|
+
* @param tolerance - Maximum allowed difference between R, G, and B
|
|
25048
|
+
* values for a pixel to be considered gray.
|
|
25049
|
+
* Default: `2`.
|
|
25050
|
+
* @returns `true` if the image is effectively grayscale.
|
|
25051
|
+
*
|
|
25052
|
+
* @example
|
|
25053
|
+
* ```ts
|
|
25054
|
+
* import { isGrayscaleImage, convertToGrayscale } from 'modern-pdf-lib';
|
|
25055
|
+
*
|
|
25056
|
+
* if (isGrayscaleImage(pixels, width, height, 3)) {
|
|
25057
|
+
* const grayPixels = convertToGrayscale(pixels, width, height, 3);
|
|
25058
|
+
* // grayPixels has 1 byte per pixel instead of 3
|
|
25059
|
+
* }
|
|
25060
|
+
* ```
|
|
25061
|
+
*/
|
|
25062
|
+
function isGrayscaleImage(pixels, width, height, channels, tolerance = 2) {
|
|
25063
|
+
const pixelCount = width * height;
|
|
25064
|
+
const maxNonGray = Math.floor(pixelCount * .01);
|
|
25065
|
+
let nonGrayCount = 0;
|
|
25066
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
25067
|
+
const r = pixels[i * channels];
|
|
25068
|
+
const g = pixels[i * channels + 1];
|
|
25069
|
+
const b = pixels[i * channels + 2];
|
|
25070
|
+
if (Math.max(r, g, b) - Math.min(r, g, b) > tolerance) {
|
|
25071
|
+
nonGrayCount++;
|
|
25072
|
+
if (nonGrayCount > maxNonGray) return false;
|
|
25073
|
+
}
|
|
25074
|
+
}
|
|
25075
|
+
return true;
|
|
25076
|
+
}
|
|
25077
|
+
/**
|
|
25078
|
+
* Convert an RGB/RGBA image to single-channel grayscale.
|
|
25079
|
+
*
|
|
25080
|
+
* Uses the ITU-R BT.601 luma formula:
|
|
25081
|
+
* ```
|
|
25082
|
+
* gray = 0.299 × R + 0.587 × G + 0.114 × B
|
|
25083
|
+
* ```
|
|
25084
|
+
*
|
|
25085
|
+
* The alpha channel (if present) is discarded.
|
|
25086
|
+
*
|
|
25087
|
+
* @param pixels - Raw pixel data (row-major, channel-interleaved).
|
|
25088
|
+
* @param width - Image width in pixels.
|
|
25089
|
+
* @param height - Image height in pixels.
|
|
25090
|
+
* @param channels - Number of channels: 3 (RGB) or 4 (RGBA).
|
|
25091
|
+
* @returns Grayscale pixel data (1 byte per pixel).
|
|
25092
|
+
*/
|
|
25093
|
+
function convertToGrayscale(pixels, width, height, channels) {
|
|
25094
|
+
const pixelCount = width * height;
|
|
25095
|
+
const gray = new Uint8Array(pixelCount);
|
|
25096
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
25097
|
+
const r = pixels[i * channels];
|
|
25098
|
+
const g = pixels[i * channels + 1];
|
|
25099
|
+
const b = pixels[i * channels + 2];
|
|
25100
|
+
gray[i] = Math.round(.299 * r + .587 * g + .114 * b);
|
|
25101
|
+
}
|
|
25102
|
+
return gray;
|
|
25103
|
+
}
|
|
25104
|
+
|
|
25105
|
+
//#endregion
|
|
25106
|
+
//#region src/assets/image/batchOptimize.ts
|
|
25107
|
+
/** Minimum image size to bother optimizing (10 KB). */
|
|
25108
|
+
const SMALL_IMAGE_THRESHOLD = 10240;
|
|
25109
|
+
/**
|
|
25110
|
+
* Optimize all images in a PDF document by recompressing them as JPEG.
|
|
25111
|
+
*
|
|
25112
|
+
* Walks every image XObject in the document, decodes its pixel data,
|
|
25113
|
+
* recompresses it as JPEG using the WASM encoder (if available), and
|
|
25114
|
+
* replaces the stream data in-place when the result is smaller.
|
|
25115
|
+
*
|
|
25116
|
+
* **Requires the JPEG WASM module to be initialized** via
|
|
25117
|
+
* `initJpegWasm()` or `initWasm({ jpeg: true })`. Without it,
|
|
25118
|
+
* no images will be optimized (all will be skipped).
|
|
25119
|
+
*
|
|
25120
|
+
* @param doc - A parsed `PdfDocument` (from `loadPdf()`).
|
|
25121
|
+
* @param options - Optimization settings.
|
|
25122
|
+
* @returns A report summarizing the optimization results.
|
|
25123
|
+
*
|
|
25124
|
+
* @example
|
|
25125
|
+
* ```ts
|
|
25126
|
+
* import { loadPdf, initWasm, optimizeAllImages } from 'modern-pdf-lib';
|
|
25127
|
+
*
|
|
25128
|
+
* await initWasm({ jpeg: true });
|
|
25129
|
+
*
|
|
25130
|
+
* const doc = await loadPdf(pdfBytes);
|
|
25131
|
+
* const report = await optimizeAllImages(doc);
|
|
25132
|
+
*
|
|
25133
|
+
* console.log(`Optimized ${report.optimizedImages} of ${report.totalImages} images`);
|
|
25134
|
+
* console.log(`Savings: ${report.savings.toFixed(1)}%`);
|
|
25135
|
+
*
|
|
25136
|
+
* const optimizedBytes = await doc.save();
|
|
25137
|
+
* ```
|
|
25138
|
+
*/
|
|
25139
|
+
async function optimizeAllImages(doc, options = {}) {
|
|
25140
|
+
const quality = options.quality ?? 80;
|
|
25141
|
+
const minSavingsPercent = options.minSavingsPercent ?? 10;
|
|
25142
|
+
const skipSmall = options.skipSmallImages ?? false;
|
|
25143
|
+
const progressive = options.progressive ?? false;
|
|
25144
|
+
const chromaSubsampling = options.chromaSubsampling ?? "4:2:0";
|
|
25145
|
+
const { encodeJpegWasm, isJpegWasmReady } = await Promise.resolve().then(() => require("./bridge-DUcJFVsk.cjs")).then((n) => n.bridge_exports);
|
|
25146
|
+
const { decodeJpegWasm } = await Promise.resolve().then(() => require("./bridge-DUcJFVsk.cjs")).then((n) => n.bridge_exports);
|
|
25147
|
+
const images = extractImages(doc);
|
|
25148
|
+
const perImage = [];
|
|
25149
|
+
let totalOriginal = 0;
|
|
25150
|
+
let totalNew = 0;
|
|
25151
|
+
let optimizedCount = 0;
|
|
25152
|
+
for (const img of images) {
|
|
25153
|
+
totalOriginal += img.compressedSize;
|
|
25154
|
+
if (!isJpegWasmReady()) {
|
|
25155
|
+
perImage.push({
|
|
25156
|
+
name: img.name,
|
|
25157
|
+
pageIndex: img.pageIndex,
|
|
25158
|
+
originalSize: img.compressedSize,
|
|
25159
|
+
newSize: img.compressedSize,
|
|
25160
|
+
skipped: true,
|
|
25161
|
+
reason: "JPEG WASM encoder not initialized"
|
|
25162
|
+
});
|
|
25163
|
+
totalNew += img.compressedSize;
|
|
25164
|
+
continue;
|
|
25165
|
+
}
|
|
25166
|
+
if (skipSmall && img.compressedSize < SMALL_IMAGE_THRESHOLD) {
|
|
25167
|
+
perImage.push({
|
|
25168
|
+
name: img.name,
|
|
25169
|
+
pageIndex: img.pageIndex,
|
|
25170
|
+
originalSize: img.compressedSize,
|
|
25171
|
+
newSize: img.compressedSize,
|
|
25172
|
+
skipped: true,
|
|
25173
|
+
reason: `Below size threshold (${SMALL_IMAGE_THRESHOLD} bytes)`
|
|
25174
|
+
});
|
|
25175
|
+
totalNew += img.compressedSize;
|
|
25176
|
+
continue;
|
|
25177
|
+
}
|
|
25178
|
+
if (img.bitsPerComponent !== 8) {
|
|
25179
|
+
perImage.push({
|
|
25180
|
+
name: img.name,
|
|
25181
|
+
pageIndex: img.pageIndex,
|
|
25182
|
+
originalSize: img.compressedSize,
|
|
25183
|
+
newSize: img.compressedSize,
|
|
25184
|
+
skipped: true,
|
|
25185
|
+
reason: `Unsupported bits per component: ${img.bitsPerComponent}`
|
|
25186
|
+
});
|
|
25187
|
+
totalNew += img.compressedSize;
|
|
25188
|
+
continue;
|
|
25189
|
+
}
|
|
25190
|
+
if (img.colorSpace === "Indexed") {
|
|
25191
|
+
perImage.push({
|
|
25192
|
+
name: img.name,
|
|
25193
|
+
pageIndex: img.pageIndex,
|
|
25194
|
+
originalSize: img.compressedSize,
|
|
25195
|
+
newSize: img.compressedSize,
|
|
25196
|
+
skipped: true,
|
|
25197
|
+
reason: "Indexed color space not suitable for JPEG"
|
|
25198
|
+
});
|
|
25199
|
+
totalNew += img.compressedSize;
|
|
25200
|
+
continue;
|
|
25201
|
+
}
|
|
25202
|
+
let pixels;
|
|
25203
|
+
let channels = img.channels;
|
|
25204
|
+
try {
|
|
25205
|
+
if (img.filters[0] === "DCTDecode") {
|
|
25206
|
+
const decoded = decodeJpegWasm(img.stream.data);
|
|
25207
|
+
if (!decoded) {
|
|
25208
|
+
perImage.push({
|
|
25209
|
+
name: img.name,
|
|
25210
|
+
pageIndex: img.pageIndex,
|
|
25211
|
+
originalSize: img.compressedSize,
|
|
25212
|
+
newSize: img.compressedSize,
|
|
25213
|
+
skipped: true,
|
|
25214
|
+
reason: "Failed to decode existing JPEG"
|
|
25215
|
+
});
|
|
25216
|
+
totalNew += img.compressedSize;
|
|
25217
|
+
continue;
|
|
25218
|
+
}
|
|
25219
|
+
pixels = decoded.pixels;
|
|
25220
|
+
channels = decoded.channels;
|
|
25221
|
+
} else pixels = decodeImageStream(img);
|
|
25222
|
+
} catch {
|
|
25223
|
+
perImage.push({
|
|
25224
|
+
name: img.name,
|
|
25225
|
+
pageIndex: img.pageIndex,
|
|
25226
|
+
originalSize: img.compressedSize,
|
|
25227
|
+
newSize: img.compressedSize,
|
|
25228
|
+
skipped: true,
|
|
25229
|
+
reason: "Failed to decode image stream"
|
|
25230
|
+
});
|
|
25231
|
+
totalNew += img.compressedSize;
|
|
25232
|
+
continue;
|
|
25233
|
+
}
|
|
25234
|
+
const expectedLen = img.width * img.height * channels;
|
|
25235
|
+
if (pixels.length !== expectedLen) {
|
|
25236
|
+
perImage.push({
|
|
25237
|
+
name: img.name,
|
|
25238
|
+
pageIndex: img.pageIndex,
|
|
25239
|
+
originalSize: img.compressedSize,
|
|
25240
|
+
newSize: img.compressedSize,
|
|
25241
|
+
skipped: true,
|
|
25242
|
+
reason: `Pixel data length mismatch: got ${pixels.length}, expected ${expectedLen}`
|
|
25243
|
+
});
|
|
25244
|
+
totalNew += img.compressedSize;
|
|
25245
|
+
continue;
|
|
25246
|
+
}
|
|
25247
|
+
if (channels === 4 && img.colorSpace === "DeviceCMYK") {
|
|
25248
|
+
const rgb = new Uint8Array(img.width * img.height * 3);
|
|
25249
|
+
for (let i = 0; i < img.width * img.height; i++) {
|
|
25250
|
+
const c = pixels[i * 4] / 255;
|
|
25251
|
+
const m = pixels[i * 4 + 1] / 255;
|
|
25252
|
+
const y = pixels[i * 4 + 2] / 255;
|
|
25253
|
+
const k = pixels[i * 4 + 3] / 255;
|
|
25254
|
+
rgb[i * 3] = Math.round(255 * (1 - c) * (1 - k));
|
|
25255
|
+
rgb[i * 3 + 1] = Math.round(255 * (1 - m) * (1 - k));
|
|
25256
|
+
rgb[i * 3 + 2] = Math.round(255 * (1 - y) * (1 - k));
|
|
25257
|
+
}
|
|
25258
|
+
pixels = rgb;
|
|
25259
|
+
channels = 3;
|
|
25260
|
+
}
|
|
25261
|
+
if (options.autoGrayscale && (channels === 3 || channels === 4)) {
|
|
25262
|
+
if (isGrayscaleImage(pixels, img.width, img.height, channels)) {
|
|
25263
|
+
pixels = convertToGrayscale(pixels, img.width, img.height, channels);
|
|
25264
|
+
channels = 1;
|
|
25265
|
+
}
|
|
25266
|
+
}
|
|
25267
|
+
const jpegBytes = encodeJpegWasm(pixels, img.width, img.height, channels, quality, progressive, chromaSubsampling);
|
|
25268
|
+
if (!jpegBytes) {
|
|
25269
|
+
perImage.push({
|
|
25270
|
+
name: img.name,
|
|
25271
|
+
pageIndex: img.pageIndex,
|
|
25272
|
+
originalSize: img.compressedSize,
|
|
25273
|
+
newSize: img.compressedSize,
|
|
25274
|
+
skipped: true,
|
|
25275
|
+
reason: "JPEG encoding failed"
|
|
25276
|
+
});
|
|
25277
|
+
totalNew += img.compressedSize;
|
|
25278
|
+
continue;
|
|
25279
|
+
}
|
|
25280
|
+
const savingsPercent = (img.compressedSize - jpegBytes.length) / img.compressedSize * 100;
|
|
25281
|
+
if (savingsPercent < minSavingsPercent) {
|
|
25282
|
+
perImage.push({
|
|
25283
|
+
name: img.name,
|
|
25284
|
+
pageIndex: img.pageIndex,
|
|
25285
|
+
originalSize: img.compressedSize,
|
|
25286
|
+
newSize: img.compressedSize,
|
|
25287
|
+
skipped: true,
|
|
25288
|
+
reason: `Savings ${savingsPercent.toFixed(1)}% below threshold ${minSavingsPercent}%`
|
|
25289
|
+
});
|
|
25290
|
+
totalNew += img.compressedSize;
|
|
25291
|
+
continue;
|
|
25292
|
+
}
|
|
25293
|
+
img.stream.data = jpegBytes;
|
|
25294
|
+
img.stream.syncLength();
|
|
25295
|
+
const dict = img.stream.dict;
|
|
25296
|
+
dict.set("/Filter", require_pdfCatalog.PdfName.of("/DCTDecode"));
|
|
25297
|
+
if (img.colorSpace === "DeviceCMYK" && channels === 3) dict.set("/ColorSpace", require_pdfCatalog.PdfName.of("/DeviceRGB"));
|
|
25298
|
+
if (channels === 1) dict.set("/ColorSpace", require_pdfCatalog.PdfName.of("/DeviceGray"));
|
|
25299
|
+
dict.delete("/DecodeParms");
|
|
25300
|
+
if (img.colorSpace === "DeviceCMYK") dict.delete("/Decode");
|
|
25301
|
+
optimizedCount++;
|
|
25302
|
+
perImage.push({
|
|
25303
|
+
name: img.name,
|
|
25304
|
+
pageIndex: img.pageIndex,
|
|
25305
|
+
originalSize: img.compressedSize,
|
|
25306
|
+
newSize: jpegBytes.length,
|
|
25307
|
+
skipped: false
|
|
25308
|
+
});
|
|
25309
|
+
totalNew += jpegBytes.length;
|
|
25310
|
+
}
|
|
25311
|
+
const overallSavings = totalOriginal > 0 ? (totalOriginal - totalNew) / totalOriginal * 100 : 0;
|
|
25312
|
+
return {
|
|
25313
|
+
totalImages: images.length,
|
|
25314
|
+
optimizedImages: optimizedCount,
|
|
25315
|
+
originalTotalBytes: totalOriginal,
|
|
25316
|
+
optimizedTotalBytes: totalNew,
|
|
25317
|
+
savings: overallSavings,
|
|
25318
|
+
perImage
|
|
25319
|
+
};
|
|
25320
|
+
}
|
|
25321
|
+
|
|
25322
|
+
//#endregion
|
|
25323
|
+
//#region src/assets/image/deduplicateImages.ts
|
|
25324
|
+
/**
|
|
25325
|
+
* Compute a fast FNV-1a hash of a byte array.
|
|
25326
|
+
*
|
|
25327
|
+
* This is used instead of SHA-256 because:
|
|
25328
|
+
* 1. It's synchronous (no need for crypto.subtle)
|
|
25329
|
+
* 2. It's fast for large buffers
|
|
25330
|
+
* 3. We only need collision resistance within a single document
|
|
25331
|
+
*
|
|
25332
|
+
* Returns a 64-char hex string (two 32-bit hashes concatenated).
|
|
25333
|
+
* @internal
|
|
25334
|
+
*/
|
|
25335
|
+
function hashBytes(data) {
|
|
25336
|
+
let h1 = 2166136261;
|
|
25337
|
+
for (let i = 0; i < data.length; i++) {
|
|
25338
|
+
h1 ^= data[i];
|
|
25339
|
+
h1 = Math.imul(h1, 16777619);
|
|
25340
|
+
}
|
|
25341
|
+
let h2 = 16777619;
|
|
25342
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
25343
|
+
h2 ^= data[i];
|
|
25344
|
+
h2 = Math.imul(h2, 2166136261);
|
|
25345
|
+
}
|
|
25346
|
+
const h3 = data.length * 2654435769 | 0;
|
|
25347
|
+
return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0") + (h3 >>> 0).toString(16).padStart(8, "0");
|
|
25348
|
+
}
|
|
25349
|
+
/**
|
|
25350
|
+
* Deduplicate identical images in a PDF document.
|
|
25351
|
+
*
|
|
25352
|
+
* Scans all image XObjects, hashes their compressed stream data (plus
|
|
25353
|
+
* dimensions and filter), and replaces duplicate references in page
|
|
25354
|
+
* resource dictionaries with the canonical (first-seen) copy.
|
|
25355
|
+
*
|
|
25356
|
+
* This operation modifies the document in-place. Duplicate streams
|
|
25357
|
+
* are not removed from the object registry (they become unreferenced
|
|
25358
|
+
* and will be omitted on save if the writer supports garbage collection).
|
|
25359
|
+
*
|
|
25360
|
+
* @param doc - A parsed `PdfDocument` (from `loadPdf()`).
|
|
25361
|
+
* @returns A report summarizing deduplication results.
|
|
25362
|
+
*
|
|
25363
|
+
* @example
|
|
25364
|
+
* ```ts
|
|
25365
|
+
* import { loadPdf, deduplicateImages } from 'modern-pdf-lib';
|
|
25366
|
+
*
|
|
25367
|
+
* const doc = await loadPdf(pdfBytes);
|
|
25368
|
+
* const report = await deduplicateImages(doc);
|
|
25369
|
+
*
|
|
25370
|
+
* console.log(`Removed ${report.duplicatesRemoved} duplicate images`);
|
|
25371
|
+
* console.log(`Saved ~${(report.bytesSaved / 1024).toFixed(0)} KB`);
|
|
25372
|
+
*
|
|
25373
|
+
* const optimizedBytes = await doc.save();
|
|
25374
|
+
* ```
|
|
25375
|
+
*/
|
|
25376
|
+
function deduplicateImages(doc) {
|
|
25377
|
+
const images = extractImages(doc);
|
|
25378
|
+
const hashToCanonical = /* @__PURE__ */ new Map();
|
|
25379
|
+
const duplicates = [];
|
|
25380
|
+
for (const img of images) {
|
|
25381
|
+
const key = `${img.width}x${img.height}:${img.filters.join(",")}:` + hashBytes(img.stream.data);
|
|
25382
|
+
const existing = hashToCanonical.get(key);
|
|
25383
|
+
if (existing) duplicates.push({
|
|
25384
|
+
image: img,
|
|
25385
|
+
canonicalRef: existing.ref
|
|
25386
|
+
});
|
|
25387
|
+
else hashToCanonical.set(key, {
|
|
25388
|
+
ref: img.ref,
|
|
25389
|
+
size: img.compressedSize
|
|
25390
|
+
});
|
|
25391
|
+
}
|
|
25392
|
+
let bytesSaved = 0;
|
|
25393
|
+
for (const { image, canonicalRef } of duplicates) {
|
|
25394
|
+
const page = doc.getPages()[image.pageIndex];
|
|
25395
|
+
if (!page) continue;
|
|
25396
|
+
const resources = page.getOriginalResources();
|
|
25397
|
+
if (!resources) continue;
|
|
25398
|
+
const xObjEntry = resources.get("/XObject");
|
|
25399
|
+
if (!xObjEntry) continue;
|
|
25400
|
+
let xObjDict;
|
|
25401
|
+
if (xObjEntry.kind === "dict") xObjDict = xObjEntry;
|
|
25402
|
+
else if (xObjEntry.kind === "ref") {
|
|
25403
|
+
const resolved = page.getRegistry().resolve(xObjEntry);
|
|
25404
|
+
if (resolved && resolved.kind === "dict") xObjDict = resolved;
|
|
25405
|
+
}
|
|
25406
|
+
if (!xObjDict) continue;
|
|
25407
|
+
xObjDict.set(image.name, canonicalRef);
|
|
25408
|
+
bytesSaved += image.compressedSize;
|
|
25409
|
+
}
|
|
25410
|
+
return {
|
|
25411
|
+
totalImages: images.length,
|
|
25412
|
+
uniqueImages: hashToCanonical.size,
|
|
25413
|
+
duplicatesRemoved: duplicates.length,
|
|
25414
|
+
bytesSaved
|
|
25415
|
+
};
|
|
25416
|
+
}
|
|
25417
|
+
|
|
25418
|
+
//#endregion
|
|
25419
|
+
//#region src/assets/image/dpiAnalyze.ts
|
|
25420
|
+
/**
|
|
25421
|
+
* Compute the effective DPI of an image given its pixel dimensions
|
|
25422
|
+
* and display dimensions in points.
|
|
25423
|
+
*
|
|
25424
|
+
* PDF uses 72 points per inch, so:
|
|
25425
|
+
* ```
|
|
25426
|
+
* DPI = imagePixels / (displayPoints / 72)
|
|
25427
|
+
* ```
|
|
25428
|
+
*
|
|
25429
|
+
* @param imageWidth - Image width in pixels.
|
|
25430
|
+
* @param imageHeight - Image height in pixels.
|
|
25431
|
+
* @param displayWidth - Display width in PDF points (1/72 inch).
|
|
25432
|
+
* @param displayHeight - Display height in PDF points (1/72 inch).
|
|
25433
|
+
* @returns DPI information.
|
|
25434
|
+
*
|
|
25435
|
+
* @example
|
|
25436
|
+
* ```ts
|
|
25437
|
+
* import { computeImageDpi } from 'modern-pdf-lib';
|
|
25438
|
+
*
|
|
25439
|
+
* // A 3000×2000 image displayed at 4.17×2.78 inches (300×200 points)
|
|
25440
|
+
* const dpi = computeImageDpi(3000, 2000, 300, 200);
|
|
25441
|
+
* console.log(dpi.effectiveDpi); // 720
|
|
25442
|
+
* ```
|
|
25443
|
+
*/
|
|
25444
|
+
function computeImageDpi(imageWidth, imageHeight, displayWidth, displayHeight) {
|
|
25445
|
+
const xDpi = displayWidth > 0 ? imageWidth / displayWidth * 72 : Infinity;
|
|
25446
|
+
const yDpi = displayHeight > 0 ? imageHeight / displayHeight * 72 : Infinity;
|
|
25447
|
+
return {
|
|
25448
|
+
xDpi,
|
|
25449
|
+
yDpi,
|
|
25450
|
+
effectiveDpi: Math.min(xDpi, yDpi)
|
|
25451
|
+
};
|
|
25452
|
+
}
|
|
25453
|
+
/**
|
|
25454
|
+
* Compute the target pixel dimensions for downscaling an image
|
|
25455
|
+
* to a maximum DPI at a given display size.
|
|
25456
|
+
*
|
|
25457
|
+
* @param imageWidth - Current image width in pixels.
|
|
25458
|
+
* @param imageHeight - Current image height in pixels.
|
|
25459
|
+
* @param displayWidth - Display width in PDF points.
|
|
25460
|
+
* @param displayHeight - Display height in PDF points.
|
|
25461
|
+
* @param maxDpi - Maximum allowed DPI.
|
|
25462
|
+
* @returns Target dimensions, or the original dimensions if no
|
|
25463
|
+
* downscaling is needed.
|
|
25464
|
+
*/
|
|
25465
|
+
function computeTargetDimensions(imageWidth, imageHeight, displayWidth, displayHeight, maxDpi) {
|
|
25466
|
+
const dpi = computeImageDpi(imageWidth, imageHeight, displayWidth, displayHeight);
|
|
25467
|
+
if (dpi.effectiveDpi <= maxDpi || !isFinite(dpi.effectiveDpi)) return {
|
|
25468
|
+
width: imageWidth,
|
|
25469
|
+
height: imageHeight,
|
|
25470
|
+
downscaled: false
|
|
25471
|
+
};
|
|
25472
|
+
const scale = maxDpi / dpi.effectiveDpi;
|
|
25473
|
+
return {
|
|
25474
|
+
width: Math.max(1, Math.round(imageWidth * scale)),
|
|
25475
|
+
height: Math.max(1, Math.round(imageHeight * scale)),
|
|
25476
|
+
downscaled: true
|
|
25477
|
+
};
|
|
25478
|
+
}
|
|
25479
|
+
|
|
25480
|
+
//#endregion
|
|
25481
|
+
//#region src/errors.ts
|
|
25482
|
+
/**
|
|
25483
|
+
* @module errors
|
|
25484
|
+
*
|
|
25485
|
+
* Typed error classes for common failure modes in the modern-pdf library.
|
|
25486
|
+
* Each error class extends the native `Error` and carries a descriptive
|
|
25487
|
+
* `name` so callers can use `instanceof` checks or `error.name` comparisons.
|
|
25488
|
+
*
|
|
25489
|
+
* All constructors accept an optional `ErrorOptions` parameter to support
|
|
25490
|
+
* error chaining via the standard `{ cause }` option (ES2022+).
|
|
25491
|
+
*
|
|
25492
|
+
* These match the pdf-lib error hierarchy for API compatibility.
|
|
25493
|
+
*/
|
|
25494
|
+
/**
|
|
25495
|
+
* Thrown when attempting to load or manipulate an encrypted PDF without
|
|
25496
|
+
* providing the correct password.
|
|
25497
|
+
*/
|
|
25498
|
+
var EncryptedPdfError = class extends Error {
|
|
25499
|
+
name = "EncryptedPdfError";
|
|
25500
|
+
constructor(message = "The PDF is encrypted. Please provide a password.", options) {
|
|
25501
|
+
super(message, options);
|
|
25502
|
+
}
|
|
25503
|
+
};
|
|
25504
|
+
/**
|
|
25505
|
+
* Thrown when a font operation requires an embedded font but none has been
|
|
25506
|
+
* registered or the font reference is invalid.
|
|
25507
|
+
*/
|
|
25508
|
+
var FontNotEmbeddedError = class extends Error {
|
|
25509
|
+
name = "FontNotEmbeddedError";
|
|
25510
|
+
constructor(fontName, options) {
|
|
25511
|
+
super(fontName ? `The font "${fontName}" has not been embedded in this document.` : "No font has been embedded. Call doc.embedFont() first.", options);
|
|
25512
|
+
}
|
|
25513
|
+
};
|
|
25514
|
+
/**
|
|
25515
|
+
* Thrown when attempting to use a page from a different document without
|
|
25516
|
+
* first copying it.
|
|
25517
|
+
*/
|
|
25518
|
+
var ForeignPageError = class extends Error {
|
|
25519
|
+
name = "ForeignPageError";
|
|
25520
|
+
constructor(options) {
|
|
25521
|
+
super("The page belongs to a different document. Use doc.copyPages() to import pages from another document before adding them.", options);
|
|
25522
|
+
}
|
|
25523
|
+
};
|
|
25524
|
+
/**
|
|
25525
|
+
* Thrown when attempting to remove a page from a document that has no pages.
|
|
25526
|
+
*/
|
|
25527
|
+
var RemovePageFromEmptyDocumentError = class extends Error {
|
|
25528
|
+
name = "RemovePageFromEmptyDocumentError";
|
|
25529
|
+
constructor(options) {
|
|
25530
|
+
super("Cannot remove a page from a document with no pages.", options);
|
|
25531
|
+
}
|
|
25532
|
+
};
|
|
25533
|
+
/**
|
|
25534
|
+
* Thrown when looking up a form field by name that does not exist.
|
|
25535
|
+
*/
|
|
25536
|
+
var NoSuchFieldError = class extends Error {
|
|
25537
|
+
name = "NoSuchFieldError";
|
|
25538
|
+
constructor(fieldName, options) {
|
|
25539
|
+
super(`No form field named "${fieldName}" exists in this document.`, options);
|
|
25540
|
+
}
|
|
23917
25541
|
};
|
|
23918
25542
|
/**
|
|
23919
25543
|
* Thrown when a form field is accessed via the wrong typed getter
|
|
@@ -24021,15 +25645,18 @@ async function initWasm(options) {
|
|
|
24021
25645
|
if (options === void 0 || typeof options === "string" || options instanceof URL) return;
|
|
24022
25646
|
if (wasmInitialized) return;
|
|
24023
25647
|
const inits = [];
|
|
24024
|
-
if (options.deflate || options.deflateWasm) inits.push(Promise.resolve().then(() => require("./libdeflateWasm-
|
|
25648
|
+
if (options.deflate || options.deflateWasm) inits.push(Promise.resolve().then(() => require("./libdeflateWasm-Enus0G1k.cjs")).then((n) => n.libdeflateWasm_exports).then(async ({ initDeflateWasm }) => {
|
|
24025
25649
|
await initDeflateWasm(options.deflateWasm);
|
|
24026
25650
|
}));
|
|
24027
|
-
if (options.png || options.pngWasm) inits.push(Promise.resolve().then(() => require("./pngEmbed-
|
|
25651
|
+
if (options.png || options.pngWasm) inits.push(Promise.resolve().then(() => require("./pngEmbed-10m4CfBU.cjs")).then((n) => n.pngEmbed_exports).then(async ({ initPngWasm }) => {
|
|
24028
25652
|
await initPngWasm(options.pngWasm);
|
|
24029
25653
|
}));
|
|
24030
25654
|
if (options.fonts || options.fontWasm) inits.push(Promise.resolve().then(() => require("./fontSubset-pFc8Dueu.cjs")).then((n) => n.fontSubset_exports).then(async ({ initSubsetWasm }) => {
|
|
24031
25655
|
await initSubsetWasm(options.fontWasm);
|
|
24032
25656
|
}));
|
|
25657
|
+
if (options.jpeg || options.jpegWasm) inits.push(Promise.resolve().then(() => require("./bridge-DUcJFVsk.cjs")).then((n) => n.bridge_exports).then(async ({ initJpegWasm }) => {
|
|
25658
|
+
await initJpegWasm(options.jpegWasm);
|
|
25659
|
+
}));
|
|
24033
25660
|
await Promise.all(inits);
|
|
24034
25661
|
wasmInitialized = true;
|
|
24035
25662
|
}
|
|
@@ -24060,6 +25687,7 @@ exports.PdfAnnotation = require_pdfPage.PdfAnnotation;
|
|
|
24060
25687
|
exports.PdfArray = require_pdfCatalog.PdfArray;
|
|
24061
25688
|
exports.PdfBool = require_pdfCatalog.PdfBool;
|
|
24062
25689
|
exports.PdfButtonField = PdfButtonField;
|
|
25690
|
+
exports.PdfCaretAnnotation = PdfCaretAnnotation;
|
|
24063
25691
|
exports.PdfCheckboxField = PdfCheckboxField;
|
|
24064
25692
|
exports.PdfCircleAnnotation = PdfCircleAnnotation;
|
|
24065
25693
|
exports.PdfDict = require_pdfCatalog.PdfDict;
|
|
@@ -24067,6 +25695,7 @@ exports.PdfDocument = PdfDocument;
|
|
|
24067
25695
|
exports.PdfDropdownField = PdfDropdownField;
|
|
24068
25696
|
exports.PdfEncryptionHandler = PdfEncryptionHandler;
|
|
24069
25697
|
exports.PdfField = PdfField;
|
|
25698
|
+
exports.PdfFileAttachmentAnnotation = PdfFileAttachmentAnnotation;
|
|
24070
25699
|
exports.PdfForm = PdfForm;
|
|
24071
25700
|
exports.PdfFreeTextAnnotation = PdfFreeTextAnnotation;
|
|
24072
25701
|
exports.PdfHighlightAnnotation = PdfHighlightAnnotation;
|
|
@@ -24086,6 +25715,7 @@ exports.PdfPage = require_pdfPage.PdfPage;
|
|
|
24086
25715
|
exports.PdfParseError = PdfParseError;
|
|
24087
25716
|
exports.PdfPolyLineAnnotation = PdfPolyLineAnnotation;
|
|
24088
25717
|
exports.PdfPolygonAnnotation = PdfPolygonAnnotation;
|
|
25718
|
+
exports.PdfPopupAnnotation = PdfPopupAnnotation;
|
|
24089
25719
|
exports.PdfRadioGroup = PdfRadioGroup;
|
|
24090
25720
|
exports.PdfRedactAnnotation = PdfRedactAnnotation;
|
|
24091
25721
|
exports.PdfRef = require_pdfCatalog.PdfRef;
|
|
@@ -24156,9 +25786,12 @@ exports.colorToComponents = require_pdfPage.colorToComponents;
|
|
|
24156
25786
|
exports.componentsToColor = require_pdfPage.componentsToColor;
|
|
24157
25787
|
exports.computeFileEncryptionKey = computeFileEncryptionKey;
|
|
24158
25788
|
exports.computeFontSize = computeFontSize;
|
|
25789
|
+
exports.computeImageDpi = computeImageDpi;
|
|
24159
25790
|
exports.computeSignatureHash = computeSignatureHash;
|
|
25791
|
+
exports.computeTargetDimensions = computeTargetDimensions;
|
|
24160
25792
|
exports.concatMatrix = require_pdfPage.concatMatrix;
|
|
24161
25793
|
exports.concatTransformationMatrix = require_pdfPage.concatMatrix;
|
|
25794
|
+
exports.convertToGrayscale = convertToGrayscale;
|
|
24162
25795
|
exports.copyPages = copyPages;
|
|
24163
25796
|
exports.createAnnotation = require_pdfPage.createAnnotation;
|
|
24164
25797
|
exports.createMarkedContentScope = require_pdfPage.createMarkedContentScope;
|
|
@@ -24168,10 +25801,14 @@ exports.cropPage = cropPage;
|
|
|
24168
25801
|
exports.curveToFinal = require_pdfPage.curveToFinal;
|
|
24169
25802
|
exports.curveToInitial = require_pdfPage.curveToInitial;
|
|
24170
25803
|
exports.curveToOp = require_pdfPage.curveTo;
|
|
25804
|
+
exports.decodeImageStream = decodeImageStream;
|
|
25805
|
+
exports.decodeJpegWasm = require_bridge.decodeJpegWasm;
|
|
24171
25806
|
exports.decodePermissions = decodePermissions;
|
|
24172
25807
|
exports.decodeStream = decodeStream;
|
|
25808
|
+
exports.deduplicateImages = deduplicateImages;
|
|
24173
25809
|
exports.degrees = require_pdfPage.degrees;
|
|
24174
25810
|
exports.degreesToRadians = require_pdfPage.degreesToRadians;
|
|
25811
|
+
exports.downscaleImage = downscaleImage;
|
|
24175
25812
|
exports.drawImageWithMatrix = require_pdfPage.drawImageWithMatrix;
|
|
24176
25813
|
exports.drawImageXObject = require_pdfPage.drawImageXObject;
|
|
24177
25814
|
exports.drawObject = require_pdfPage.drawXObject;
|
|
@@ -24182,6 +25819,7 @@ exports.embedPageAsFormXObject = embedPageAsFormXObject;
|
|
|
24182
25819
|
exports.embedSignature = embedSignature;
|
|
24183
25820
|
exports.encodeContextTag = encodeContextTag;
|
|
24184
25821
|
exports.encodeInteger = encodeInteger;
|
|
25822
|
+
exports.encodeJpegWasm = require_bridge.encodeJpegWasm;
|
|
24185
25823
|
exports.encodeLength = encodeLength;
|
|
24186
25824
|
exports.encodeOID = encodeOID;
|
|
24187
25825
|
exports.encodeOctetString = encodeOctetString;
|
|
@@ -24197,6 +25835,8 @@ exports.endMarkedContent = require_pdfPage.endMarkedContent;
|
|
|
24197
25835
|
exports.endPathOp = require_pdfPage.endPath;
|
|
24198
25836
|
exports.endText = require_pdfPage.endText;
|
|
24199
25837
|
exports.enforcePdfA = enforcePdfA;
|
|
25838
|
+
exports.estimateJpegQuality = estimateJpegQuality;
|
|
25839
|
+
exports.extractImages = extractImages;
|
|
24200
25840
|
exports.extractMetrics = extractMetrics;
|
|
24201
25841
|
exports.extractText = extractText;
|
|
24202
25842
|
exports.extractTextWithPositions = extractTextWithPositions;
|
|
@@ -24228,9 +25868,12 @@ exports.getPageSize = getPageSize;
|
|
|
24228
25868
|
exports.getRedactionMarks = require_pdfPage.getRedactionMarks;
|
|
24229
25869
|
exports.getSignatures = getSignatures;
|
|
24230
25870
|
exports.grayscale = require_pdfPage.grayscale;
|
|
25871
|
+
exports.initJpegWasm = require_bridge.initJpegWasm;
|
|
24231
25872
|
exports.initWasm = initWasm;
|
|
24232
25873
|
exports.insertPage = insertPage;
|
|
24233
25874
|
exports.isAccessible = isAccessible;
|
|
25875
|
+
exports.isGrayscaleImage = isGrayscaleImage;
|
|
25876
|
+
exports.isJpegWasmReady = require_bridge.isJpegWasmReady;
|
|
24234
25877
|
exports.isLinearized = isLinearized;
|
|
24235
25878
|
exports.isOpenTypeCFF = isOpenTypeCFF;
|
|
24236
25879
|
exports.isTrueType = isTrueType;
|
|
@@ -24249,6 +25892,8 @@ exports.moveTextOp = require_pdfPage.moveText;
|
|
|
24249
25892
|
exports.moveTextSetLeading = require_pdfPage.moveTextSetLeading;
|
|
24250
25893
|
exports.moveToOp = require_pdfPage.moveTo;
|
|
24251
25894
|
exports.nextLineOp = require_pdfPage.nextLine;
|
|
25895
|
+
exports.optimizeAllImages = optimizeAllImages;
|
|
25896
|
+
exports.optimizeImage = optimizeImage;
|
|
24252
25897
|
exports.parseContentStream = parseContentStream;
|
|
24253
25898
|
exports.parseSvg = require_pdfPage.parseSvg;
|
|
24254
25899
|
exports.parseSvgColor = require_pdfPage.parseSvgColor;
|
|
@@ -24264,6 +25909,7 @@ exports.radialGradient = require_pdfPage.radialGradient;
|
|
|
24264
25909
|
exports.radians = require_pdfPage.radians;
|
|
24265
25910
|
exports.radiansToDegrees = require_pdfPage.radiansToDegrees;
|
|
24266
25911
|
exports.rc4 = rc4;
|
|
25912
|
+
exports.recompressImage = recompressImage;
|
|
24267
25913
|
exports.rectangleOp = require_pdfPage.rectangle;
|
|
24268
25914
|
exports.removePage = removePage;
|
|
24269
25915
|
exports.removePages = removePages;
|