@u1f992/pdfdiff 0.2.1 → 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/src/pdf.ts CHANGED
@@ -19,6 +19,7 @@ import * as jimp from "jimp";
19
19
  import * as mupdf from "mupdf";
20
20
 
21
21
  import type { JimpInstance } from "./jimp.ts";
22
+ import { perf } from "./perf.ts";
22
23
 
23
24
  export function* loadPages(pdf: mupdf.Document) {
24
25
  for (let i = 0; i < pdf.countPages(); i++) {
@@ -60,6 +61,7 @@ export async function pageToImage(
60
61
  alpha: boolean,
61
62
  ) {
62
63
  const zoom = dpi / 72;
64
+ const sToPixmap = perf.span("pdf.toPixmap_ms");
63
65
  const pixmap = page.toPixmap(
64
66
  [zoom, 0, 0, zoom, 0, 0],
65
67
  mupdf.ColorSpace.DeviceRGB,
@@ -67,8 +69,14 @@ export async function pageToImage(
67
69
  );
68
70
  const width = pixmap.getWidth();
69
71
  const height = pixmap.getHeight();
72
+ sToPixmap.stop();
73
+ const sRgba = perf.span("pdf.pixmapToRGBA_ms");
70
74
  const data = pixmapToRGBA(pixmap);
71
75
  pixmap.destroy();
72
76
  page.destroy();
73
- return jimp.Jimp.fromBitmap({ width, height, data }) as JimpInstance;
77
+ sRgba.stop();
78
+ const sFromBitmap = perf.span("pdf.fromBitmap_ms");
79
+ const result = jimp.Jimp.fromBitmap({ width, height, data }) as JimpInstance;
80
+ sFromBitmap.stop();
81
+ return result;
74
82
  }
package/src/perf.ts ADDED
@@ -0,0 +1,94 @@
1
+ /*
2
+ * Copyright (C) 2025 Koutaro Mukai
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ const _enabled = (() => {
19
+ try {
20
+ if (
21
+ typeof process !== "undefined" &&
22
+ process.env &&
23
+ process.env.PDFDIFF_PROFILE === "1"
24
+ ) {
25
+ return true;
26
+ }
27
+ } catch {
28
+ // process not available (e.g. in some browser worker environments)
29
+ }
30
+ const g = globalThis as { __PDFDIFF_PROFILE__?: boolean };
31
+ return g.__PDFDIFF_PROFILE__ === true;
32
+ })();
33
+
34
+ export type Counters = Readonly<Record<string, number>>;
35
+
36
+ export type Span = { stop(): void };
37
+
38
+ type Perf = {
39
+ readonly enabled: boolean;
40
+ span(key: string): Span;
41
+ incr(key: string, delta?: number): void;
42
+ setMax(key: string, value: number): void;
43
+ merge(other: Counters): void;
44
+ dump(): Counters;
45
+ reset(): void;
46
+ };
47
+
48
+ const _counters: Record<string, number> = Object.create(null);
49
+
50
+ const _NOOP_SPAN: Span = Object.freeze({ stop() {} });
51
+ const _noop = () => {};
52
+ const _emptyDump = (): Counters => Object.freeze({});
53
+
54
+ const _realPerf: Perf = {
55
+ enabled: true,
56
+ span(key) {
57
+ const t0 = performance.now();
58
+ return {
59
+ stop() {
60
+ _counters[key] = (_counters[key] ?? 0) + (performance.now() - t0);
61
+ },
62
+ };
63
+ },
64
+ incr(key, delta = 1) {
65
+ _counters[key] = (_counters[key] ?? 0) + delta;
66
+ },
67
+ setMax(key, value) {
68
+ const cur = _counters[key];
69
+ if (cur === undefined || value > cur) _counters[key] = value;
70
+ },
71
+ merge(other) {
72
+ for (const k of Object.keys(other)) {
73
+ _counters[k] = (_counters[k] ?? 0) + other[k]!;
74
+ }
75
+ },
76
+ dump() {
77
+ return { ..._counters };
78
+ },
79
+ reset() {
80
+ for (const k of Object.keys(_counters)) delete _counters[k];
81
+ },
82
+ };
83
+
84
+ const _noopPerf: Perf = {
85
+ enabled: false,
86
+ span: () => _NOOP_SPAN,
87
+ incr: _noop,
88
+ setMax: _noop,
89
+ merge: _noop,
90
+ dump: _emptyDump,
91
+ reset: _noop,
92
+ };
93
+
94
+ export const perf: Perf = _enabled ? _realPerf : _noopPerf;
package/src/style.css CHANGED
@@ -11,9 +11,21 @@
11
11
  .diff-table {
12
12
  border: 1px solid black;
13
13
  border-collapse: collapse;
14
+ width: 100%;
15
+ table-layout: fixed;
14
16
  }
15
17
 
16
18
  .diff-table th,
17
19
  .diff-table td {
18
20
  border: 1px solid black;
19
21
  }
22
+
23
+ .diff-table img {
24
+ display: block;
25
+ max-width: 100%;
26
+ height: auto;
27
+ }
28
+
29
+ body.hide-no-diff .diff-details.no-diff {
30
+ display: none;
31
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright (C) 2025 Koutaro Mukai
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ /**
19
+ * Slice a typed-array view into a standalone backing buffer of the same kind
20
+ * (ArrayBuffer in, ArrayBuffer out; SharedArrayBuffer in, SharedArrayBuffer
21
+ * out). The buffer kind is preserved through the generic parameter.
22
+ */
23
+ export function sliceBackingBuffer<TArrayBuffer extends ArrayBufferLike>(
24
+ src: Uint8Array<TArrayBuffer> | Uint8ClampedArray<TArrayBuffer>,
25
+ ): TArrayBuffer {
26
+ return src.buffer.slice(
27
+ src.byteOffset,
28
+ src.byteOffset + src.byteLength,
29
+ ) as TArrayBuffer;
30
+ }
package/src/worker.ts CHANGED
@@ -25,6 +25,8 @@ import {
25
25
  } from "./image.ts";
26
26
  import type { JimpInstance } from "./jimp.ts";
27
27
  import { pageToImage } from "./pdf.ts";
28
+ import { perf, type Counters } from "./perf.ts";
29
+ import { sliceBackingBuffer } from "./transferable.ts";
28
30
 
29
31
  export type InitMessage = {
30
32
  type: "init";
@@ -59,6 +61,7 @@ export type PageResultMessage = {
59
61
  addition: [number, number][];
60
62
  deletion: [number, number][];
61
63
  modification: [number, number][];
64
+ perf?: Counters | undefined;
62
65
  };
63
66
 
64
67
  export type ErrorMessage = {
@@ -76,19 +79,8 @@ let opts: {
76
79
  align: AlignStrategy;
77
80
  };
78
81
 
79
- function toTransferable(
80
- src: Buffer | Uint8Array | Uint8ClampedArray | number[],
81
- ): ArrayBuffer {
82
- const view =
83
- src instanceof Uint8Array || src instanceof Uint8ClampedArray
84
- ? src
85
- : Uint8Array.from(src as ArrayLike<number>);
86
- const out = new ArrayBuffer(view.byteLength);
87
- new Uint8Array(out).set(view);
88
- return out;
89
- }
90
-
91
82
  async function processPage(index: number): Promise<PageResultMessage> {
83
+ const sLoad = perf.span("worker.pageToImageAll_ms");
92
84
  const [pageA, pageB, pageMask] = (await Promise.all([
93
85
  index < pdfA.countPages()
94
86
  ? pageToImage(pdfA.loadPage(index), opts.dpi, opts.alpha)
@@ -98,42 +90,52 @@ async function processPage(index: number): Promise<PageResultMessage> {
98
90
  : createEmptyImage(1, 1),
99
91
  index < pdfMask.countPages()
100
92
  ? pageToImage(pdfMask.loadPage(index), opts.dpi, opts.alpha)
101
- : createEmptyImage(1, 1),
102
- ])) as [JimpInstance, JimpInstance, JimpInstance];
93
+ : Promise.resolve(null),
94
+ ])) as [JimpInstance, JimpInstance, JimpInstance | null];
95
+ sLoad.stop();
103
96
 
97
+ const sDiff = perf.span("worker.drawDifference_ms");
104
98
  const {
105
99
  diff: diffLayer,
106
100
  addition,
107
101
  deletion,
108
102
  modification,
103
+ hasDiff,
109
104
  } = drawDifference(pageA, pageB, pageMask, opts.pallet, opts.align);
110
- const diff = composeLayers(pageA.width, pageA.height, [
105
+ sDiff.stop();
106
+
107
+ const sCompose = perf.span("worker.composeLayers_ms");
108
+ const layers: [JimpInstance, number][] = [
111
109
  [pageA, 0.2],
112
110
  [pageB, 0.2],
113
- [diffLayer, 1],
114
- ]);
111
+ ];
112
+ if (hasDiff) layers.push([diffLayer, 1]);
113
+ const diff = composeLayers(pageA.width, pageA.height, layers);
114
+ sCompose.stop();
115
+
116
+ const sXfer = perf.span("worker.toTransferable_ms");
117
+ const aBuf = sliceBackingBuffer(pageA.bitmap.data);
118
+ const bBuf = sliceBackingBuffer(pageB.bitmap.data);
119
+ const dBuf = sliceBackingBuffer(diff.bitmap.data);
120
+ sXfer.stop();
121
+ perf.incr("worker.pages");
122
+
123
+ let pagePerf: Counters | undefined;
124
+ if (perf.enabled) {
125
+ pagePerf = perf.dump();
126
+ perf.reset();
127
+ }
115
128
 
116
129
  return {
117
130
  type: "pageResult",
118
131
  index,
119
- a: {
120
- width: pageA.width,
121
- height: pageA.height,
122
- data: toTransferable(pageA.bitmap.data),
123
- },
124
- b: {
125
- width: pageB.width,
126
- height: pageB.height,
127
- data: toTransferable(pageB.bitmap.data),
128
- },
129
- diff: {
130
- width: diff.width,
131
- height: diff.height,
132
- data: toTransferable(diff.bitmap.data),
133
- },
132
+ a: { width: pageA.width, height: pageA.height, data: aBuf },
133
+ b: { width: pageB.width, height: pageB.height, data: bBuf },
134
+ diff: { width: diff.width, height: diff.height, data: dBuf },
134
135
  addition,
135
136
  deletion,
136
137
  modification,
138
+ perf: pagePerf,
137
139
  };
138
140
  }
139
141