@u1f992/pdfdiff 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -18,7 +18,6 @@ var HeaderTypes;
18
18
  HeaderTypes[HeaderTypes["BITMAP_V4_HEADER"] = 108] = "BITMAP_V4_HEADER";
19
19
  HeaderTypes[HeaderTypes["BITMAP_V5_HEADER"] = 124] = "BITMAP_V5_HEADER";
20
20
  })(HeaderTypes || (HeaderTypes = {}));
21
- var HeaderTypes$1 = HeaderTypes;
22
21
 
23
22
  // We have these:
24
23
  //
@@ -148,7 +147,7 @@ class BmpDecoder {
148
147
  this.offset = this.readUInt32LE();
149
148
  // End of BITMAP_FILE_HEADER
150
149
  this.headerSize = this.readUInt32LE();
151
- if (!(this.headerSize in HeaderTypes$1)) {
150
+ if (!(this.headerSize in HeaderTypes)) {
152
151
  throw new Error(`Unsupported BMP header size ${this.headerSize}`);
153
152
  }
154
153
  this.width = this.readUInt32LE();
@@ -180,7 +179,7 @@ class BmpDecoder {
180
179
  this.maskBlue = 0x001f;
181
180
  }
182
181
  // End of BITMAP_INFO_HEADER
183
- if (this.headerSize > HeaderTypes$1.BITMAP_INFO_HEADER ||
182
+ if (this.headerSize > HeaderTypes.BITMAP_INFO_HEADER ||
184
183
  this.compression === BmpCompression.BI_BIT_FIELDS ||
185
184
  this.compression === BmpCompression.BI_ALPHA_BIT_FIELDS) {
186
185
  this.maskRed = this.readUInt32LE();
@@ -188,18 +187,18 @@ class BmpDecoder {
188
187
  this.maskBlue = this.readUInt32LE();
189
188
  }
190
189
  // End of BITMAP_V2_INFO_HEADER
191
- if (this.headerSize > HeaderTypes$1.BITMAP_V2_INFO_HEADER ||
190
+ if (this.headerSize > HeaderTypes.BITMAP_V2_INFO_HEADER ||
192
191
  this.compression === BmpCompression.BI_ALPHA_BIT_FIELDS) {
193
192
  this.maskAlpha = this.readUInt32LE();
194
193
  }
195
194
  // End of BITMAP_V3_INFO_HEADER
196
- if (this.headerSize > HeaderTypes$1.BITMAP_V3_INFO_HEADER) {
195
+ if (this.headerSize > HeaderTypes.BITMAP_V3_INFO_HEADER) {
197
196
  this.pos +=
198
- HeaderTypes$1.BITMAP_V4_HEADER - HeaderTypes$1.BITMAP_V3_INFO_HEADER;
197
+ HeaderTypes.BITMAP_V4_HEADER - HeaderTypes.BITMAP_V3_INFO_HEADER;
199
198
  }
200
199
  // End of BITMAP_V4_HEADER
201
- if (this.headerSize > HeaderTypes$1.BITMAP_V4_HEADER) {
202
- this.pos += HeaderTypes$1.BITMAP_V5_HEADER - HeaderTypes$1.BITMAP_V4_HEADER;
200
+ if (this.headerSize > HeaderTypes.BITMAP_V4_HEADER) {
201
+ this.pos += HeaderTypes.BITMAP_V5_HEADER - HeaderTypes.BITMAP_V4_HEADER;
203
202
  }
204
203
  // End of BITMAP_V5_HEADER
205
204
  if (this.bitPP <= 8 || this.colors > 0) {
@@ -515,7 +514,7 @@ class BmpEncoder {
515
514
  this.buffer = imgData.data;
516
515
  this.width = imgData.width;
517
516
  this.height = imgData.height;
518
- this.headerSize = HeaderTypes$1.BITMAP_INFO_HEADER;
517
+ this.headerSize = HeaderTypes.BITMAP_INFO_HEADER;
519
518
  // Header
520
519
  this.flag = 'BM';
521
520
  this.bitPP = imgData.bitPP || 24;
@@ -2032,31 +2031,6 @@ function intToRGBA$1(i) {
2032
2031
  Math.pow(256, 0));
2033
2032
  return rgba;
2034
2033
  }
2035
- /**
2036
- * A static helper method that converts RGBA values to a single integer value
2037
- * @param r the red value (0-255)
2038
- * @param g the green value (0-255)
2039
- * @param b the blue value (0-255)
2040
- * @param a the alpha value (0-255)
2041
- * @example
2042
- * ```ts
2043
- * import { rgbaToInt } from "@jimp/utils";
2044
- *
2045
- * rgbaToInt(255, 0, 0, 255); // 0xFF0000FF
2046
- * ```
2047
- */
2048
- function rgbaToInt(r, g, b, a) {
2049
- let i = r & 0xff;
2050
- i <<= 8;
2051
- i |= g & 0xff;
2052
- i <<= 8;
2053
- i |= b & 0xff;
2054
- i <<= 8;
2055
- i |= a & 0xff;
2056
- // Ensure sign is correct
2057
- i >>>= 0;
2058
- return i;
2059
- }
2060
2034
  /**
2061
2035
  * Compute color difference
2062
2036
  * 0 means no difference, 1 means maximum difference.
@@ -36374,13 +36348,13 @@ const defaultPlugins = [
36374
36348
  ];
36375
36349
  const defaultFormats = [bmp, msBmp, gif, jpeg$1, png, tiff];
36376
36350
  /** Convenience object for getting the MIME types of the default formats */
36377
- const JimpMime = {
36351
+ ({
36378
36352
  bmp: bmp().mime,
36379
36353
  gif: gif().mime,
36380
36354
  jpeg: jpeg$1().mime,
36381
36355
  png: png().mime,
36382
36356
  tiff: tiff().mime,
36383
- };
36357
+ });
36384
36358
  // TODO: This doesn't document the constructor of the class
36385
36359
  /**
36386
36360
  * @class
@@ -39449,13 +39423,6 @@ globalThis.$libmupdf_device = {
39449
39423
  * You should have received a copy of the GNU General Public License
39450
39424
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
39451
39425
  */
39452
- function createEmptyImage(width, height) {
39453
- return new Jimp({
39454
- width,
39455
- height,
39456
- color: rgbaToInt(0, 0, 0, 0),
39457
- });
39458
- }
39459
39426
  const alignStrategyValues = new Set([
39460
39427
  "resize",
39461
39428
  "top-left",
@@ -39470,55 +39437,6 @@ const alignStrategyValues = new Set([
39470
39437
  ]);
39471
39438
  const isValidAlignStrategy = (str) => alignStrategyValues.has(str);
39472
39439
 
39473
- /*
39474
- * Copyright (C) 2025 Koutaro Mukai
39475
- *
39476
- * This program is free software: you can redistribute it and/or modify
39477
- * it under the terms of the GNU General Public License as published by
39478
- * the Free Software Foundation, either version 3 of the License, or
39479
- * (at your option) any later version.
39480
- *
39481
- * This program is distributed in the hope that it will be useful,
39482
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
39483
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39484
- * GNU General Public License for more details.
39485
- *
39486
- * You should have received a copy of the GNU General Public License
39487
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
39488
- */
39489
- async function* withIndex(iter, start = 0) {
39490
- let index = start;
39491
- for await (const item of iter) {
39492
- yield [index, item];
39493
- index++;
39494
- }
39495
- }
39496
-
39497
- /*
39498
- * Copyright (C) 2025 Koutaro Mukai
39499
- *
39500
- * This program is free software: you can redistribute it and/or modify
39501
- * it under the terms of the GNU General Public License as published by
39502
- * the Free Software Foundation, either version 3 of the License, or
39503
- * (at your option) any later version.
39504
- *
39505
- * This program is distributed in the hope that it will be useful,
39506
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
39507
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39508
- * GNU General Public License for more details.
39509
- *
39510
- * You should have received a copy of the GNU General Public License
39511
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
39512
- */
39513
- async function pageToImage(page, dpi, alpha) {
39514
- const zoom = dpi / 72;
39515
- const pixmap = page.toPixmap([zoom, 0, 0, zoom, 0, 0], ColorSpace.DeviceRGB, alpha);
39516
- const ret = await Jimp.fromBuffer(new Uint8Array(pixmap.asPNG()).buffer);
39517
- pixmap.destroy();
39518
- page.destroy();
39519
- return ret;
39520
- }
39521
-
39522
39440
  /*
39523
39441
  * Copyright (C) 2025 Koutaro Mukai
39524
39442
  *
@@ -39578,6 +39496,32 @@ const formatHex = ([r, g, b, a]) => "#" +
39578
39496
  })
39579
39497
  .join("");
39580
39498
 
39499
+ /*
39500
+ * Copyright (C) 2025 Koutaro Mukai
39501
+ *
39502
+ * This program is free software: you can redistribute it and/or modify
39503
+ * it under the terms of the GNU General Public License as published by
39504
+ * the Free Software Foundation, either version 3 of the License, or
39505
+ * (at your option) any later version.
39506
+ *
39507
+ * This program is distributed in the hope that it will be useful,
39508
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
39509
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39510
+ * GNU General Public License for more details.
39511
+ *
39512
+ * You should have received a copy of the GNU General Public License
39513
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
39514
+ */
39515
+ async function* withIndex(iter, start = 0) {
39516
+ let index = start;
39517
+ for await (const item of iter) {
39518
+ yield [index, item];
39519
+ index++;
39520
+ }
39521
+ }
39522
+
39523
+ const VERSION = "0.2.1";
39524
+
39581
39525
  /*
39582
39526
  * Copyright (C) 2025 Koutaro Mukai
39583
39527
  *
@@ -39604,9 +39548,105 @@ const defaultOptions = {
39604
39548
  deletion: [0xff, 0x57, 0x24, 0xff],
39605
39549
  modification: [0xff, 0xc1, 0x05, 0xff],
39606
39550
  },
39551
+ workers: 1,
39607
39552
  };
39553
+ function asSharedBytes(bytes) {
39554
+ const isNode = typeof globalThis.process !== "undefined" &&
39555
+ !!globalThis.process.versions?.node;
39556
+ const coiOk = globalThis.crossOriginIsolated ===
39557
+ true;
39558
+ if (typeof SharedArrayBuffer !== "undefined" && (isNode || coiOk)) {
39559
+ const sab = new SharedArrayBuffer(bytes.byteLength);
39560
+ const view = new Uint8Array(sab);
39561
+ view.set(bytes);
39562
+ return view;
39563
+ }
39564
+ return new Uint8Array(bytes);
39565
+ }
39566
+ class WorkerHandle {
39567
+ worker;
39568
+ loaded;
39569
+ pendingResolve = null;
39570
+ pendingReject = null;
39571
+ constructor(url) {
39572
+ this.worker = new Worker(url, { type: "module" });
39573
+ this.loaded = new Promise((resolveLoaded, rejectLoaded) => {
39574
+ const onMessage = (e) => {
39575
+ const data = e.data;
39576
+ if (data.type === "loaded") {
39577
+ resolveLoaded();
39578
+ return;
39579
+ }
39580
+ const resolve = this.pendingResolve;
39581
+ const reject = this.pendingReject;
39582
+ this.pendingResolve = null;
39583
+ this.pendingReject = null;
39584
+ if (data.type === "error") {
39585
+ reject?.(new Error(`worker: ${data.message}`));
39586
+ }
39587
+ else {
39588
+ resolve?.(data);
39589
+ }
39590
+ };
39591
+ this.worker.addEventListener("message", onMessage);
39592
+ this.worker.addEventListener("error", (e) => {
39593
+ const err = e.error ?? new Error(e.message);
39594
+ rejectLoaded(err);
39595
+ const reject = this.pendingReject;
39596
+ this.pendingResolve = null;
39597
+ this.pendingReject = null;
39598
+ reject?.(err);
39599
+ });
39600
+ });
39601
+ }
39602
+ async init(msg) {
39603
+ await this.loaded;
39604
+ return new Promise((resolve, reject) => {
39605
+ this.pendingResolve = resolve;
39606
+ this.pendingReject = reject;
39607
+ this.worker.postMessage(msg);
39608
+ });
39609
+ }
39610
+ processPage(index) {
39611
+ return new Promise((resolve, reject) => {
39612
+ this.pendingResolve = resolve;
39613
+ this.pendingReject = reject;
39614
+ const msg = { type: "page", index };
39615
+ this.worker.postMessage(msg);
39616
+ });
39617
+ }
39618
+ terminate() {
39619
+ this.worker.terminate();
39620
+ }
39621
+ }
39622
+ function workerUrl() {
39623
+ const file = import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js";
39624
+ return new URL(`${file}?v=${encodeURIComponent(VERSION)}`, import.meta.url);
39625
+ }
39626
+ function pageResultToResult(msg) {
39627
+ return {
39628
+ a: Jimp.fromBitmap({
39629
+ width: msg.a.width,
39630
+ height: msg.a.height,
39631
+ data: new Uint8Array(msg.a.data),
39632
+ }),
39633
+ b: Jimp.fromBitmap({
39634
+ width: msg.b.width,
39635
+ height: msg.b.height,
39636
+ data: new Uint8Array(msg.b.data),
39637
+ }),
39638
+ diff: Jimp.fromBitmap({
39639
+ width: msg.diff.width,
39640
+ height: msg.diff.height,
39641
+ data: new Uint8Array(msg.diff.data),
39642
+ }),
39643
+ addition: msg.addition,
39644
+ deletion: msg.deletion,
39645
+ modification: msg.modification,
39646
+ };
39647
+ }
39608
39648
  async function* visualizeDifferences(a, b, options) {
39609
- const mergedOptions = {
39649
+ const merged = {
39610
39650
  dpi: options?.dpi ?? defaultOptions.dpi,
39611
39651
  alpha: options?.alpha ?? defaultOptions.alpha,
39612
39652
  mask: options?.mask ?? defaultOptions.mask,
@@ -39616,76 +39656,92 @@ async function* visualizeDifferences(a, b, options) {
39616
39656
  deletion: options?.pallet?.deletion ?? defaultOptions.pallet.deletion,
39617
39657
  modification: options?.pallet?.modification ?? defaultOptions.pallet.modification,
39618
39658
  },
39659
+ workers: options?.workers ?? defaultOptions.workers,
39619
39660
  };
39620
- const pdfA = PDFDocument.openDocument(a, "application/pdf");
39621
- const pdfB = PDFDocument.openDocument(b, "application/pdf");
39622
- const pdfMask = typeof mergedOptions.mask !== "undefined"
39623
- ? PDFDocument.openDocument(mergedOptions.mask, "application/pdf")
39661
+ const probe = PDFDocument.openDocument(a, "application/pdf");
39662
+ const probeB = PDFDocument.openDocument(b, "application/pdf");
39663
+ const probeMask = typeof merged.mask !== "undefined"
39664
+ ? PDFDocument.openDocument(merged.mask, "application/pdf")
39624
39665
  : new PDFDocument();
39625
- const maxPages = Math.max(pdfA.countPages(), pdfB.countPages(), pdfMask.countPages());
39626
- async function processPage(pageIndex) {
39627
- const [pageA, pageB, pageMask] = await Promise.all([
39628
- pageIndex < pdfA.countPages()
39629
- ? pageToImage(pdfA.loadPage(pageIndex), mergedOptions.dpi, mergedOptions.alpha)
39630
- : createEmptyImage(1, 1),
39631
- pageIndex < pdfB.countPages()
39632
- ? pageToImage(pdfB.loadPage(pageIndex), mergedOptions.dpi, mergedOptions.alpha)
39633
- : createEmptyImage(1, 1),
39634
- pageIndex < pdfMask.countPages()
39635
- ? pageToImage(pdfMask.loadPage(pageIndex), mergedOptions.dpi, mergedOptions.alpha)
39636
- : createEmptyImage(1, 1),
39637
- ]);
39638
- // NOTE: getBufferはcopyなので、Workerに移譲した後もa, bを使用して問題ない
39639
- // https://github.com/jimp-dev/jimp/blob/b6b0e418a5f1259211a133b20cddb4f4e5c25679/packages/core/src/index.ts#L444
39640
- const [bufA, bufB, bufMask] = await Promise.all([
39641
- pageA
39642
- .getBuffer(JimpMime.png)
39643
- .then((buf) => new Uint8Array(buf).buffer),
39644
- pageB
39645
- .getBuffer(JimpMime.png)
39646
- .then((buf) => new Uint8Array(buf).buffer),
39647
- pageMask
39648
- .getBuffer(JimpMime.png)
39649
- .then((buf) => new Uint8Array(buf).buffer),
39650
- ]);
39651
- const { bufDiff, addition, deletion, modification } = (await new Promise((resolve, reject) => {
39652
- const url = new URL("./worker.js", import.meta.url);
39653
- const worker = new Worker(url, { type: "module" });
39654
- worker.addEventListener("message", (e) => {
39655
- resolve(e.data);
39656
- worker.terminate();
39657
- });
39658
- worker.addEventListener("error", (e) => {
39659
- reject(e);
39660
- worker.terminate();
39661
- });
39662
- worker.postMessage({
39663
- bufA,
39664
- bufB,
39665
- bufMask,
39666
- pallet: mergedOptions.pallet,
39667
- align: mergedOptions.align,
39668
- }, [bufA, bufB, bufMask]);
39669
- }));
39670
- const diff = await Jimp.fromBuffer(bufDiff);
39671
- return { a: pageA, b: pageB, diff, addition, deletion, modification };
39672
- }
39673
- // ページ処理を並列発行し、順序を保証して出力
39674
- const concurrency = navigator.hardwareConcurrency;
39675
- const pending = /** @type {Promise<VisualizeDifferencesResult>[]} */ [];
39676
- let nextPageToProcess = 0;
39677
- let nextPageToYield = 0;
39678
- while (nextPageToYield < maxPages) {
39679
- // プールに空きがあれば新しいページ処理を追加
39680
- while (nextPageToProcess < maxPages && pending.length < concurrency) {
39681
- pending.push(processPage(nextPageToProcess));
39682
- nextPageToProcess++;
39666
+ const maxPages = Math.max(probe.countPages(), probeB.countPages(), probeMask.countPages());
39667
+ probe.destroy();
39668
+ probeB.destroy();
39669
+ probeMask.destroy();
39670
+ if (maxPages === 0)
39671
+ return;
39672
+ const aBytes = asSharedBytes(a);
39673
+ const bBytes = asSharedBytes(b);
39674
+ const maskBytes = typeof merged.mask !== "undefined" ? asSharedBytes(merged.mask) : null;
39675
+ const initMsg = {
39676
+ type: "init",
39677
+ aBytes,
39678
+ bBytes,
39679
+ maskBytes,
39680
+ dpi: merged.dpi,
39681
+ alpha: merged.alpha,
39682
+ pallet: merged.pallet,
39683
+ align: merged.align,
39684
+ };
39685
+ const N = Math.max(1, Math.min(merged.workers, maxPages));
39686
+ const url = workerUrl();
39687
+ const worker0 = new WorkerHandle(url);
39688
+ await worker0.init(initMsg);
39689
+ const buffered = new Map();
39690
+ let nextToAssign = 0;
39691
+ const workers = [worker0];
39692
+ for (let i = 1; i < N; i++) {
39693
+ const w = new WorkerHandle(url);
39694
+ await w.init(initMsg);
39695
+ workers.push(w);
39696
+ }
39697
+ const resolvers = new Map();
39698
+ let workerError = null;
39699
+ const loops = workers.map(async (w) => {
39700
+ while (nextToAssign < maxPages && workerError === null) {
39701
+ const idx = nextToAssign++;
39702
+ try {
39703
+ const msg = await w.processPage(idx);
39704
+ const result = pageResultToResult(msg);
39705
+ const resolve = resolvers.get(idx);
39706
+ if (resolve) {
39707
+ resolvers.delete(idx);
39708
+ resolve(result);
39709
+ }
39710
+ else {
39711
+ buffered.set(idx, result);
39712
+ }
39713
+ }
39714
+ catch (e) {
39715
+ workerError = e;
39716
+ for (const [, resolve] of resolvers)
39717
+ resolve(null);
39718
+ resolvers.clear();
39719
+ return;
39720
+ }
39721
+ }
39722
+ });
39723
+ try {
39724
+ for (let i = 0; i < maxPages; i++) {
39725
+ if (workerError !== null)
39726
+ throw workerError;
39727
+ let r;
39728
+ const buf = buffered.get(i);
39729
+ if (buf !== undefined) {
39730
+ buffered.delete(i);
39731
+ r = buf;
39732
+ }
39733
+ else {
39734
+ r = await new Promise((resolve) => resolvers.set(i, resolve));
39735
+ if (workerError !== null)
39736
+ throw workerError;
39737
+ }
39738
+ yield r;
39683
39739
  }
39684
- // 次に出力すべきページのPromiseを待つ
39685
- const result = await pending[0];
39686
- pending.shift();
39687
- yield result;
39688
- nextPageToYield++;
39740
+ await Promise.all(loops);
39741
+ }
39742
+ finally {
39743
+ for (const w of workers)
39744
+ w.terminate();
39689
39745
  }
39690
39746
  }
39691
39747
 
@@ -39705,7 +39761,7 @@ async function* visualizeDifferences(a, b, options) {
39705
39761
  * You should have received a copy of the GNU General Public License
39706
39762
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
39707
39763
  */
39708
- const { positionals, values: { dpi: dpi_, alpha: alpha_, mask: mask_, align: align_, "addition-color": additionColorHex, "deletion-color": deletionColorHex, "modification-color": modificationColorHex, version, help, }, } = util$2.parseArgs({
39764
+ const { positionals, values: { dpi: dpi_, alpha: alpha_, mask: mask_, align: align_, "addition-color": additionColorHex, "deletion-color": deletionColorHex, "modification-color": modificationColorHex, workers: workers_, version, help, }, } = util$2.parseArgs({
39709
39765
  allowPositionals: true,
39710
39766
  options: {
39711
39767
  dpi: { type: "string" },
@@ -39715,6 +39771,7 @@ const { positionals, values: { dpi: dpi_, alpha: alpha_, mask: mask_, align: ali
39715
39771
  "addition-color": { type: "string" },
39716
39772
  "deletion-color": { type: "string" },
39717
39773
  "modification-color": { type: "string" },
39774
+ workers: { type: "string" },
39718
39775
  version: { type: "boolean", short: "v" },
39719
39776
  help: { type: "boolean", short: "h" },
39720
39777
  },
@@ -39733,21 +39790,22 @@ OPTIONS:
39733
39790
  --addition-color <#HEX> default: ${formatHex(defaultOptions.pallet.addition)}
39734
39791
  --deletion-color <#HEX> default: ${formatHex(defaultOptions.pallet.deletion)}
39735
39792
  --modification-color <#HEX> default: ${formatHex(defaultOptions.pallet.modification)}
39793
+ --workers <N> default: ${defaultOptions.workers}
39736
39794
  -v, --version
39737
39795
  -h, --help
39796
+
39797
+ NOTES:
39798
+ Approximate per-worker memory:
39799
+ a_size_MB + b_size_MB [+ mask_size_MB] (PDF buffers in wasm)
39800
+ + 300 MB (mupdf + V8 base)
39801
+ + (dpi / 150)^2 * 50 MB (pixmap working set)
39802
+ The main process adds ~500 MB - 1 GB (varies with --workers).
39803
+ Choose --workers so the total stays under ~80% of available memory.
39738
39804
  `);
39739
39805
  process.exit(0);
39740
39806
  }
39741
39807
  if (version) {
39742
- try {
39743
- const versionStr = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), {
39744
- encoding: "utf-8",
39745
- })).version;
39746
- console.log(versionStr);
39747
- }
39748
- catch {
39749
- console.log("unknown");
39750
- }
39808
+ console.log(VERSION);
39751
39809
  process.exit(0);
39752
39810
  }
39753
39811
  if (positionals.length !== 3) {
@@ -39782,6 +39840,12 @@ if (additionColor === null ||
39782
39840
  modificationColor === null) {
39783
39841
  throw new Error("Invalid color format");
39784
39842
  }
39843
+ const workers = typeof workers_ !== "undefined"
39844
+ ? parseInt(workers_, 10)
39845
+ : defaultOptions.workers;
39846
+ if (Number.isNaN(workers) || workers < 1) {
39847
+ throw new Error("Invalid workers value");
39848
+ }
39785
39849
  fs.mkdirSync(outDir, { recursive: true });
39786
39850
  for await (const [i, { a, b, diff, addition, deletion, modification },] of withIndex(visualizeDifferences(pdfA, pdfB, {
39787
39851
  dpi,
@@ -39793,6 +39857,7 @@ for await (const [i, { a, b, diff, addition, deletion, modification },] of withI
39793
39857
  deletion: deletionColor,
39794
39858
  modification: modificationColor,
39795
39859
  },
39860
+ workers,
39796
39861
  }), 1)) {
39797
39862
  console.log(`Page ${i}, Addition: ${addition.length}, Deletion: ${deletion.length}, Modification: ${modification.length}`);
39798
39863
  const dir = path.join(outDir, i.toString(10));