@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.
- package/.github/workflows/gh-pages.yml +6 -1
- package/dist/browser.js +1219 -19
- package/dist/browser.js.map +1 -1
- package/dist/cli-png-worker.d.ts +13 -0
- package/dist/cli-png-worker.d.ts.map +1 -0
- package/dist/cli-png-worker.js +303 -0
- package/dist/cli-png-worker.js.map +1 -0
- package/dist/cli.js +240 -26
- package/dist/cli.js.map +1 -1
- package/dist/diff.d.ts +2 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +6 -1
- package/dist/index.js +123 -15
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts.map +1 -1
- package/dist/perf.d.ts +16 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/squoosh_png_bg.wasm +0 -0
- package/dist/style.css +12 -0
- package/dist/transferable.d.ts +7 -0
- package/dist/transferable.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/worker.d.ts +9 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +274 -88
- package/dist/worker.js.map +1 -1
- package/package.json +5 -2
- package/rollup.config.js +20 -0
- package/scripts/version.ts +35 -0
- package/src/browser.ts +119 -5
- package/src/cli-png-worker.ts +59 -0
- package/src/cli.ts +106 -21
- package/src/diff.ts +99 -34
- package/src/image.ts +3 -0
- package/src/index.html +6 -1
- package/src/index.ts +53 -27
- package/src/pdf.ts +9 -1
- package/src/perf.ts +94 -0
- package/src/style.css +12 -0
- package/src/transferable.ts +30 -0
- 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.
|
|
39571
|
-
const
|
|
39572
|
-
|
|
39573
|
-
|
|
39574
|
-
|
|
39575
|
-
|
|
39576
|
-
|
|
39577
|
-
|
|
39578
|
-
|
|
39579
|
-
|
|
39580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39853
|
-
|
|
39854
|
-
|
|
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
|