@u1f992/pdfdiff 0.2.2 → 0.3.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.
Files changed (65) hide show
  1. package/.clang-format +3 -0
  2. package/LICENSE +68 -81
  3. package/dist/browser.js +239 -3109
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cli-png-worker.d.ts.map +1 -1
  6. package/dist/cli-png-worker.js +0 -16
  7. package/dist/cli-png-worker.js.map +1 -1
  8. package/dist/cli.js +240 -3150
  9. package/dist/cli.js.map +1 -1
  10. package/dist/core.wasm +0 -0
  11. package/dist/decode.d.ts +9 -0
  12. package/dist/decode.d.ts.map +1 -0
  13. package/dist/diff.d.ts.map +1 -1
  14. package/dist/gs-wasm/gs.js +5821 -0
  15. package/dist/gs-wasm/gs.wasm +0 -0
  16. package/dist/gs-wasm/index.js +120 -0
  17. package/dist/gs-wasm/index.js.map +1 -0
  18. package/dist/gs-wasm/worker.js +764 -0
  19. package/dist/gs-wasm/worker.js.map +1 -0
  20. package/dist/image.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.html +1 -1
  24. package/dist/index.js +238 -3109
  25. package/dist/index.js.map +1 -1
  26. package/dist/iterable.d.ts.map +1 -1
  27. package/dist/jimp.d.ts +23 -1
  28. package/dist/jimp.d.ts.map +1 -1
  29. package/dist/pdf.d.ts +15 -4
  30. package/dist/pdf.d.ts.map +1 -1
  31. package/dist/perf.d.ts.map +1 -1
  32. package/dist/rgba-color.d.ts.map +1 -1
  33. package/dist/transferable.d.ts +6 -2
  34. package/dist/transferable.d.ts.map +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/worker.d.ts +6 -8
  37. package/dist/worker.d.ts.map +1 -1
  38. package/dist/worker.js +70 -3311
  39. package/dist/worker.js.map +1 -1
  40. package/package.json +9 -4
  41. package/rollup.config.js +63 -5
  42. package/scripts/build-wasm.sh +32 -0
  43. package/src/browser.ts +9 -6
  44. package/src/cli-png-worker.ts +0 -17
  45. package/src/cli.ts +9 -23
  46. package/src/decode.ts +15 -0
  47. package/src/diff.ts +0 -17
  48. package/src/image.ts +1 -18
  49. package/src/index.html +1 -1
  50. package/src/index.test.ts +10 -18
  51. package/src/index.ts +163 -74
  52. package/src/iterable.test.ts +0 -17
  53. package/src/iterable.ts +0 -17
  54. package/src/jimp.ts +25 -7
  55. package/src/pdf.ts +98 -69
  56. package/src/perf.ts +0 -17
  57. package/src/rgba-color.test.ts +0 -17
  58. package/src/rgba-color.ts +0 -17
  59. package/src/transferable.ts +6 -21
  60. package/src/worker.ts +91 -87
  61. package/wasm/Makefile +34 -0
  62. package/wasm/bindings.cpp +76 -0
  63. package/wasm/core.c +176 -0
  64. package/wasm/core.h +69 -0
  65. package/dist/mupdf-wasm.wasm +0 -0
@@ -1,28 +1,13 @@
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
1
  /**
19
- * Slice a typed-array view into a standalone backing buffer of the same kind
2
+ * Slice an ArrayBufferView into a standalone backing buffer of the same kind
20
3
  * (ArrayBuffer in, ArrayBuffer out; SharedArrayBuffer in, SharedArrayBuffer
21
4
  * out). The buffer kind is preserved through the generic parameter.
22
5
  */
23
- export function sliceBackingBuffer<TArrayBuffer extends ArrayBufferLike>(
24
- src: Uint8Array<TArrayBuffer> | Uint8ClampedArray<TArrayBuffer>,
25
- ): TArrayBuffer {
6
+ export function sliceBackingBuffer<TArrayBuffer extends ArrayBufferLike>(src: {
7
+ buffer: TArrayBuffer;
8
+ byteOffset: number;
9
+ byteLength: number;
10
+ }): TArrayBuffer {
26
11
  return src.buffer.slice(
27
12
  src.byteOffset,
28
13
  src.byteOffset + src.byteLength,
package/src/worker.ts CHANGED
@@ -1,40 +1,14 @@
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
- import * as mupdf from "mupdf";
19
-
20
- import { drawDifference, type Pallet } from "./diff.ts";
21
- import {
22
- composeLayers,
23
- createEmptyImage,
24
- type AlignStrategy,
25
- } from "./image.ts";
1
+ import { decodePng } from "./decode.ts";
2
+ import { type Pallet } from "./diff.ts";
3
+ import { alignSize, createEmptyImage, type AlignStrategy } from "./image.ts";
26
4
  import type { JimpInstance } from "./jimp.ts";
27
- import { pageToImage } from "./pdf.ts";
28
5
  import { perf, type Counters } from "./perf.ts";
6
+ import { type RGBAColor } from "./rgba-color.ts";
29
7
  import { sliceBackingBuffer } from "./transferable.ts";
8
+ import createWasmModule, { type MainModule } from "./wasm/core.js";
30
9
 
31
10
  export type InitMessage = {
32
11
  type: "init";
33
- aBytes: Uint8Array;
34
- bBytes: Uint8Array;
35
- maskBytes: Uint8Array | null;
36
- dpi: number;
37
- alpha: boolean;
38
12
  pallet: Pallet;
39
13
  align: AlignStrategy;
40
14
  };
@@ -42,6 +16,11 @@ export type InitMessage = {
42
16
  export type PageMessage = {
43
17
  type: "page";
44
18
  index: number;
19
+ // PNG bytes rendered on the main thread, or null when the source PDF has no
20
+ // such page (the diff then treats it as an empty/transparent page).
21
+ a: ArrayBuffer | null;
22
+ b: ArrayBuffer | null;
23
+ mask: ArrayBuffer | null;
45
24
  };
46
25
 
47
26
  export type LoadedMessage = {
@@ -58,9 +37,9 @@ export type PageResultMessage = {
58
37
  a: { width: number; height: number; data: ArrayBuffer };
59
38
  b: { width: number; height: number; data: ArrayBuffer };
60
39
  diff: { width: number; height: number; data: ArrayBuffer };
61
- addition: [number, number][];
62
- deletion: [number, number][];
63
- modification: [number, number][];
40
+ addition: ArrayBuffer;
41
+ deletion: ArrayBuffer;
42
+ modification: ArrayBuffer;
64
43
  perf?: Counters | undefined;
65
44
  };
66
45
 
@@ -69,54 +48,83 @@ export type ErrorMessage = {
69
48
  message: string;
70
49
  };
71
50
 
72
- let pdfA: mupdf.Document;
73
- let pdfB: mupdf.Document;
74
- let pdfMask: mupdf.Document;
51
+ type WasmProcessResult = {
52
+ overlay: Uint8Array<ArrayBuffer>;
53
+ addition: Int32Array<ArrayBuffer>;
54
+ deletion: Int32Array<ArrayBuffer>;
55
+ modification: Int32Array<ArrayBuffer>;
56
+ };
57
+
75
58
  let opts: {
76
- dpi: number;
77
- alpha: boolean;
78
59
  pallet: Pallet;
79
60
  align: AlignStrategy;
80
61
  };
81
62
 
82
- async function processPage(index: number): Promise<PageResultMessage> {
83
- const sLoad = perf.span("worker.pageToImageAll_ms");
84
- const [pageA, pageB, pageMask] = (await Promise.all([
85
- index < pdfA.countPages()
86
- ? pageToImage(pdfA.loadPage(index), opts.dpi, opts.alpha)
87
- : createEmptyImage(1, 1),
88
- index < pdfB.countPages()
89
- ? pageToImage(pdfB.loadPage(index), opts.dpi, opts.alpha)
90
- : createEmptyImage(1, 1),
91
- index < pdfMask.countPages()
92
- ? pageToImage(pdfMask.loadPage(index), opts.dpi, opts.alpha)
93
- : Promise.resolve(null),
63
+ let wasm: MainModule | null = null;
64
+ async function getWasm(): Promise<MainModule> {
65
+ if (!wasm) wasm = await createWasmModule();
66
+ return wasm;
67
+ }
68
+
69
+ function packColor([r, g, b, a]: RGBAColor): number {
70
+ return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
71
+ }
72
+
73
+ async function processPage(msg: PageMessage): Promise<PageResultMessage> {
74
+ const index = msg.index;
75
+ const sLoad = perf.span("worker.decodeAll_ms");
76
+ const [pageA, pageB, pageMaskOrNull] = (await Promise.all([
77
+ msg.a !== null ? decodePng(msg.a) : createEmptyImage(1, 1),
78
+ msg.b !== null ? decodePng(msg.b) : createEmptyImage(1, 1),
79
+ msg.mask !== null ? decodePng(msg.mask) : Promise.resolve(null),
94
80
  ])) as [JimpInstance, JimpInstance, JimpInstance | null];
95
81
  sLoad.stop();
96
82
 
97
- const sDiff = perf.span("worker.drawDifference_ms");
98
- const {
99
- diff: diffLayer,
100
- addition,
101
- deletion,
102
- modification,
103
- hasDiff,
104
- } = drawDifference(pageA, pageB, pageMask, opts.pallet, opts.align);
105
- sDiff.stop();
106
-
107
- const sCompose = perf.span("worker.composeLayers_ms");
108
- const layers: [JimpInstance, number][] = [
109
- [pageA, 0.2],
110
- [pageB, 0.2],
111
- ];
112
- if (hasDiff) layers.push([diffLayer, 1]);
113
- const diff = composeLayers(pageA.width, pageA.height, layers);
114
- sCompose.stop();
83
+ const sAlign = perf.span("worker.alignSize_ms");
84
+ let aAligned: JimpInstance;
85
+ let bAligned: JimpInstance;
86
+ let maskAligned: JimpInstance | null;
87
+ if (pageMaskOrNull !== null) {
88
+ [aAligned, bAligned, maskAligned] = alignSize(
89
+ [pageA, pageB, pageMaskOrNull],
90
+ opts.align,
91
+ );
92
+ } else {
93
+ [aAligned, bAligned] = alignSize([pageA, pageB], opts.align);
94
+ maskAligned = null;
95
+ }
96
+ sAlign.stop();
97
+
98
+ const width = aAligned.width;
99
+ const height = aAligned.height;
100
+ const aData = aAligned.bitmap.data;
101
+ const bData = bAligned.bitmap.data;
102
+ const maskData = maskAligned !== null ? maskAligned.bitmap.data : null;
103
+
104
+ const sProcess = perf.span("worker.processPage_ms");
105
+ const wasmModule = await getWasm();
106
+ const result = wasmModule.processPage(
107
+ aData,
108
+ bData,
109
+ maskData,
110
+ width,
111
+ height,
112
+ packColor(opts.pallet.addition),
113
+ packColor(opts.pallet.deletion),
114
+ packColor(opts.pallet.modification),
115
+ ) as WasmProcessResult | number;
116
+ if (typeof result === "number") {
117
+ throw new Error(`wasm processPage failed: ${result}`);
118
+ }
119
+ sProcess.stop();
115
120
 
116
121
  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);
122
+ const aBuf = sliceBackingBuffer(aData);
123
+ const bBuf = sliceBackingBuffer(bData);
124
+ const dBuf = sliceBackingBuffer(result.overlay);
125
+ const addBuf = sliceBackingBuffer(result.addition);
126
+ const delBuf = sliceBackingBuffer(result.deletion);
127
+ const modBuf = sliceBackingBuffer(result.modification);
120
128
  sXfer.stop();
121
129
  perf.incr("worker.pages");
122
130
 
@@ -129,12 +137,12 @@ async function processPage(index: number): Promise<PageResultMessage> {
129
137
  return {
130
138
  type: "pageResult",
131
139
  index,
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 },
135
- addition,
136
- deletion,
137
- modification,
140
+ a: { width, height, data: aBuf },
141
+ b: { width, height, data: bBuf },
142
+ diff: { width, height, data: dBuf },
143
+ addition: addBuf,
144
+ deletion: delBuf,
145
+ modification: modBuf,
138
146
  perf: pagePerf,
139
147
  };
140
148
  }
@@ -145,26 +153,22 @@ self.addEventListener(
145
153
  try {
146
154
  const msg = e.data;
147
155
  if (msg.type === "init") {
148
- pdfA = mupdf.PDFDocument.openDocument(msg.aBytes, "application/pdf");
149
- pdfB = mupdf.PDFDocument.openDocument(msg.bBytes, "application/pdf");
150
- pdfMask = msg.maskBytes
151
- ? mupdf.PDFDocument.openDocument(msg.maskBytes, "application/pdf")
152
- : new mupdf.PDFDocument();
153
156
  opts = {
154
- dpi: msg.dpi,
155
- alpha: msg.alpha,
156
157
  pallet: msg.pallet,
157
158
  align: msg.align,
158
159
  };
159
- if (pdfA.countPages() > 0) pdfA.loadPage(0).destroy();
160
+ await getWasm();
160
161
  const ready: ReadyMessage = { type: "ready" };
161
162
  self.postMessage(ready);
162
163
  } else if (msg.type === "page") {
163
- const result = await processPage(msg.index);
164
+ const result = await processPage(msg);
164
165
  self.postMessage(result, [
165
166
  result.a.data,
166
167
  result.b.data,
167
168
  result.diff.data,
169
+ result.addition,
170
+ result.deletion,
171
+ result.modification,
168
172
  ]);
169
173
  }
170
174
  } catch (err) {
package/wasm/Makefile ADDED
@@ -0,0 +1,34 @@
1
+ OUT_DIR := ../src/wasm
2
+ C_SOURCES := core.c
3
+ CPP_SOURCES := bindings.cpp
4
+ HEADERS := core.h
5
+
6
+ EMFLAGS := \
7
+ -lembind \
8
+ -s MODULARIZE=1 \
9
+ -s EXPORT_ES6=1 \
10
+ -s ALLOW_MEMORY_GROWTH=1 \
11
+ --emit-tsd core.d.ts \
12
+ -O3
13
+
14
+ all: $(OUT_DIR)/core.js
15
+
16
+ $(OUT_DIR)/core.js: $(C_SOURCES) $(CPP_SOURCES) $(HEADERS)
17
+ mkdir -p $(OUT_DIR)
18
+ em++ $(C_SOURCES) $(CPP_SOURCES) -o $@ $(EMFLAGS)
19
+
20
+ debug: $(C_SOURCES) $(CPP_SOURCES) $(HEADERS)
21
+ mkdir -p $(OUT_DIR)
22
+ em++ $(C_SOURCES) $(CPP_SOURCES) -o $(OUT_DIR)/core.js $(EMFLAGS) -DCORE_DEBUG -g -O0
23
+
24
+ format:
25
+ clang-format -i $(C_SOURCES) $(CPP_SOURCES) $(HEADERS)
26
+
27
+ tidy:
28
+ clang-tidy $(C_SOURCES) -- -I.
29
+ @echo "Note: $(CPP_SOURCES) skipped (requires Emscripten include paths)"
30
+
31
+ clean:
32
+ rm -rf $(OUT_DIR)
33
+
34
+ .PHONY: all debug format tidy clean
@@ -0,0 +1,76 @@
1
+ #include <emscripten/bind.h>
2
+ #include <emscripten/val.h>
3
+
4
+ #include <cstdint>
5
+ #include <vector>
6
+
7
+ #include "core.h"
8
+
9
+ using emscripten::val;
10
+
11
+ static CoreColor unpackColor(uint32_t packed) {
12
+ CoreColor c;
13
+ c.r = (uint8_t)((packed >> 24) & 0xff);
14
+ c.g = (uint8_t)((packed >> 16) & 0xff);
15
+ c.b = (uint8_t)((packed >> 8) & 0xff);
16
+ c.a = (uint8_t)(packed & 0xff);
17
+ return c;
18
+ }
19
+
20
+ val processPage(val aPixelsVal, val bPixelsVal, val maskPixelsVal,
21
+ int32_t width, int32_t height, uint32_t additionPacked,
22
+ uint32_t deletionPacked, uint32_t modificationPacked) {
23
+ if (width <= 0 || height <= 0 ||
24
+ (int64_t)width * (int64_t)height > INT32_MAX) {
25
+ return val(CORE_ERROR_INVALID);
26
+ }
27
+
28
+ std::vector<uint8_t> a =
29
+ emscripten::convertJSArrayToNumberVector<uint8_t>(aPixelsVal);
30
+ std::vector<uint8_t> b =
31
+ emscripten::convertJSArrayToNumberVector<uint8_t>(bPixelsVal);
32
+
33
+ bool hasMask = !maskPixelsVal.isNull() && !maskPixelsVal.isUndefined();
34
+ std::vector<uint8_t> mask;
35
+ if (hasMask) {
36
+ mask = emscripten::convertJSArrayToNumberVector<uint8_t>(maskPixelsVal);
37
+ }
38
+
39
+ CorePallet pallet = {unpackColor(additionPacked), unpackColor(deletionPacked),
40
+ unpackColor(modificationPacked)};
41
+
42
+ CoreResult result;
43
+ int32_t rc = process_page(a.data(), b.data(), hasMask ? mask.data() : nullptr,
44
+ width, height, &pallet, &result);
45
+ if (rc != CORE_OK) {
46
+ return val(rc);
47
+ }
48
+
49
+ size_t pixelByteCount = (size_t)width * (size_t)height * 4;
50
+
51
+ val obj = val::object();
52
+
53
+ val overlayArr = val::global("Uint8Array").new_(pixelByteCount);
54
+ overlayArr.call<void>("set", val(emscripten::typed_memory_view(
55
+ pixelByteCount, result.overlay)));
56
+ obj.set("overlay", overlayArr);
57
+
58
+ auto attachCoords = [&](const char *name, int32_t *xy, int32_t count) {
59
+ int32_t len = count * 2;
60
+ val arr = val::global("Int32Array").new_(len);
61
+ if (len > 0) {
62
+ arr.call<void>("set",
63
+ val(emscripten::typed_memory_view((size_t)len, xy)));
64
+ }
65
+ obj.set(name, arr);
66
+ };
67
+ attachCoords("addition", result.addition_xy, result.addition_count);
68
+ attachCoords("deletion", result.deletion_xy, result.deletion_count);
69
+ attachCoords("modification", result.modification_xy,
70
+ result.modification_count);
71
+
72
+ core_result_free(&result);
73
+ return obj;
74
+ }
75
+
76
+ EMSCRIPTEN_BINDINGS(core) { emscripten::function("processPage", &processPage); }
package/wasm/core.c ADDED
@@ -0,0 +1,176 @@
1
+ #include "core.h"
2
+
3
+ #include <math.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+
7
+ #define COORD_INITIAL_CAPACITY 256
8
+
9
+ typedef struct {
10
+ int32_t *data;
11
+ int32_t count;
12
+ int32_t capacity;
13
+ } Coords;
14
+
15
+ static int32_t coords_init(Coords *c) {
16
+ c->capacity = COORD_INITIAL_CAPACITY;
17
+ c->count = 0;
18
+ c->data = (int32_t *)malloc((size_t)c->capacity * 2 * sizeof(int32_t));
19
+ if (!c->data) {
20
+ return CORE_ERROR_ALLOC;
21
+ }
22
+ return CORE_OK;
23
+ }
24
+
25
+ static int32_t coords_push(Coords *c, int32_t x, int32_t y) {
26
+ if (c->count >= c->capacity) {
27
+ int32_t new_cap = c->capacity * 2;
28
+ int32_t *new_data =
29
+ (int32_t *)realloc(c->data, (size_t)new_cap * 2 * sizeof(int32_t));
30
+ if (!new_data) {
31
+ return CORE_ERROR_ALLOC;
32
+ }
33
+ c->data = new_data;
34
+ c->capacity = new_cap;
35
+ }
36
+ c->data[c->count * 2] = x;
37
+ c->data[c->count * 2 + 1] = y;
38
+ c->count++;
39
+ return CORE_OK;
40
+ }
41
+
42
+ static void coords_dispose(Coords *c) {
43
+ free(c->data);
44
+ c->data = NULL;
45
+ }
46
+
47
+ /*
48
+ * Mirrors the original JS `composeLayers` Porter-Duff "over" math, including
49
+ * Math.round() (round-half-up for non-negative values), to keep output
50
+ * byte-identical to the previous JS implementation.
51
+ */
52
+ static void blend_over(uint8_t *dst, uint8_t sr, uint8_t sg, uint8_t sb,
53
+ uint8_t s_alpha, double opacity) {
54
+ double sa = ((double)s_alpha / 255.0) * opacity;
55
+ if (sa == 0.0) {
56
+ return;
57
+ }
58
+ double da = (double)dst[3] / 255.0;
59
+ double oa = sa + da * (1.0 - sa);
60
+ if (oa == 0.0) {
61
+ return;
62
+ }
63
+ double sw = sa / oa;
64
+ double dw = (da * (1.0 - sa)) / oa;
65
+ dst[0] = (uint8_t)floor((double)sr * sw + (double)dst[0] * dw + 0.5);
66
+ dst[1] = (uint8_t)floor((double)sg * sw + (double)dst[1] * dw + 0.5);
67
+ dst[2] = (uint8_t)floor((double)sb * sw + (double)dst[2] * dw + 0.5);
68
+ dst[3] = (uint8_t)floor(oa * 255.0 + 0.5);
69
+ }
70
+
71
+ void core_result_free(CoreResult *r) {
72
+ if (!r) {
73
+ return;
74
+ }
75
+ free(r->overlay);
76
+ free(r->addition_xy);
77
+ free(r->deletion_xy);
78
+ free(r->modification_xy);
79
+ memset(r, 0, sizeof(*r));
80
+ }
81
+
82
+ int32_t process_page(const uint8_t *a_pixels, const uint8_t *b_pixels,
83
+ const uint8_t *mask_pixels, int32_t width, int32_t height,
84
+ const CorePallet *pallet, CoreResult *out) {
85
+ if (!a_pixels || !b_pixels || !pallet || !out) {
86
+ return CORE_ERROR_INVALID;
87
+ }
88
+ if (width <= 0 || height <= 0) {
89
+ return CORE_ERROR_INVALID;
90
+ }
91
+
92
+ memset(out, 0, sizeof(*out));
93
+
94
+ size_t pixel_count = (size_t)width * (size_t)height;
95
+ size_t byte_count = pixel_count * 4;
96
+
97
+ uint8_t *overlay = (uint8_t *)calloc(byte_count, 1);
98
+ if (!overlay) {
99
+ return CORE_ERROR_ALLOC;
100
+ }
101
+
102
+ Coords add = {0}, del = {0}, mod = {0};
103
+ if (coords_init(&add) != CORE_OK || coords_init(&del) != CORE_OK ||
104
+ coords_init(&mod) != CORE_OK) {
105
+ coords_dispose(&add);
106
+ coords_dispose(&del);
107
+ coords_dispose(&mod);
108
+ free(overlay);
109
+ return CORE_ERROR_ALLOC;
110
+ }
111
+
112
+ /* Diff layer (per-pixel pallet color) is reused into `overlay` directly:
113
+ for each pixel we compute the diff classification, then alpha-over
114
+ a*0.2, b*0.2, and the diff-pixel*1.0 onto overlay. The diff layer
115
+ itself is never materialized as a separate buffer. */
116
+ for (int32_t y = 0; y < height; y++) {
117
+ for (int32_t x = 0; x < width; x++) {
118
+ size_t idx = (size_t)(y * width + x) * 4;
119
+
120
+ const uint8_t *ap = a_pixels + idx;
121
+ const uint8_t *bp = b_pixels + idx;
122
+ uint8_t *op = overlay + idx;
123
+
124
+ /* Compose a (opacity 0.2). */
125
+ blend_over(op, ap[0], ap[1], ap[2], ap[3], 0.2);
126
+ /* Compose b (opacity 0.2). */
127
+ blend_over(op, bp[0], bp[1], bp[2], bp[3], 0.2);
128
+
129
+ if (mask_pixels && mask_pixels[idx + 3] != 0) {
130
+ continue;
131
+ }
132
+
133
+ uint8_t a_alpha = ap[3];
134
+ uint8_t b_alpha = bp[3];
135
+ if (a_alpha == b_alpha && ap[0] == bp[0] && ap[1] == bp[1] &&
136
+ ap[2] == bp[2]) {
137
+ continue;
138
+ }
139
+ if (a_alpha == 0 && b_alpha == 0) {
140
+ continue;
141
+ }
142
+
143
+ Coords *target;
144
+ const CoreColor *color;
145
+ if (a_alpha == 0) {
146
+ target = &add;
147
+ color = &pallet->addition;
148
+ } else if (b_alpha == 0) {
149
+ target = &del;
150
+ color = &pallet->deletion;
151
+ } else {
152
+ target = &mod;
153
+ color = &pallet->modification;
154
+ }
155
+ if (coords_push(target, x, y) != CORE_OK) {
156
+ coords_dispose(&add);
157
+ coords_dispose(&del);
158
+ coords_dispose(&mod);
159
+ free(overlay);
160
+ return CORE_ERROR_ALLOC;
161
+ }
162
+
163
+ /* Compose diff pixel (opacity 1.0) on top. */
164
+ blend_over(op, color->r, color->g, color->b, color->a, 1.0);
165
+ }
166
+ }
167
+
168
+ out->overlay = overlay;
169
+ out->addition_xy = add.data;
170
+ out->addition_count = add.count;
171
+ out->deletion_xy = del.data;
172
+ out->deletion_count = del.count;
173
+ out->modification_xy = mod.data;
174
+ out->modification_count = mod.count;
175
+ return CORE_OK;
176
+ }
package/wasm/core.h ADDED
@@ -0,0 +1,69 @@
1
+ #ifndef CORE_H
2
+ #define CORE_H
3
+
4
+ #include <stdint.h>
5
+
6
+ #ifdef __cplusplus
7
+ extern "C" {
8
+ #endif
9
+
10
+ enum {
11
+ CORE_OK = 0,
12
+ CORE_ERROR_ALLOC = -1,
13
+ CORE_ERROR_INVALID = -2,
14
+ };
15
+
16
+ typedef struct {
17
+ uint8_t r;
18
+ uint8_t g;
19
+ uint8_t b;
20
+ uint8_t a;
21
+ } CoreColor;
22
+
23
+ typedef struct {
24
+ CoreColor addition;
25
+ CoreColor deletion;
26
+ CoreColor modification;
27
+ } CorePallet;
28
+
29
+ /*
30
+ * Output buffers are owned by the caller; release with core_result_free.
31
+ * On error, the struct is left zero-initialized and nothing needs freeing.
32
+ *
33
+ * `overlay` : width * height * 4 bytes RGBA. Final overlay computed as
34
+ * alpha-over of (a * 0.2), (b * 0.2), and (diff layer * 1.0).
35
+ * `*_xy` : packed [x0, y0, x1, y1, ...] int32 coordinates.
36
+ * `*_count` : number of pixels (each pixel uses 2 int32 entries).
37
+ */
38
+ typedef struct {
39
+ uint8_t *overlay;
40
+ int32_t *addition_xy;
41
+ int32_t addition_count;
42
+ int32_t *deletion_xy;
43
+ int32_t deletion_count;
44
+ int32_t *modification_xy;
45
+ int32_t modification_count;
46
+ } CoreResult;
47
+
48
+ /*
49
+ * Diff scan + diff-layer paint + final overlay compose, in a single pass.
50
+ *
51
+ * a_pixels, b_pixels: width * height * 4 bytes RGBA, identical dimensions.
52
+ * mask_pixels : NULL, or width * height * 4 bytes RGBA. Pixels where
53
+ * mask alpha != 0 are excluded from the diff scan.
54
+ * pallet : colors used to paint the diff layer per category.
55
+ * out : populated with overlay + per-category coordinates.
56
+ *
57
+ * Returns CORE_OK on success, negative on error.
58
+ */
59
+ int32_t process_page(const uint8_t *a_pixels, const uint8_t *b_pixels,
60
+ const uint8_t *mask_pixels, int32_t width, int32_t height,
61
+ const CorePallet *pallet, CoreResult *out);
62
+
63
+ void core_result_free(CoreResult *r);
64
+
65
+ #ifdef __cplusplus
66
+ }
67
+ #endif
68
+
69
+ #endif
Binary file