@u1f992/pdfdiff 0.2.0 → 0.2.2

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.
Files changed (45) hide show
  1. package/.github/workflows/gh-pages.yml +6 -1
  2. package/dist/browser.js +1219 -19
  3. package/dist/browser.js.map +1 -1
  4. package/dist/cli-png-worker.d.ts +13 -0
  5. package/dist/cli-png-worker.d.ts.map +1 -0
  6. package/dist/cli-png-worker.js +303 -0
  7. package/dist/cli-png-worker.js.map +1 -0
  8. package/dist/cli.js +240 -26
  9. package/dist/cli.js.map +1 -1
  10. package/dist/diff.d.ts +2 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/image.d.ts.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.html +6 -1
  16. package/dist/index.js +123 -15
  17. package/dist/index.js.map +1 -1
  18. package/dist/pdf.d.ts.map +1 -1
  19. package/dist/perf.d.ts +16 -0
  20. package/dist/perf.d.ts.map +1 -0
  21. package/dist/squoosh_png_bg.wasm +0 -0
  22. package/dist/style.css +12 -0
  23. package/dist/transferable.d.ts +7 -0
  24. package/dist/transferable.d.ts.map +1 -0
  25. package/dist/version.d.ts +2 -0
  26. package/dist/version.d.ts.map +1 -0
  27. package/dist/worker.d.ts +9 -0
  28. package/dist/worker.d.ts.map +1 -1
  29. package/dist/worker.js +274 -88
  30. package/dist/worker.js.map +1 -1
  31. package/package.json +5 -2
  32. package/rollup.config.js +20 -0
  33. package/scripts/version.ts +35 -0
  34. package/src/browser.ts +119 -5
  35. package/src/cli-png-worker.ts +59 -0
  36. package/src/cli.ts +106 -21
  37. package/src/diff.ts +99 -34
  38. package/src/image.ts +3 -0
  39. package/src/index.html +6 -1
  40. package/src/index.ts +53 -27
  41. package/src/pdf.ts +9 -1
  42. package/src/perf.ts +94 -0
  43. package/src/style.css +12 -0
  44. package/src/transferable.ts +30 -0
  45. package/src/worker.ts +77 -54
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import util$2 from 'node:util';
5
+ import { Worker as Worker$1 } from 'node:worker_threads';
5
6
  import require$$0, { promises, existsSync } from 'fs';
6
7
  import require$$0$1 from 'util';
7
8
  import require$$1 from 'stream';
@@ -39407,6 +39408,82 @@ globalThis.$libmupdf_device = {
39407
39408
  },
39408
39409
  };
39409
39410
 
39411
+ /*
39412
+ * Copyright (C) 2025 Koutaro Mukai
39413
+ *
39414
+ * This program is free software: you can redistribute it and/or modify
39415
+ * it under the terms of the GNU General Public License as published by
39416
+ * the Free Software Foundation, either version 3 of the License, or
39417
+ * (at your option) any later version.
39418
+ *
39419
+ * This program is distributed in the hope that it will be useful,
39420
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
39421
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39422
+ * GNU General Public License for more details.
39423
+ *
39424
+ * You should have received a copy of the GNU General Public License
39425
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
39426
+ */
39427
+ const _enabled = (() => {
39428
+ try {
39429
+ if (typeof process !== "undefined" &&
39430
+ process.env &&
39431
+ process.env.PDFDIFF_PROFILE === "1") {
39432
+ return true;
39433
+ }
39434
+ }
39435
+ catch {
39436
+ // process not available (e.g. in some browser worker environments)
39437
+ }
39438
+ const g = globalThis;
39439
+ return g.__PDFDIFF_PROFILE__ === true;
39440
+ })();
39441
+ const _counters = Object.create(null);
39442
+ const _NOOP_SPAN = Object.freeze({ stop() { } });
39443
+ const _noop = () => { };
39444
+ const _emptyDump = () => Object.freeze({});
39445
+ const _realPerf = {
39446
+ enabled: true,
39447
+ span(key) {
39448
+ const t0 = performance.now();
39449
+ return {
39450
+ stop() {
39451
+ _counters[key] = (_counters[key] ?? 0) + (performance.now() - t0);
39452
+ },
39453
+ };
39454
+ },
39455
+ incr(key, delta = 1) {
39456
+ _counters[key] = (_counters[key] ?? 0) + delta;
39457
+ },
39458
+ setMax(key, value) {
39459
+ const cur = _counters[key];
39460
+ if (cur === undefined || value > cur)
39461
+ _counters[key] = value;
39462
+ },
39463
+ merge(other) {
39464
+ for (const k of Object.keys(other)) {
39465
+ _counters[k] = (_counters[k] ?? 0) + other[k];
39466
+ }
39467
+ },
39468
+ dump() {
39469
+ return { ..._counters };
39470
+ },
39471
+ reset() {
39472
+ for (const k of Object.keys(_counters))
39473
+ delete _counters[k];
39474
+ },
39475
+ };
39476
+ const _noopPerf = {
39477
+ enabled: false,
39478
+ span: () => _NOOP_SPAN,
39479
+ incr: _noop,
39480
+ setMax: _noop,
39481
+ merge: _noop,
39482
+ dump: _emptyDump,
39483
+ reset: _noop,
39484
+ };
39485
+ const perf = _enabled ? _realPerf : _noopPerf;
39486
+
39410
39487
  /*
39411
39488
  * Copyright (C) 2025 Koutaro Mukai
39412
39489
  *
@@ -39520,6 +39597,8 @@ async function* withIndex(iter, start = 0) {
39520
39597
  }
39521
39598
  }
39522
39599
 
39600
+ const VERSION = "0.2.2";
39601
+
39523
39602
  /*
39524
39603
  * Copyright (C) 2025 Koutaro Mukai
39525
39604
  *
@@ -39563,24 +39642,42 @@ function asSharedBytes(bytes) {
39563
39642
  }
39564
39643
  class WorkerHandle {
39565
39644
  worker;
39645
+ loaded;
39566
39646
  pendingResolve = null;
39567
39647
  pendingReject = null;
39568
39648
  constructor(url) {
39569
39649
  this.worker = new Worker(url, { type: "module" });
39570
- this.worker.addEventListener("message", (e) => {
39571
- const resolve = this.pendingResolve;
39572
- this.pendingResolve = null;
39573
- this.pendingReject = null;
39574
- resolve?.(e.data);
39575
- });
39576
- this.worker.addEventListener("error", (e) => {
39577
- const reject = this.pendingReject;
39578
- this.pendingResolve = null;
39579
- this.pendingReject = null;
39580
- reject?.(e.error ?? new Error(e.message));
39650
+ this.loaded = new Promise((resolveLoaded, rejectLoaded) => {
39651
+ const onMessage = (e) => {
39652
+ const data = e.data;
39653
+ if (data.type === "loaded") {
39654
+ resolveLoaded();
39655
+ return;
39656
+ }
39657
+ const resolve = this.pendingResolve;
39658
+ const reject = this.pendingReject;
39659
+ this.pendingResolve = null;
39660
+ this.pendingReject = null;
39661
+ if (data.type === "error") {
39662
+ reject?.(new Error(`worker: ${data.message}`));
39663
+ }
39664
+ else {
39665
+ resolve?.(data);
39666
+ }
39667
+ };
39668
+ this.worker.addEventListener("message", onMessage);
39669
+ this.worker.addEventListener("error", (e) => {
39670
+ const err = e.error ?? new Error(e.message);
39671
+ rejectLoaded(err);
39672
+ const reject = this.pendingReject;
39673
+ this.pendingResolve = null;
39674
+ this.pendingReject = null;
39675
+ reject?.(err);
39676
+ });
39581
39677
  });
39582
39678
  }
39583
- init(msg) {
39679
+ async init(msg) {
39680
+ await this.loaded;
39584
39681
  return new Promise((resolve, reject) => {
39585
39682
  this.pendingResolve = resolve;
39586
39683
  this.pendingReject = reject;
@@ -39600,10 +39697,12 @@ class WorkerHandle {
39600
39697
  }
39601
39698
  }
39602
39699
  function workerUrl() {
39603
- return new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js", import.meta.url);
39700
+ const file = import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js";
39701
+ return new URL(`${file}?v=${encodeURIComponent(VERSION)}`, import.meta.url);
39604
39702
  }
39605
39703
  function pageResultToResult(msg) {
39606
- return {
39704
+ const sP = perf.span("main.pageResultToResult_ms");
39705
+ const r = {
39607
39706
  a: Jimp.fromBitmap({
39608
39707
  width: msg.a.width,
39609
39708
  height: msg.a.height,
@@ -39623,6 +39722,11 @@ function pageResultToResult(msg) {
39623
39722
  deletion: msg.deletion,
39624
39723
  modification: msg.modification,
39625
39724
  };
39725
+ sP.stop();
39726
+ perf.incr("main.resultsReceived");
39727
+ if (msg.perf)
39728
+ perf.merge(msg.perf);
39729
+ return r;
39626
39730
  }
39627
39731
  async function* visualizeDifferences(a, b, options) {
39628
39732
  const merged = {
@@ -39688,6 +39792,7 @@ async function* visualizeDifferences(a, b, options) {
39688
39792
  }
39689
39793
  else {
39690
39794
  buffered.set(idx, result);
39795
+ perf.setMax("main.bufferedPeak", buffered.size);
39691
39796
  }
39692
39797
  }
39693
39798
  catch (e) {
@@ -39710,11 +39815,15 @@ async function* visualizeDifferences(a, b, options) {
39710
39815
  r = buf;
39711
39816
  }
39712
39817
  else {
39818
+ const sWait = perf.span("main.yieldWaitMain_ms");
39713
39819
  r = await new Promise((resolve) => resolvers.set(i, resolve));
39820
+ sWait.stop();
39714
39821
  if (workerError !== null)
39715
39822
  throw workerError;
39716
39823
  }
39824
+ const sYield = perf.span("main.consumerTime_ms");
39717
39825
  yield r;
39826
+ sYield.stop();
39718
39827
  }
39719
39828
  await Promise.all(loops);
39720
39829
  }
@@ -39740,6 +39849,83 @@ async function* visualizeDifferences(a, b, options) {
39740
39849
  * You should have received a copy of the GNU General Public License
39741
39850
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
39742
39851
  */
39852
+ /**
39853
+ * Slice a typed-array view into a standalone backing buffer of the same kind
39854
+ * (ArrayBuffer in, ArrayBuffer out; SharedArrayBuffer in, SharedArrayBuffer
39855
+ * out). The buffer kind is preserved through the generic parameter.
39856
+ */
39857
+ function sliceBackingBuffer(src) {
39858
+ return src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength);
39859
+ }
39860
+
39861
+ /*
39862
+ * Copyright (C) 2025 Koutaro Mukai
39863
+ *
39864
+ * This program is free software: you can redistribute it and/or modify
39865
+ * it under the terms of the GNU General Public License as published by
39866
+ * the Free Software Foundation, either version 3 of the License, or
39867
+ * (at your option) any later version.
39868
+ *
39869
+ * This program is distributed in the hope that it will be useful,
39870
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
39871
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39872
+ * GNU General Public License for more details.
39873
+ *
39874
+ * You should have received a copy of the GNU General Public License
39875
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
39876
+ */
39877
+ class PngWriterPool {
39878
+ workers = [];
39879
+ idle = [];
39880
+ waiting = [];
39881
+ constructor(size, scriptUrl) {
39882
+ for (let i = 0; i < size; i++) {
39883
+ const w = new Worker$1(scriptUrl);
39884
+ this.workers.push(w);
39885
+ this.idle.push(w);
39886
+ }
39887
+ }
39888
+ acquire() {
39889
+ const w = this.idle.pop();
39890
+ if (w)
39891
+ return Promise.resolve(w);
39892
+ return new Promise((resolve) => this.waiting.push(resolve));
39893
+ }
39894
+ release(w) {
39895
+ const next = this.waiting.shift();
39896
+ if (next)
39897
+ next(w);
39898
+ else
39899
+ this.idle.push(w);
39900
+ }
39901
+ async submit(job) {
39902
+ const w = await this.acquire();
39903
+ return new Promise((resolve, reject) => {
39904
+ const onMessage = (msg) => {
39905
+ w.off("message", onMessage);
39906
+ w.off("error", onError);
39907
+ this.release(w);
39908
+ if (msg.ok)
39909
+ resolve();
39910
+ else
39911
+ reject(new Error(msg.error));
39912
+ };
39913
+ const onError = (err) => {
39914
+ w.off("message", onMessage);
39915
+ w.off("error", onError);
39916
+ this.release(w);
39917
+ reject(err);
39918
+ };
39919
+ w.on("message", onMessage);
39920
+ w.once("error", onError);
39921
+ w.postMessage(job, [job.data]);
39922
+ });
39923
+ }
39924
+ async terminate() {
39925
+ await Promise.all(this.workers.map((w) => w.terminate()));
39926
+ }
39927
+ }
39928
+ const _wallSpan = perf.span("cli.wallTotal_ms");
39743
39929
  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({
39744
39930
  allowPositionals: true,
39745
39931
  options: {
@@ -39784,15 +39970,7 @@ NOTES:
39784
39970
  process.exit(0);
39785
39971
  }
39786
39972
  if (version) {
39787
- try {
39788
- const versionStr = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), {
39789
- encoding: "utf-8",
39790
- })).version;
39791
- console.log(versionStr);
39792
- }
39793
- catch {
39794
- console.log("unknown");
39795
- }
39973
+ console.log(VERSION);
39796
39974
  process.exit(0);
39797
39975
  }
39798
39976
  if (positionals.length !== 3) {
@@ -39834,6 +40012,9 @@ if (Number.isNaN(workers) || workers < 1) {
39834
40012
  throw new Error("Invalid workers value");
39835
40013
  }
39836
40014
  fs.mkdirSync(outDir, { recursive: true });
40015
+ const writerPool = new PngWriterPool(workers, new URL("./cli-png-worker.js", import.meta.url));
40016
+ const pendingWrites = [];
40017
+ const _loopSpan = perf.span("cli.loopWall_ms");
39837
40018
  for await (const [i, { a, b, diff, addition, deletion, modification },] of withIndex(visualizeDifferences(pdfA, pdfB, {
39838
40019
  dpi,
39839
40020
  alpha,
@@ -39849,8 +40030,41 @@ for await (const [i, { a, b, diff, addition, deletion, modification },] of withI
39849
40030
  console.log(`Page ${i}, Addition: ${addition.length}, Deletion: ${deletion.length}, Modification: ${modification.length}`);
39850
40031
  const dir = path.join(outDir, i.toString(10));
39851
40032
  fs.mkdirSync(dir, { recursive: true });
39852
- fs.writeFileSync(path.join(dir, "a.png"), new Uint8Array(await a.getBuffer("image/png")));
39853
- fs.writeFileSync(path.join(dir, "b.png"), new Uint8Array(await b.getBuffer("image/png")));
39854
- fs.writeFileSync(path.join(dir, "diff.png"), new Uint8Array(await diff.getBuffer("image/png")));
40033
+ const sSubmit = perf.span("cli.poolSubmit_ms");
40034
+ const aBuf = sliceBackingBuffer(a.bitmap.data);
40035
+ const bBuf = sliceBackingBuffer(b.bitmap.data);
40036
+ const dBuf = sliceBackingBuffer(diff.bitmap.data);
40037
+ pendingWrites.push(writerPool.submit({
40038
+ width: a.width,
40039
+ height: a.height,
40040
+ data: aBuf,
40041
+ path: path.join(dir, "a.png"),
40042
+ }), writerPool.submit({
40043
+ width: b.width,
40044
+ height: b.height,
40045
+ data: bBuf,
40046
+ path: path.join(dir, "b.png"),
40047
+ }), writerPool.submit({
40048
+ width: diff.width,
40049
+ height: diff.height,
40050
+ data: dBuf,
40051
+ path: path.join(dir, "diff.png"),
40052
+ }));
40053
+ sSubmit.stop();
40054
+ }
40055
+ const sDrain = perf.span("cli.poolDrain_ms");
40056
+ await Promise.all(pendingWrites);
40057
+ sDrain.stop();
40058
+ await writerPool.terminate();
40059
+ _loopSpan.stop();
40060
+ _wallSpan.stop();
40061
+ if (perf.enabled) {
40062
+ const counters = perf.dump();
40063
+ process.stderr.write("\n=== PERF ===\n");
40064
+ const keys = Object.keys(counters).sort();
40065
+ const out = {};
40066
+ for (const k of keys)
40067
+ out[k] = Math.round(counters[k] * 1000) / 1000;
40068
+ process.stderr.write(JSON.stringify(out, null, 2) + "\n");
39855
40069
  }
39856
40070
  //# sourceMappingURL=cli.js.map