@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.
- package/.clang-format +3 -0
- package/LICENSE +68 -81
- package/dist/browser.js +239 -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 +240 -3150
- 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 +238 -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 +9 -4
- 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 +9 -23
- package/src/decode.ts +15 -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 +163 -74
- package/src/iterable.test.ts +0 -17
- package/src/iterable.ts +0 -17
- package/src/jimp.ts +25 -7
- package/src/pdf.ts +98 -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/wasm/Makefile +34 -0
- package/wasm/bindings.cpp +76 -0
- package/wasm/core.c +176 -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/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
|
package/dist/mupdf-wasm.wasm
DELETED
|
Binary file
|