@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.
- package/.clang-format +3 -0
- package/.github/workflows/gh-pages.yml +6 -6
- package/.vscode/extensions.json +1 -1
- package/.vscode/settings.json +1 -1
- package/LICENSE +68 -81
- package/README.md +7 -0
- package/dist/browser.js +243 -3109
- package/dist/browser.js.map +1 -1
- package/dist/cli-png-worker.d.ts.map +1 -1
- package/dist/cli-png-worker.js +0 -16
- package/dist/cli-png-worker.js.map +1 -1
- package/dist/cli.js +270 -3151
- package/dist/cli.js.map +1 -1
- package/dist/core.wasm +0 -0
- package/dist/decode.d.ts +9 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/gs-wasm/gs.js +5821 -0
- package/dist/gs-wasm/gs.wasm +0 -0
- package/dist/gs-wasm/index.js +120 -0
- package/dist/gs-wasm/index.js.map +1 -0
- package/dist/gs-wasm/worker.js +764 -0
- package/dist/gs-wasm/worker.js.map +1 -0
- package/dist/image.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +242 -3109
- package/dist/index.js.map +1 -1
- package/dist/iterable.d.ts.map +1 -1
- package/dist/jimp.d.ts +23 -1
- package/dist/jimp.d.ts.map +1 -1
- package/dist/pdf.d.ts +15 -4
- package/dist/pdf.d.ts.map +1 -1
- package/dist/perf.d.ts.map +1 -1
- package/dist/rgba-color.d.ts.map +1 -1
- package/dist/transferable.d.ts +6 -2
- package/dist/transferable.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/worker.d.ts +6 -8
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +70 -3311
- package/dist/worker.js.map +1 -1
- package/package.json +10 -5
- package/prettier.config.js +1 -1
- package/rollup.config.js +63 -5
- package/scripts/build-wasm.sh +32 -0
- package/src/browser.ts +9 -6
- package/src/cli-png-worker.ts +0 -17
- package/src/cli.ts +38 -23
- package/src/decode.ts +13 -0
- package/src/diff.ts +0 -17
- package/src/image.ts +1 -18
- package/src/index.html +1 -1
- package/src/index.test.ts +10 -18
- package/src/index.ts +170 -74
- package/src/iterable.test.ts +0 -17
- package/src/iterable.ts +0 -17
- package/src/jimp.ts +25 -7
- package/src/pdf.ts +100 -69
- package/src/perf.ts +0 -17
- package/src/rgba-color.test.ts +0 -17
- package/src/rgba-color.ts +0 -17
- package/src/transferable.ts +6 -21
- package/src/worker.ts +91 -87
- package/tsconfig.json +53 -50
- package/wasm/Makefile +34 -0
- package/wasm/bindings.cpp +76 -0
- package/wasm/core.c +179 -0
- package/wasm/core.h +69 -0
- package/dist/mupdf-wasm.wasm +0 -0
package/src/transferable.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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:
|
|
62
|
-
deletion:
|
|
63
|
-
modification:
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
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(
|
|
118
|
-
const bBuf = sliceBackingBuffer(
|
|
119
|
-
const dBuf = sliceBackingBuffer(
|
|
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
|
|
133
|
-
b: { width
|
|
134
|
-
diff: { width
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|