@u1f992/pdfdiff 0.2.2 → 0.3.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.
Files changed (71) hide show
  1. package/.clang-format +3 -0
  2. package/.github/workflows/gh-pages.yml +6 -6
  3. package/.vscode/extensions.json +1 -1
  4. package/.vscode/settings.json +1 -1
  5. package/LICENSE +68 -81
  6. package/README.md +7 -0
  7. package/dist/browser.js +243 -3109
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cli-png-worker.d.ts.map +1 -1
  10. package/dist/cli-png-worker.js +0 -16
  11. package/dist/cli-png-worker.js.map +1 -1
  12. package/dist/cli.js +270 -3151
  13. package/dist/cli.js.map +1 -1
  14. package/dist/core.wasm +0 -0
  15. package/dist/decode.d.ts +9 -0
  16. package/dist/decode.d.ts.map +1 -0
  17. package/dist/diff.d.ts.map +1 -1
  18. package/dist/gs-wasm/gs.js +5821 -0
  19. package/dist/gs-wasm/gs.wasm +0 -0
  20. package/dist/gs-wasm/index.js +120 -0
  21. package/dist/gs-wasm/index.js.map +1 -0
  22. package/dist/gs-wasm/worker.js +764 -0
  23. package/dist/gs-wasm/worker.js.map +1 -0
  24. package/dist/image.d.ts.map +1 -1
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.html +1 -1
  28. package/dist/index.js +242 -3109
  29. package/dist/index.js.map +1 -1
  30. package/dist/iterable.d.ts.map +1 -1
  31. package/dist/jimp.d.ts +23 -1
  32. package/dist/jimp.d.ts.map +1 -1
  33. package/dist/pdf.d.ts +15 -4
  34. package/dist/pdf.d.ts.map +1 -1
  35. package/dist/perf.d.ts.map +1 -1
  36. package/dist/rgba-color.d.ts.map +1 -1
  37. package/dist/transferable.d.ts +6 -2
  38. package/dist/transferable.d.ts.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/worker.d.ts +6 -8
  41. package/dist/worker.d.ts.map +1 -1
  42. package/dist/worker.js +70 -3311
  43. package/dist/worker.js.map +1 -1
  44. package/package.json +10 -5
  45. package/prettier.config.js +1 -1
  46. package/rollup.config.js +63 -5
  47. package/scripts/build-wasm.sh +32 -0
  48. package/src/browser.ts +9 -6
  49. package/src/cli-png-worker.ts +0 -17
  50. package/src/cli.ts +38 -23
  51. package/src/decode.ts +13 -0
  52. package/src/diff.ts +0 -17
  53. package/src/image.ts +1 -18
  54. package/src/index.html +1 -1
  55. package/src/index.test.ts +10 -18
  56. package/src/index.ts +170 -74
  57. package/src/iterable.test.ts +0 -17
  58. package/src/iterable.ts +0 -17
  59. package/src/jimp.ts +25 -7
  60. package/src/pdf.ts +100 -69
  61. package/src/perf.ts +0 -17
  62. package/src/rgba-color.test.ts +0 -17
  63. package/src/rgba-color.ts +0 -17
  64. package/src/transferable.ts +6 -21
  65. package/src/worker.ts +91 -87
  66. package/tsconfig.json +53 -50
  67. package/wasm/Makefile +34 -0
  68. package/wasm/bindings.cpp +76 -0
  69. package/wasm/core.c +179 -0
  70. package/wasm/core.h +69 -0
  71. 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/tsconfig.json CHANGED
@@ -1,50 +1,53 @@
1
- {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
- "compilerOptions": {
4
- // File Layout
5
- "rootDir": "./src",
6
- "outDir": "./dist",
7
-
8
- // Environment Settings
9
- // See also https://aka.ms/tsconfig/module
10
- "module": "esnext",
11
- "target": "esnext",
12
- // "types": [],
13
- // For nodejs:
14
- "lib": ["esnext", "WebWorker", "DOM"],
15
- "types": ["node"],
16
- // and npm install -D @types/node
17
-
18
- // Other Outputs
19
- "sourceMap": true,
20
- "declaration": true,
21
- "declarationMap": true,
22
-
23
- // Stricter Typechecking Options
24
- "noUncheckedIndexedAccess": true,
25
- "exactOptionalPropertyTypes": true,
26
-
27
- // Style Options
28
- // "noImplicitReturns": true,
29
- // "noImplicitOverride": true,
30
- // "noUnusedLocals": true,
31
- // "noUnusedParameters": true,
32
- // "noFallthroughCasesInSwitch": true,
33
- // "noPropertyAccessFromIndexSignature": true,
34
-
35
- // Recommended Options
36
- "strict": true,
37
- "jsx": "react-jsx",
38
- "verbatimModuleSyntax": true,
39
- "isolatedModules": true,
40
- "noUncheckedSideEffectImports": true,
41
- "moduleDetection": "force",
42
- "skipLibCheck": true,
43
-
44
- // With Rollup
45
- "moduleResolution": "bundler",
46
- "allowImportingTsExtensions": true,
47
- "rewriteRelativeImportExtensions": true,
48
- "noEmit": true
49
- }
50
- }
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "esnext",
11
+ "target": "esnext",
12
+ // "types": [],
13
+ // For nodejs:
14
+ "lib": ["esnext", "WebWorker", "DOM"],
15
+ "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": true,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+
44
+ // With Rollup
45
+ "moduleResolution": "bundler",
46
+ "allowImportingTsExtensions": true,
47
+ "rewriteRelativeImportExtensions": true,
48
+ "noEmit": true
49
+ },
50
+ // Keep `tsc --noEmit` runnable as a type check: scripts/version.ts is run
51
+ // directly by node (type stripping) and lives outside rootDir.
52
+ "include": ["src"]
53
+ }
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,179 @@
1
+ #include "core.h"
2
+
3
+ #include <math.h>
4
+ #include <stdlib.h>
5
+
6
+ #define COORD_INITIAL_CAPACITY 256
7
+
8
+ /* Zero constant for CoreResult; assigning it replaces struct-zeroing memset,
9
+ which clang-tidy's insecureAPI check rejects. */
10
+ static const CoreResult CORE_RESULT_ZERO = {0};
11
+
12
+ typedef struct {
13
+ int32_t *data;
14
+ int32_t count;
15
+ int32_t capacity;
16
+ } Coords;
17
+
18
+ static int32_t coords_init(Coords *c) {
19
+ c->capacity = COORD_INITIAL_CAPACITY;
20
+ c->count = 0;
21
+ c->data = (int32_t *)malloc((size_t)c->capacity * 2 * sizeof(int32_t));
22
+ if (!c->data) {
23
+ return CORE_ERROR_ALLOC;
24
+ }
25
+ return CORE_OK;
26
+ }
27
+
28
+ static int32_t coords_push(Coords *c, int32_t x, int32_t y) {
29
+ if (c->count >= c->capacity) {
30
+ int32_t new_cap = c->capacity * 2;
31
+ int32_t *new_data =
32
+ (int32_t *)realloc(c->data, (size_t)new_cap * 2 * sizeof(int32_t));
33
+ if (!new_data) {
34
+ return CORE_ERROR_ALLOC;
35
+ }
36
+ c->data = new_data;
37
+ c->capacity = new_cap;
38
+ }
39
+ c->data[c->count * 2] = x;
40
+ c->data[c->count * 2 + 1] = y;
41
+ c->count++;
42
+ return CORE_OK;
43
+ }
44
+
45
+ static void coords_dispose(Coords *c) {
46
+ free(c->data);
47
+ c->data = NULL;
48
+ }
49
+
50
+ /*
51
+ * Mirrors the original JS `composeLayers` Porter-Duff "over" math, including
52
+ * Math.round() (round-half-up for non-negative values), to keep output
53
+ * byte-identical to the previous JS implementation.
54
+ */
55
+ static void blend_over(uint8_t *dst, uint8_t sr, uint8_t sg, uint8_t sb,
56
+ uint8_t s_alpha, double opacity) {
57
+ double sa = ((double)s_alpha / 255.0) * opacity;
58
+ if (sa == 0.0) {
59
+ return;
60
+ }
61
+ double da = (double)dst[3] / 255.0;
62
+ double oa = sa + da * (1.0 - sa);
63
+ if (oa == 0.0) {
64
+ return;
65
+ }
66
+ double sw = sa / oa;
67
+ double dw = (da * (1.0 - sa)) / oa;
68
+ dst[0] = (uint8_t)floor((double)sr * sw + (double)dst[0] * dw + 0.5);
69
+ dst[1] = (uint8_t)floor((double)sg * sw + (double)dst[1] * dw + 0.5);
70
+ dst[2] = (uint8_t)floor((double)sb * sw + (double)dst[2] * dw + 0.5);
71
+ dst[3] = (uint8_t)floor(oa * 255.0 + 0.5);
72
+ }
73
+
74
+ void core_result_free(CoreResult *r) {
75
+ if (!r) {
76
+ return;
77
+ }
78
+ free(r->overlay);
79
+ free(r->addition_xy);
80
+ free(r->deletion_xy);
81
+ free(r->modification_xy);
82
+ *r = CORE_RESULT_ZERO;
83
+ }
84
+
85
+ int32_t process_page(const uint8_t *a_pixels, const uint8_t *b_pixels,
86
+ const uint8_t *mask_pixels, int32_t width, int32_t height,
87
+ const CorePallet *pallet, CoreResult *out) {
88
+ if (!a_pixels || !b_pixels || !pallet || !out) {
89
+ return CORE_ERROR_INVALID;
90
+ }
91
+ if (width <= 0 || height <= 0) {
92
+ return CORE_ERROR_INVALID;
93
+ }
94
+
95
+ *out = CORE_RESULT_ZERO;
96
+
97
+ size_t pixel_count = (size_t)width * (size_t)height;
98
+ size_t byte_count = pixel_count * 4;
99
+
100
+ uint8_t *overlay = (uint8_t *)calloc(byte_count, 1);
101
+ if (!overlay) {
102
+ return CORE_ERROR_ALLOC;
103
+ }
104
+
105
+ Coords add = {0}, del = {0}, mod = {0};
106
+ if (coords_init(&add) != CORE_OK || coords_init(&del) != CORE_OK ||
107
+ coords_init(&mod) != CORE_OK) {
108
+ coords_dispose(&add);
109
+ coords_dispose(&del);
110
+ coords_dispose(&mod);
111
+ free(overlay);
112
+ return CORE_ERROR_ALLOC;
113
+ }
114
+
115
+ /* Diff layer (per-pixel pallet color) is reused into `overlay` directly:
116
+ for each pixel we compute the diff classification, then alpha-over
117
+ a*0.2, b*0.2, and the diff-pixel*1.0 onto overlay. The diff layer
118
+ itself is never materialized as a separate buffer. */
119
+ for (int32_t y = 0; y < height; y++) {
120
+ for (int32_t x = 0; x < width; x++) {
121
+ size_t idx = (size_t)(y * width + x) * 4;
122
+
123
+ const uint8_t *ap = a_pixels + idx;
124
+ const uint8_t *bp = b_pixels + idx;
125
+ uint8_t *op = overlay + idx;
126
+
127
+ /* Compose a (opacity 0.2). */
128
+ blend_over(op, ap[0], ap[1], ap[2], ap[3], 0.2);
129
+ /* Compose b (opacity 0.2). */
130
+ blend_over(op, bp[0], bp[1], bp[2], bp[3], 0.2);
131
+
132
+ if (mask_pixels && mask_pixels[idx + 3] != 0) {
133
+ continue;
134
+ }
135
+
136
+ uint8_t a_alpha = ap[3];
137
+ uint8_t b_alpha = bp[3];
138
+ if (a_alpha == b_alpha && ap[0] == bp[0] && ap[1] == bp[1] &&
139
+ ap[2] == bp[2]) {
140
+ continue;
141
+ }
142
+ if (a_alpha == 0 && b_alpha == 0) {
143
+ continue;
144
+ }
145
+
146
+ Coords *target;
147
+ const CoreColor *color;
148
+ if (a_alpha == 0) {
149
+ target = &add;
150
+ color = &pallet->addition;
151
+ } else if (b_alpha == 0) {
152
+ target = &del;
153
+ color = &pallet->deletion;
154
+ } else {
155
+ target = &mod;
156
+ color = &pallet->modification;
157
+ }
158
+ if (coords_push(target, x, y) != CORE_OK) {
159
+ coords_dispose(&add);
160
+ coords_dispose(&del);
161
+ coords_dispose(&mod);
162
+ free(overlay);
163
+ return CORE_ERROR_ALLOC;
164
+ }
165
+
166
+ /* Compose diff pixel (opacity 1.0) on top. */
167
+ blend_over(op, color->r, color->g, color->b, color->a, 1.0);
168
+ }
169
+ }
170
+
171
+ out->overlay = overlay;
172
+ out->addition_xy = add.data;
173
+ out->addition_count = add.count;
174
+ out->deletion_xy = del.data;
175
+ out->deletion_count = del.count;
176
+ out->modification_xy = mod.data;
177
+ out->modification_count = mod.count;
178
+ return CORE_OK;
179
+ }