@image-marker/web 0.1.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/CHANGELOG.md +11 -0
- package/README.md +100 -0
- package/api-contract.json +224 -0
- package/lib/commonjs/core/batch.d.ts +32 -0
- package/lib/commonjs/core/batch.js +85 -0
- package/lib/commonjs/core/content-credentials.d.ts +53 -0
- package/lib/commonjs/core/content-credentials.js +96 -0
- package/lib/commonjs/core/image-info.d.ts +30 -0
- package/lib/commonjs/core/image-info.js +348 -0
- package/lib/commonjs/core/index.d.ts +1014 -0
- package/lib/commonjs/core/index.js +92 -0
- package/lib/commonjs/core/invisible-watermark.d.ts +102 -0
- package/lib/commonjs/core/invisible-watermark.js +913 -0
- package/lib/commonjs/core/job.d.ts +57 -0
- package/lib/commonjs/core/job.js +151 -0
- package/lib/commonjs/core/layout.d.ts +19 -0
- package/lib/commonjs/core/layout.js +75 -0
- package/lib/commonjs/core/marker.d.ts +1 -0
- package/lib/commonjs/core/marker.js +11 -0
- package/lib/commonjs/core/recipe.d.ts +44 -0
- package/lib/commonjs/core/recipe.js +175 -0
- package/lib/commonjs/core/result.d.ts +34 -0
- package/lib/commonjs/core/result.js +41 -0
- package/lib/commonjs/core/trace-runtime.d.ts +43 -0
- package/lib/commonjs/core/trace-runtime.js +92 -0
- package/lib/commonjs/core/validate.d.ts +4 -0
- package/lib/commonjs/core/validate.js +192 -0
- package/lib/commonjs/core/web/browser.d.ts +105 -0
- package/lib/commonjs/core/web/browser.js +434 -0
- package/lib/commonjs/core/web/helpers.d.ts +41 -0
- package/lib/commonjs/core/web/helpers.js +264 -0
- package/lib/commonjs/core/web/index.d.ts +59 -0
- package/lib/commonjs/core/web/index.js +724 -0
- package/lib/commonjs/core/web/invisible-worker-client.d.ts +13 -0
- package/lib/commonjs/core/web/invisible-worker-client.js +140 -0
- package/lib/commonjs/core/web/renderer.d.ts +20 -0
- package/lib/commonjs/core/web/renderer.js +665 -0
- package/lib/commonjs/core/web/runtime.d.ts +106 -0
- package/lib/commonjs/core/web/runtime.js +2 -0
- package/lib/commonjs/editor/adapter.d.ts +26 -0
- package/lib/commonjs/editor/adapter.js +102 -0
- package/lib/commonjs/editor/controller.d.ts +106 -0
- package/lib/commonjs/editor/controller.js +919 -0
- package/lib/commonjs/editor/core-contract.d.ts +2 -0
- package/lib/commonjs/editor/core-contract.js +23 -0
- package/lib/commonjs/editor/geometry.d.ts +8 -0
- package/lib/commonjs/editor/geometry.js +109 -0
- package/lib/commonjs/editor/headless.d.ts +5 -0
- package/lib/commonjs/editor/headless.js +22 -0
- package/lib/commonjs/editor/presets.d.ts +19 -0
- package/lib/commonjs/editor/presets.js +64 -0
- package/lib/commonjs/editor/projection.d.ts +12 -0
- package/lib/commonjs/editor/projection.js +195 -0
- package/lib/commonjs/editor/types.d.ts +166 -0
- package/lib/commonjs/editor/types.js +2 -0
- package/lib/commonjs/editor-adapter.d.ts +7 -0
- package/lib/commonjs/editor-adapter.js +17 -0
- package/lib/commonjs/headless.d.ts +2 -0
- package/lib/commonjs/headless.js +26 -0
- package/lib/commonjs/index.d.ts +5 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/core/batch.d.ts +32 -0
- package/lib/module/core/batch.js +82 -0
- package/lib/module/core/content-credentials.d.ts +53 -0
- package/lib/module/core/content-credentials.js +92 -0
- package/lib/module/core/image-info.d.ts +30 -0
- package/lib/module/core/image-info.js +343 -0
- package/lib/module/core/index.d.ts +1014 -0
- package/lib/module/core/index.js +56 -0
- package/lib/module/core/invisible-watermark.d.ts +102 -0
- package/lib/module/core/invisible-watermark.js +897 -0
- package/lib/module/core/job.d.ts +57 -0
- package/lib/module/core/job.js +143 -0
- package/lib/module/core/layout.d.ts +19 -0
- package/lib/module/core/layout.js +70 -0
- package/lib/module/core/marker.d.ts +1 -0
- package/lib/module/core/marker.js +4 -0
- package/lib/module/core/recipe.d.ts +44 -0
- package/lib/module/core/recipe.js +169 -0
- package/lib/module/core/result.d.ts +34 -0
- package/lib/module/core/result.js +38 -0
- package/lib/module/core/trace-runtime.d.ts +43 -0
- package/lib/module/core/trace-runtime.js +89 -0
- package/lib/module/core/validate.d.ts +4 -0
- package/lib/module/core/validate.js +187 -0
- package/lib/module/core/web/browser.d.ts +105 -0
- package/lib/module/core/web/browser.js +427 -0
- package/lib/module/core/web/helpers.d.ts +41 -0
- package/lib/module/core/web/helpers.js +251 -0
- package/lib/module/core/web/index.d.ts +59 -0
- package/lib/module/core/web/index.js +709 -0
- package/lib/module/core/web/invisible-worker-client.d.ts +13 -0
- package/lib/module/core/web/invisible-worker-client.js +137 -0
- package/lib/module/core/web/renderer.d.ts +20 -0
- package/lib/module/core/web/renderer.js +661 -0
- package/lib/module/core/web/runtime.d.ts +106 -0
- package/lib/module/core/web/runtime.js +1 -0
- package/lib/module/editor/adapter.d.ts +26 -0
- package/lib/module/editor/adapter.js +99 -0
- package/lib/module/editor/controller.d.ts +106 -0
- package/lib/module/editor/controller.js +915 -0
- package/lib/module/editor/core-contract.d.ts +2 -0
- package/lib/module/editor/core-contract.js +2 -0
- package/lib/module/editor/geometry.d.ts +8 -0
- package/lib/module/editor/geometry.js +103 -0
- package/lib/module/editor/headless.d.ts +5 -0
- package/lib/module/editor/headless.js +4 -0
- package/lib/module/editor/presets.d.ts +19 -0
- package/lib/module/editor/presets.js +57 -0
- package/lib/module/editor/projection.d.ts +12 -0
- package/lib/module/editor/projection.js +188 -0
- package/lib/module/editor/types.d.ts +166 -0
- package/lib/module/editor/types.js +1 -0
- package/lib/module/editor-adapter.d.ts +7 -0
- package/lib/module/editor-adapter.js +10 -0
- package/lib/module/headless.d.ts +2 -0
- package/lib/module/headless.js +2 -0
- package/lib/module/index.d.ts +5 -0
- package/lib/module/index.js +3 -0
- package/lib/module/package.json +1 -0
- package/lib/source-manifest.json +34 -0
- package/lib/worker/invisible-watermark.js +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise -- SHA-256, CRC, seeded permutation, and QIM bit operations are intentional. */
|
|
2
|
+
export const INVISIBLE_WATERMARK_ALGORITHM = 'dct-qim-v1';
|
|
3
|
+
export const INVISIBLE_WATERMARK_MAX_PAYLOAD_BYTES = 12;
|
|
4
|
+
export const INVISIBLE_WATERMARK_MIN_KEY_BYTES = 16;
|
|
5
|
+
export const INVISIBLE_WATERMARK_MIN_WIDTH = 128;
|
|
6
|
+
export const INVISIBLE_WATERMARK_MIN_HEIGHT = 88;
|
|
7
|
+
export const INVISIBLE_WATERMARK_RESIZE_SCALES = [
|
|
8
|
+
0.95, 1.05, 0.9, 1.1,
|
|
9
|
+
];
|
|
10
|
+
const INVISIBLE_WATERMARK_RESIZE_DELTA_FACTORS = [1, 0.9, 0.8, 0.7, 0.6];
|
|
11
|
+
const BLOCK_SIZE = 8;
|
|
12
|
+
const TILE_WIDTH = 16;
|
|
13
|
+
const TILE_HEIGHT = 11;
|
|
14
|
+
const FRAME_BYTES = 22;
|
|
15
|
+
const FRAME_BITS = FRAME_BYTES * 8;
|
|
16
|
+
const PAYLOAD_OFFSET = 4;
|
|
17
|
+
const PAYLOAD_BYTES = 12;
|
|
18
|
+
const CRC_OFFSET = 16;
|
|
19
|
+
const AUTH_OFFSET = 18;
|
|
20
|
+
const MAGIC_0 = 0x49;
|
|
21
|
+
const MAGIC_1 = 0x4d;
|
|
22
|
+
const FRAME_VERSION = 1;
|
|
23
|
+
const SHA256_BLOCK_BYTES = 64;
|
|
24
|
+
const SHA256_INITIAL = new Uint32Array([
|
|
25
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
26
|
+
0x1f83d9ab, 0x5be0cd19,
|
|
27
|
+
]);
|
|
28
|
+
const SHA256_ROUND = new Uint32Array([
|
|
29
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
30
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
31
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
32
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
33
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
34
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
35
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
36
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
37
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
38
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
39
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
40
|
+
]);
|
|
41
|
+
const COEFFICIENT_PAIRS = [
|
|
42
|
+
[1, 2, 2, 1],
|
|
43
|
+
[2, 3, 3, 2],
|
|
44
|
+
[1, 3, 3, 1],
|
|
45
|
+
[2, 4, 4, 2],
|
|
46
|
+
];
|
|
47
|
+
const STRENGTH_DELTAS = {
|
|
48
|
+
subtle: 18,
|
|
49
|
+
balanced: 28,
|
|
50
|
+
robust: 42,
|
|
51
|
+
};
|
|
52
|
+
function toPublicDetectionResult(candidate) {
|
|
53
|
+
const result = {
|
|
54
|
+
detected: candidate.detected,
|
|
55
|
+
confidence: candidate.confidence,
|
|
56
|
+
algorithm: candidate.algorithm,
|
|
57
|
+
};
|
|
58
|
+
if (candidate.payload !== undefined)
|
|
59
|
+
result.payload = candidate.payload;
|
|
60
|
+
if (candidate.bitErrorRate !== undefined) {
|
|
61
|
+
result.bitErrorRate = candidate.bitErrorRate;
|
|
62
|
+
}
|
|
63
|
+
if (candidate.scale !== undefined && candidate.scale !== 1) {
|
|
64
|
+
result.scale = candidate.scale;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function rotateRight(value, bits) {
|
|
69
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
70
|
+
}
|
|
71
|
+
/** Small dependency-free SHA-256 used to keep Web frame vectors deterministic. */
|
|
72
|
+
export function sha256(input) {
|
|
73
|
+
const bitLength = input.length * 8;
|
|
74
|
+
const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
|
|
75
|
+
const padded = new Uint8Array(paddedLength);
|
|
76
|
+
padded.set(input);
|
|
77
|
+
padded[input.length] = 0x80;
|
|
78
|
+
const view = new DataView(padded.buffer);
|
|
79
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000));
|
|
80
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0);
|
|
81
|
+
const hash = new Uint32Array(SHA256_INITIAL);
|
|
82
|
+
const words = new Uint32Array(64);
|
|
83
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
84
|
+
for (let index = 0; index < 16; index += 1) {
|
|
85
|
+
words[index] = view.getUint32(offset + index * 4);
|
|
86
|
+
}
|
|
87
|
+
for (let index = 16; index < 64; index += 1) {
|
|
88
|
+
const word15 = words[index - 15];
|
|
89
|
+
const word2 = words[index - 2];
|
|
90
|
+
const sigma0 = rotateRight(word15, 7) ^ rotateRight(word15, 18) ^ (word15 >>> 3);
|
|
91
|
+
const sigma1 = rotateRight(word2, 17) ^ rotateRight(word2, 19) ^ (word2 >>> 10);
|
|
92
|
+
words[index] =
|
|
93
|
+
(words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
94
|
+
}
|
|
95
|
+
let a = hash[0];
|
|
96
|
+
let b = hash[1];
|
|
97
|
+
let c = hash[2];
|
|
98
|
+
let d = hash[3];
|
|
99
|
+
let e = hash[4];
|
|
100
|
+
let f = hash[5];
|
|
101
|
+
let g = hash[6];
|
|
102
|
+
let h = hash[7];
|
|
103
|
+
for (let index = 0; index < 64; index += 1) {
|
|
104
|
+
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
105
|
+
const choose = (e & f) ^ (~e & g);
|
|
106
|
+
const temp1 = (h + sum1 + choose + SHA256_ROUND[index] + words[index]) >>> 0;
|
|
107
|
+
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
108
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
109
|
+
const temp2 = (sum0 + majority) >>> 0;
|
|
110
|
+
h = g;
|
|
111
|
+
g = f;
|
|
112
|
+
f = e;
|
|
113
|
+
e = (d + temp1) >>> 0;
|
|
114
|
+
d = c;
|
|
115
|
+
c = b;
|
|
116
|
+
b = a;
|
|
117
|
+
a = (temp1 + temp2) >>> 0;
|
|
118
|
+
}
|
|
119
|
+
hash[0] = (hash[0] + a) >>> 0;
|
|
120
|
+
hash[1] = (hash[1] + b) >>> 0;
|
|
121
|
+
hash[2] = (hash[2] + c) >>> 0;
|
|
122
|
+
hash[3] = (hash[3] + d) >>> 0;
|
|
123
|
+
hash[4] = (hash[4] + e) >>> 0;
|
|
124
|
+
hash[5] = (hash[5] + f) >>> 0;
|
|
125
|
+
hash[6] = (hash[6] + g) >>> 0;
|
|
126
|
+
hash[7] = (hash[7] + h) >>> 0;
|
|
127
|
+
}
|
|
128
|
+
const output = new Uint8Array(32);
|
|
129
|
+
const outputView = new DataView(output.buffer);
|
|
130
|
+
hash.forEach((word, index) => outputView.setUint32(index * 4, word));
|
|
131
|
+
return output;
|
|
132
|
+
}
|
|
133
|
+
export function hmacSha256(key, message) {
|
|
134
|
+
let normalizedKey = key.length > SHA256_BLOCK_BYTES ? sha256(key) : key;
|
|
135
|
+
if (normalizedKey.length < SHA256_BLOCK_BYTES) {
|
|
136
|
+
const padded = new Uint8Array(SHA256_BLOCK_BYTES);
|
|
137
|
+
padded.set(normalizedKey);
|
|
138
|
+
normalizedKey = padded;
|
|
139
|
+
}
|
|
140
|
+
const innerKey = new Uint8Array(SHA256_BLOCK_BYTES);
|
|
141
|
+
const outerKey = new Uint8Array(SHA256_BLOCK_BYTES);
|
|
142
|
+
for (let index = 0; index < SHA256_BLOCK_BYTES; index += 1) {
|
|
143
|
+
innerKey[index] = normalizedKey[index] ^ 0x36;
|
|
144
|
+
outerKey[index] = normalizedKey[index] ^ 0x5c;
|
|
145
|
+
}
|
|
146
|
+
const inner = new Uint8Array(innerKey.length + message.length);
|
|
147
|
+
inner.set(innerKey);
|
|
148
|
+
inner.set(message, innerKey.length);
|
|
149
|
+
const innerHash = sha256(inner);
|
|
150
|
+
const outer = new Uint8Array(outerKey.length + innerHash.length);
|
|
151
|
+
outer.set(outerKey);
|
|
152
|
+
outer.set(innerHash, outerKey.length);
|
|
153
|
+
return sha256(outer);
|
|
154
|
+
}
|
|
155
|
+
export function encodeUtf8(value) {
|
|
156
|
+
const bytes = [];
|
|
157
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
158
|
+
let codePoint = value.charCodeAt(index);
|
|
159
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff) {
|
|
160
|
+
const next = value.charCodeAt(index + 1);
|
|
161
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
162
|
+
codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00);
|
|
163
|
+
index += 1;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
codePoint = 0xfffd;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (codePoint >= 0xdc00 && codePoint <= 0xdfff) {
|
|
170
|
+
codePoint = 0xfffd;
|
|
171
|
+
}
|
|
172
|
+
if (codePoint <= 0x7f) {
|
|
173
|
+
bytes.push(codePoint);
|
|
174
|
+
}
|
|
175
|
+
else if (codePoint <= 0x7ff) {
|
|
176
|
+
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
|
|
177
|
+
}
|
|
178
|
+
else if (codePoint <= 0xffff) {
|
|
179
|
+
bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
bytes.push(0xf0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3f), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return new Uint8Array(bytes);
|
|
186
|
+
}
|
|
187
|
+
export function decodeUtf8(bytes) {
|
|
188
|
+
let output = '';
|
|
189
|
+
for (let index = 0; index < bytes.length;) {
|
|
190
|
+
const first = bytes[index++];
|
|
191
|
+
let codePoint;
|
|
192
|
+
if (first < 0x80) {
|
|
193
|
+
codePoint = first;
|
|
194
|
+
}
|
|
195
|
+
else if ((first & 0xe0) === 0xc0 && index < bytes.length) {
|
|
196
|
+
codePoint = ((first & 0x1f) << 6) | (bytes[index++] & 0x3f);
|
|
197
|
+
}
|
|
198
|
+
else if ((first & 0xf0) === 0xe0 && index + 1 < bytes.length) {
|
|
199
|
+
codePoint =
|
|
200
|
+
((first & 0x0f) << 12) |
|
|
201
|
+
((bytes[index++] & 0x3f) << 6) |
|
|
202
|
+
(bytes[index++] & 0x3f);
|
|
203
|
+
}
|
|
204
|
+
else if ((first & 0xf8) === 0xf0 && index + 2 < bytes.length) {
|
|
205
|
+
codePoint =
|
|
206
|
+
((first & 0x07) << 18) |
|
|
207
|
+
((bytes[index++] & 0x3f) << 12) |
|
|
208
|
+
((bytes[index++] & 0x3f) << 6) |
|
|
209
|
+
(bytes[index++] & 0x3f);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
codePoint = 0xfffd;
|
|
213
|
+
}
|
|
214
|
+
output += String.fromCodePoint(codePoint);
|
|
215
|
+
}
|
|
216
|
+
return output;
|
|
217
|
+
}
|
|
218
|
+
export function crc16Ccitt(bytes) {
|
|
219
|
+
let crc = 0xffff;
|
|
220
|
+
for (const byte of bytes) {
|
|
221
|
+
crc ^= byte << 8;
|
|
222
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
223
|
+
crc = ((crc << 1) ^ (crc & 0x8000 ? 0x1021 : 0)) & 0xffff;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return crc;
|
|
227
|
+
}
|
|
228
|
+
function equalBytes(left, right) {
|
|
229
|
+
if (left.length !== right.length)
|
|
230
|
+
return false;
|
|
231
|
+
let mismatch = 0;
|
|
232
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
233
|
+
mismatch |= left[index] ^ right[index];
|
|
234
|
+
}
|
|
235
|
+
return mismatch === 0;
|
|
236
|
+
}
|
|
237
|
+
function bytesToBits(bytes) {
|
|
238
|
+
const bits = new Uint8Array(bytes.length * 8);
|
|
239
|
+
for (let index = 0; index < bits.length; index += 1) {
|
|
240
|
+
bits[index] = (bytes[index >>> 3] >>> (7 - (index & 7))) & 1;
|
|
241
|
+
}
|
|
242
|
+
return bits;
|
|
243
|
+
}
|
|
244
|
+
function bitsToBytes(bits) {
|
|
245
|
+
const bytes = new Uint8Array(Math.ceil(bits.length / 8));
|
|
246
|
+
for (let index = 0; index < bits.length; index += 1) {
|
|
247
|
+
bytes[index >>> 3] =
|
|
248
|
+
bytes[index >>> 3] | (bits[index] << (7 - (index & 7)));
|
|
249
|
+
}
|
|
250
|
+
return bytes;
|
|
251
|
+
}
|
|
252
|
+
export function buildInvisibleWatermarkFrame(payload, key) {
|
|
253
|
+
const payloadBytes = encodeUtf8(payload);
|
|
254
|
+
const keyBytes = encodeUtf8(key);
|
|
255
|
+
validatePayloadAndKey(payloadBytes, keyBytes);
|
|
256
|
+
const frame = new Uint8Array(FRAME_BYTES);
|
|
257
|
+
frame[0] = MAGIC_0;
|
|
258
|
+
frame[1] = MAGIC_1;
|
|
259
|
+
frame[2] = FRAME_VERSION;
|
|
260
|
+
frame[3] = payloadBytes.length;
|
|
261
|
+
frame.set(payloadBytes, PAYLOAD_OFFSET);
|
|
262
|
+
const crc = crc16Ccitt(frame.subarray(0, CRC_OFFSET));
|
|
263
|
+
frame[CRC_OFFSET] = crc >>> 8;
|
|
264
|
+
frame[CRC_OFFSET + 1] = crc & 0xff;
|
|
265
|
+
const tag = hmacSha256(keyBytes, frame.subarray(0, AUTH_OFFSET));
|
|
266
|
+
frame.set(tag.subarray(0, 4), AUTH_OFFSET);
|
|
267
|
+
return frame;
|
|
268
|
+
}
|
|
269
|
+
function parseFrame(frame, keyBytes) {
|
|
270
|
+
if (frame.length !== FRAME_BYTES ||
|
|
271
|
+
frame[0] !== MAGIC_0 ||
|
|
272
|
+
frame[1] !== MAGIC_1 ||
|
|
273
|
+
frame[2] !== FRAME_VERSION ||
|
|
274
|
+
frame[3] < 1 ||
|
|
275
|
+
frame[3] > PAYLOAD_BYTES) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const expectedCrc = crc16Ccitt(frame.subarray(0, CRC_OFFSET));
|
|
279
|
+
const actualCrc = (frame[CRC_OFFSET] << 8) | frame[CRC_OFFSET + 1];
|
|
280
|
+
if (expectedCrc !== actualCrc)
|
|
281
|
+
return null;
|
|
282
|
+
const expectedTag = hmacSha256(keyBytes, frame.subarray(0, AUTH_OFFSET)).subarray(0, 4);
|
|
283
|
+
if (!equalBytes(expectedTag, frame.subarray(AUTH_OFFSET)))
|
|
284
|
+
return null;
|
|
285
|
+
return decodeUtf8(frame.subarray(PAYLOAD_OFFSET, PAYLOAD_OFFSET + frame[3]));
|
|
286
|
+
}
|
|
287
|
+
function xorshift32(value) {
|
|
288
|
+
let output = value >>> 0;
|
|
289
|
+
output ^= output << 13;
|
|
290
|
+
output ^= output >>> 17;
|
|
291
|
+
output ^= output << 5;
|
|
292
|
+
return output >>> 0;
|
|
293
|
+
}
|
|
294
|
+
function seedForKey(keyBytes) {
|
|
295
|
+
const label = encodeUtf8('react-native-image-marker:dct-qim-v1');
|
|
296
|
+
const digest = hmacSha256(keyBytes, label);
|
|
297
|
+
const seed = new DataView(digest.buffer, digest.byteOffset, digest.byteLength).getUint32(0);
|
|
298
|
+
return seed || 0x6d2b79f5;
|
|
299
|
+
}
|
|
300
|
+
export function createInvisibleWatermarkPermutation(key) {
|
|
301
|
+
const keyBytes = encodeUtf8(key);
|
|
302
|
+
if (keyBytes.length < INVISIBLE_WATERMARK_MIN_KEY_BYTES) {
|
|
303
|
+
throw new Error(`key must contain at least ${INVISIBLE_WATERMARK_MIN_KEY_BYTES} UTF-8 bytes.`);
|
|
304
|
+
}
|
|
305
|
+
const output = new Uint16Array(FRAME_BITS);
|
|
306
|
+
for (let index = 0; index < output.length; index += 1)
|
|
307
|
+
output[index] = index;
|
|
308
|
+
let state = seedForKey(keyBytes);
|
|
309
|
+
for (let index = output.length - 1; index > 0; index -= 1) {
|
|
310
|
+
state = xorshift32(state);
|
|
311
|
+
const swapIndex = state % (index + 1);
|
|
312
|
+
const value = output[index];
|
|
313
|
+
output[index] = output[swapIndex];
|
|
314
|
+
output[swapIndex] = value;
|
|
315
|
+
}
|
|
316
|
+
return output;
|
|
317
|
+
}
|
|
318
|
+
function mixSlot(seed, slot) {
|
|
319
|
+
let mixed = (seed ^ Math.imul(slot + 1, 0x9e3779b1)) >>> 0;
|
|
320
|
+
mixed ^= mixed >>> 16;
|
|
321
|
+
mixed = Math.imul(mixed, 0x85ebca6b) >>> 0;
|
|
322
|
+
mixed ^= mixed >>> 13;
|
|
323
|
+
mixed = Math.imul(mixed, 0xc2b2ae35) >>> 0;
|
|
324
|
+
mixed ^= mixed >>> 16;
|
|
325
|
+
return mixed >>> 0;
|
|
326
|
+
}
|
|
327
|
+
function pairAndDither(seed, slot, delta) {
|
|
328
|
+
const mixed = mixSlot(seed, slot);
|
|
329
|
+
return {
|
|
330
|
+
pair: COEFFICIENT_PAIRS[mixed & 3],
|
|
331
|
+
dither: (((mixed >>> 8) & 0xffffff) / 0x1000000 - 0.5) * delta,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function validatePayloadAndKey(payloadBytes, keyBytes) {
|
|
335
|
+
if (payloadBytes.length < 1 || payloadBytes.length > PAYLOAD_BYTES) {
|
|
336
|
+
throw new Error(`payload must contain between 1 and ${PAYLOAD_BYTES} UTF-8 bytes.`);
|
|
337
|
+
}
|
|
338
|
+
if (keyBytes.length < INVISIBLE_WATERMARK_MIN_KEY_BYTES) {
|
|
339
|
+
throw new Error(`key must contain at least ${INVISIBLE_WATERMARK_MIN_KEY_BYTES} UTF-8 bytes.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function validateImageDimensions(width, height) {
|
|
343
|
+
if (!Number.isInteger(width) ||
|
|
344
|
+
!Number.isInteger(height) ||
|
|
345
|
+
width < INVISIBLE_WATERMARK_MIN_WIDTH ||
|
|
346
|
+
height < INVISIBLE_WATERMARK_MIN_HEIGHT) {
|
|
347
|
+
throw new Error(`invisible watermark images must be at least ${INVISIBLE_WATERMARK_MIN_WIDTH}x${INVISIBLE_WATERMARK_MIN_HEIGHT} pixels.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function validateStrength(strength) {
|
|
351
|
+
const resolved = strength ?? 'balanced';
|
|
352
|
+
if (!Object.prototype.hasOwnProperty.call(STRENGTH_DELTAS, resolved)) {
|
|
353
|
+
throw new Error(`Unsupported invisible watermark strength: ${resolved}.`);
|
|
354
|
+
}
|
|
355
|
+
return resolved;
|
|
356
|
+
}
|
|
357
|
+
function validateSearch(search) {
|
|
358
|
+
const resolved = search ?? 'fast';
|
|
359
|
+
if (resolved !== 'fast' && resolved !== 'robust') {
|
|
360
|
+
throw new Error(`Unsupported invisible watermark search mode: ${resolved}.`);
|
|
361
|
+
}
|
|
362
|
+
return resolved;
|
|
363
|
+
}
|
|
364
|
+
function validateOutputNumber(value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
365
|
+
if (value !== undefined &&
|
|
366
|
+
(!Number.isFinite(value) ||
|
|
367
|
+
!Number.isInteger(value) ||
|
|
368
|
+
value < minimum ||
|
|
369
|
+
value > maximum)) {
|
|
370
|
+
throw new Error(name === 'quality'
|
|
371
|
+
? 'quality must be a finite integer between 0 and 100.'
|
|
372
|
+
: 'maxSize must be a positive finite integer.');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
export function validateEmbedInvisibleOptions(options) {
|
|
376
|
+
if (!options?.image?.src)
|
|
377
|
+
throw new Error('please set image!');
|
|
378
|
+
validatePayloadAndKey(encodeUtf8(options.payload ?? ''), encodeUtf8(options.key ?? ''));
|
|
379
|
+
validateStrength(options.strength);
|
|
380
|
+
validateOutputNumber(options.quality, 'quality', 0, 100);
|
|
381
|
+
validateOutputNumber(options.maxSize, 'maxSize', 1);
|
|
382
|
+
if (options.saveFormat !== undefined &&
|
|
383
|
+
options.saveFormat !== 'jpg' &&
|
|
384
|
+
options.saveFormat !== 'png' &&
|
|
385
|
+
options.saveFormat !== 'webp' &&
|
|
386
|
+
options.saveFormat !== 'base64') {
|
|
387
|
+
throw new Error(`saveFormat is not supported: ${String(options.saveFormat)}.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
export function validateDetectInvisibleOptions(options) {
|
|
391
|
+
if (!options?.image?.src)
|
|
392
|
+
throw new Error('please set image!');
|
|
393
|
+
const keyBytes = encodeUtf8(options.key ?? '');
|
|
394
|
+
if (keyBytes.length < INVISIBLE_WATERMARK_MIN_KEY_BYTES) {
|
|
395
|
+
throw new Error(`key must contain at least ${INVISIBLE_WATERMARK_MIN_KEY_BYTES} UTF-8 bytes.`);
|
|
396
|
+
}
|
|
397
|
+
validateStrength(options.strength);
|
|
398
|
+
validateSearch(options.search);
|
|
399
|
+
validateOutputNumber(options.maxSize, 'maxSize', 1);
|
|
400
|
+
if (options.worker !== undefined) {
|
|
401
|
+
if (typeof options.worker.scriptUrl !== 'string' ||
|
|
402
|
+
options.worker.scriptUrl.trim().length === 0) {
|
|
403
|
+
throw new Error('worker.scriptUrl must be a non-empty string.');
|
|
404
|
+
}
|
|
405
|
+
if (options.worker.onProgress !== undefined &&
|
|
406
|
+
typeof options.worker.onProgress !== 'function') {
|
|
407
|
+
throw new Error('worker.onProgress must be a function.');
|
|
408
|
+
}
|
|
409
|
+
if (options.worker.signal !== undefined &&
|
|
410
|
+
(typeof options.worker.signal.aborted !== 'boolean' ||
|
|
411
|
+
typeof options.worker.signal.addEventListener !== 'function' ||
|
|
412
|
+
typeof options.worker.signal.removeEventListener !== 'function')) {
|
|
413
|
+
throw new Error('worker.signal must be an AbortSignal.');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function nearestParity(value, bit) {
|
|
418
|
+
const rounded = Math.round(value);
|
|
419
|
+
if ((rounded & 1) === bit)
|
|
420
|
+
return rounded;
|
|
421
|
+
const lower = rounded - 1;
|
|
422
|
+
const upper = rounded + 1;
|
|
423
|
+
return Math.abs(value - lower) <= Math.abs(value - upper) ? lower : upper;
|
|
424
|
+
}
|
|
425
|
+
function basisValue(u, v, x, y, size = BLOCK_SIZE) {
|
|
426
|
+
const alphaU = u === 0 ? Math.sqrt(1 / size) : Math.sqrt(2 / size);
|
|
427
|
+
const alphaV = v === 0 ? Math.sqrt(1 / size) : Math.sqrt(2 / size);
|
|
428
|
+
return (alphaU *
|
|
429
|
+
alphaV *
|
|
430
|
+
Math.cos(((2 * x + 1) * u * Math.PI) / (2 * size)) *
|
|
431
|
+
Math.cos(((2 * y + 1) * v * Math.PI) / (2 * size)));
|
|
432
|
+
}
|
|
433
|
+
const BASIS_CACHE = new Map();
|
|
434
|
+
function basis(u, v) {
|
|
435
|
+
const key = `${u}:${v}`;
|
|
436
|
+
const cached = BASIS_CACHE.get(key);
|
|
437
|
+
if (cached)
|
|
438
|
+
return cached;
|
|
439
|
+
const output = new Float64Array(BLOCK_SIZE * BLOCK_SIZE);
|
|
440
|
+
for (let y = 0; y < BLOCK_SIZE; y += 1) {
|
|
441
|
+
for (let x = 0; x < BLOCK_SIZE; x += 1) {
|
|
442
|
+
output[y * BLOCK_SIZE + x] = basisValue(u, v, x, y);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
BASIS_CACHE.set(key, output);
|
|
446
|
+
return output;
|
|
447
|
+
}
|
|
448
|
+
function readLuminanceBlock(buffer, startX, startY) {
|
|
449
|
+
const output = new Float64Array(BLOCK_SIZE * BLOCK_SIZE);
|
|
450
|
+
let opaque = 0;
|
|
451
|
+
for (let y = 0; y < BLOCK_SIZE; y += 1) {
|
|
452
|
+
for (let x = 0; x < BLOCK_SIZE; x += 1) {
|
|
453
|
+
const pixel = ((startY + y) * buffer.width + startX + x) * 4;
|
|
454
|
+
const alpha = buffer.data[pixel + 3];
|
|
455
|
+
if (alpha >= 224)
|
|
456
|
+
opaque += 1;
|
|
457
|
+
output[y * BLOCK_SIZE + x] =
|
|
458
|
+
buffer.data[pixel] * 0.299 +
|
|
459
|
+
buffer.data[pixel + 1] * 0.587 +
|
|
460
|
+
buffer.data[pixel + 2] * 0.114;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { luminance: output, usable: opaque >= 56 };
|
|
464
|
+
}
|
|
465
|
+
function coefficient(luminance, values) {
|
|
466
|
+
let output = 0;
|
|
467
|
+
for (let index = 0; index < luminance.length; index += 1) {
|
|
468
|
+
output += luminance[index] * values[index];
|
|
469
|
+
}
|
|
470
|
+
return output;
|
|
471
|
+
}
|
|
472
|
+
function coefficientDifference(luminance, pair) {
|
|
473
|
+
return (coefficient(luminance, basis(pair[0], pair[1])) -
|
|
474
|
+
coefficient(luminance, basis(pair[2], pair[3])));
|
|
475
|
+
}
|
|
476
|
+
function clampByte(value) {
|
|
477
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
478
|
+
}
|
|
479
|
+
function embedBlock(buffer, startX, startY, bit, pair, dither, delta) {
|
|
480
|
+
const block = readLuminanceBlock(buffer, startX, startY);
|
|
481
|
+
if (!block.usable)
|
|
482
|
+
return;
|
|
483
|
+
const firstBasis = basis(pair[0], pair[1]);
|
|
484
|
+
const secondBasis = basis(pair[2], pair[3]);
|
|
485
|
+
const difference = coefficient(block.luminance, firstBasis) -
|
|
486
|
+
coefficient(block.luminance, secondBasis);
|
|
487
|
+
const quantized = nearestParity((difference - dither) / delta, bit);
|
|
488
|
+
const target = quantized * delta + dither;
|
|
489
|
+
const coefficientShift = (target - difference) / 2;
|
|
490
|
+
for (let y = 0; y < BLOCK_SIZE; y += 1) {
|
|
491
|
+
for (let x = 0; x < BLOCK_SIZE; x += 1) {
|
|
492
|
+
const blockIndex = y * BLOCK_SIZE + x;
|
|
493
|
+
const pixel = ((startY + y) * buffer.width + startX + x) * 4;
|
|
494
|
+
if (buffer.data[pixel + 3] < 224)
|
|
495
|
+
continue;
|
|
496
|
+
const luminanceShift = coefficientShift * (firstBasis[blockIndex] - secondBasis[blockIndex]);
|
|
497
|
+
buffer.data[pixel] = clampByte(buffer.data[pixel] + luminanceShift);
|
|
498
|
+
buffer.data[pixel + 1] = clampByte(buffer.data[pixel + 1] + luminanceShift);
|
|
499
|
+
buffer.data[pixel + 2] = clampByte(buffer.data[pixel + 2] + luminanceShift);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
export function embedInvisibleWatermarkPixels(buffer, options) {
|
|
504
|
+
validateImageDimensions(buffer.width, buffer.height);
|
|
505
|
+
if (buffer.data.length < buffer.width * buffer.height * 4) {
|
|
506
|
+
throw new Error('RGBA pixel buffer is smaller than its declared dimensions.');
|
|
507
|
+
}
|
|
508
|
+
const frame = buildInvisibleWatermarkFrame(options.payload, options.key);
|
|
509
|
+
const bits = bytesToBits(frame);
|
|
510
|
+
const keyBytes = encodeUtf8(options.key);
|
|
511
|
+
const seed = seedForKey(keyBytes);
|
|
512
|
+
const permutation = createInvisibleWatermarkPermutation(options.key);
|
|
513
|
+
const delta = STRENGTH_DELTAS[validateStrength(options.strength)];
|
|
514
|
+
const blocksX = Math.floor(buffer.width / BLOCK_SIZE);
|
|
515
|
+
const blocksY = Math.floor(buffer.height / BLOCK_SIZE);
|
|
516
|
+
for (let blockY = 0; blockY < blocksY; blockY += 1) {
|
|
517
|
+
for (let blockX = 0; blockX < blocksX; blockX += 1) {
|
|
518
|
+
const slot = (blockY % TILE_HEIGHT) * TILE_WIDTH + (blockX % TILE_WIDTH);
|
|
519
|
+
const { pair, dither } = pairAndDither(seed, slot, delta);
|
|
520
|
+
embedBlock(buffer, blockX * BLOCK_SIZE, blockY * BLOCK_SIZE, bits[permutation[slot]], pair, dither, delta);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function observeGrid(buffer, offsetX, offsetY) {
|
|
525
|
+
const blocksX = Math.floor((buffer.width - offsetX) / BLOCK_SIZE);
|
|
526
|
+
const blocksY = Math.floor((buffer.height - offsetY) / BLOCK_SIZE);
|
|
527
|
+
const observations = [];
|
|
528
|
+
for (let blockY = 0; blockY < blocksY; blockY += 1) {
|
|
529
|
+
for (let blockX = 0; blockX < blocksX; blockX += 1) {
|
|
530
|
+
const block = readLuminanceBlock(buffer, offsetX + blockX * BLOCK_SIZE, offsetY + blockY * BLOCK_SIZE);
|
|
531
|
+
if (!block.usable)
|
|
532
|
+
continue;
|
|
533
|
+
const differences = new Float64Array(COEFFICIENT_PAIRS.length);
|
|
534
|
+
COEFFICIENT_PAIRS.forEach((pair, index) => {
|
|
535
|
+
differences[index] = coefficientDifference(block.luminance, pair);
|
|
536
|
+
});
|
|
537
|
+
observations.push({ differences, blockX, blockY });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return observations;
|
|
541
|
+
}
|
|
542
|
+
async function yieldToEventLoop() {
|
|
543
|
+
const scheduler = globalThis.scheduler;
|
|
544
|
+
if (typeof scheduler?.yield === 'function') {
|
|
545
|
+
await scheduler.yield();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
549
|
+
}
|
|
550
|
+
async function observeGridAsync(buffer, offsetX, offsetY) {
|
|
551
|
+
const blocksX = Math.floor((buffer.width - offsetX) / BLOCK_SIZE);
|
|
552
|
+
const blocksY = Math.floor((buffer.height - offsetY) / BLOCK_SIZE);
|
|
553
|
+
const observations = [];
|
|
554
|
+
for (let blockY = 0; blockY < blocksY; blockY += 1) {
|
|
555
|
+
for (let blockX = 0; blockX < blocksX; blockX += 1) {
|
|
556
|
+
const block = readLuminanceBlock(buffer, offsetX + blockX * BLOCK_SIZE, offsetY + blockY * BLOCK_SIZE);
|
|
557
|
+
if (!block.usable)
|
|
558
|
+
continue;
|
|
559
|
+
const differences = new Float64Array(COEFFICIENT_PAIRS.length);
|
|
560
|
+
COEFFICIENT_PAIRS.forEach((pair, index) => {
|
|
561
|
+
differences[index] = coefficientDifference(block.luminance, pair);
|
|
562
|
+
});
|
|
563
|
+
observations.push({ differences, blockX, blockY });
|
|
564
|
+
}
|
|
565
|
+
if ((blockY + 1) % 8 === 0) {
|
|
566
|
+
await yieldToEventLoop();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return observations;
|
|
570
|
+
}
|
|
571
|
+
function decodeCandidate(observations, keyBytes, permutation, seed, delta, phaseX, phaseY) {
|
|
572
|
+
const votes = new Float64Array(FRAME_BITS);
|
|
573
|
+
const counts = new Uint32Array(FRAME_BITS);
|
|
574
|
+
const hardBits = new Uint8Array(observations.length);
|
|
575
|
+
const frameIndexes = new Uint16Array(observations.length);
|
|
576
|
+
observations.forEach((observation, observationIndex) => {
|
|
577
|
+
const slot = ((observation.blockY + phaseY) % TILE_HEIGHT) * TILE_WIDTH +
|
|
578
|
+
((observation.blockX + phaseX) % TILE_WIDTH);
|
|
579
|
+
const mixed = mixSlot(seed, slot);
|
|
580
|
+
const pairIndex = mixed & 3;
|
|
581
|
+
const dither = (((mixed >>> 8) & 0xffffff) / 0x1000000 - 0.5) * delta;
|
|
582
|
+
const normalized = (observation.differences[pairIndex] - dither) / delta;
|
|
583
|
+
const rounded = Math.round(normalized);
|
|
584
|
+
const bit = Math.abs(rounded % 2);
|
|
585
|
+
const reliability = Math.max(0, Math.min(1, Math.abs(normalized - Math.floor(normalized) - 0.5) * 2));
|
|
586
|
+
const frameIndex = permutation[slot];
|
|
587
|
+
votes[frameIndex] =
|
|
588
|
+
votes[frameIndex] + (bit === 1 ? reliability : -reliability);
|
|
589
|
+
counts[frameIndex] = counts[frameIndex] + 1;
|
|
590
|
+
hardBits[observationIndex] = bit;
|
|
591
|
+
frameIndexes[observationIndex] = frameIndex;
|
|
592
|
+
});
|
|
593
|
+
if (counts.some((count) => count === 0))
|
|
594
|
+
return null;
|
|
595
|
+
const decodedBits = new Uint8Array(FRAME_BITS);
|
|
596
|
+
for (let index = 0; index < FRAME_BITS; index += 1) {
|
|
597
|
+
decodedBits[index] = votes[index] >= 0 ? 1 : 0;
|
|
598
|
+
}
|
|
599
|
+
const frame = bitsToBytes(decodedBits);
|
|
600
|
+
const payload = parseFrame(frame, keyBytes);
|
|
601
|
+
if (payload === null)
|
|
602
|
+
return null;
|
|
603
|
+
let mismatches = 0;
|
|
604
|
+
hardBits.forEach((bit, index) => {
|
|
605
|
+
if (bit !== decodedBits[frameIndexes[index]])
|
|
606
|
+
mismatches += 1;
|
|
607
|
+
});
|
|
608
|
+
const bitErrorRate = observations.length
|
|
609
|
+
? mismatches / observations.length
|
|
610
|
+
: 1;
|
|
611
|
+
let margin = 0;
|
|
612
|
+
for (let index = 0; index < FRAME_BITS; index += 1) {
|
|
613
|
+
margin += Math.min(1, Math.abs(votes[index]) / counts[index]);
|
|
614
|
+
}
|
|
615
|
+
margin /= FRAME_BITS;
|
|
616
|
+
return {
|
|
617
|
+
detected: true,
|
|
618
|
+
payload,
|
|
619
|
+
confidence: Math.max(0, Math.min(1, margin * 0.6 + (1 - bitErrorRate) * 0.4)),
|
|
620
|
+
bitErrorRate,
|
|
621
|
+
algorithm: INVISIBLE_WATERMARK_ALGORITHM,
|
|
622
|
+
frame,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function createDetectionContext(options) {
|
|
626
|
+
const keyBytes = encodeUtf8(options.key ?? '');
|
|
627
|
+
if (keyBytes.length < INVISIBLE_WATERMARK_MIN_KEY_BYTES) {
|
|
628
|
+
throw new Error(`key must contain at least ${INVISIBLE_WATERMARK_MIN_KEY_BYTES} UTF-8 bytes.`);
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
keyBytes,
|
|
632
|
+
delta: STRENGTH_DELTAS[validateStrength(options.strength)],
|
|
633
|
+
permutation: createInvisibleWatermarkPermutation(options.key),
|
|
634
|
+
seed: seedForKey(keyBytes),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function validatePixelBuffer(buffer) {
|
|
638
|
+
validateImageDimensions(buffer.width, buffer.height);
|
|
639
|
+
if (buffer.data.length < buffer.width * buffer.height * 4) {
|
|
640
|
+
throw new Error('RGBA pixel buffer is smaller than its declared dimensions.');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function detectAtScale(buffer, context, search, scale = 1) {
|
|
644
|
+
const offsets = search === 'robust' ? BLOCK_SIZE : 1;
|
|
645
|
+
const phaseXs = search === 'robust' ? TILE_WIDTH : 1;
|
|
646
|
+
const phaseYs = search === 'robust' ? TILE_HEIGHT : 1;
|
|
647
|
+
let best = null;
|
|
648
|
+
for (let offsetY = 0; offsetY < offsets; offsetY += 1) {
|
|
649
|
+
for (let offsetX = 0; offsetX < offsets; offsetX += 1) {
|
|
650
|
+
const observations = observeGrid(buffer, offsetX, offsetY);
|
|
651
|
+
for (let phaseY = 0; phaseY < phaseYs; phaseY += 1) {
|
|
652
|
+
for (let phaseX = 0; phaseX < phaseXs; phaseX += 1) {
|
|
653
|
+
const candidate = decodeCandidate(observations, context.keyBytes, context.permutation, context.seed, context.delta, phaseX, phaseY);
|
|
654
|
+
if (candidate && (!best || candidate.confidence > best.confidence)) {
|
|
655
|
+
candidate.scale = scale;
|
|
656
|
+
best = candidate;
|
|
657
|
+
if (search === 'fast' || candidate.confidence >= 0.98) {
|
|
658
|
+
return candidate;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return best;
|
|
666
|
+
}
|
|
667
|
+
async function detectAtScaleAsync(buffer, context, search, scale = 1) {
|
|
668
|
+
const offsets = search === 'robust' ? BLOCK_SIZE : 1;
|
|
669
|
+
const phaseXs = search === 'robust' ? TILE_WIDTH : 1;
|
|
670
|
+
const phaseYs = search === 'robust' ? TILE_HEIGHT : 1;
|
|
671
|
+
let best = null;
|
|
672
|
+
let candidatesSinceYield = 0;
|
|
673
|
+
for (let offsetY = 0; offsetY < offsets; offsetY += 1) {
|
|
674
|
+
for (let offsetX = 0; offsetX < offsets; offsetX += 1) {
|
|
675
|
+
const observations = await observeGridAsync(buffer, offsetX, offsetY);
|
|
676
|
+
for (let phaseY = 0; phaseY < phaseYs; phaseY += 1) {
|
|
677
|
+
for (let phaseX = 0; phaseX < phaseXs; phaseX += 1) {
|
|
678
|
+
const candidate = decodeCandidate(observations, context.keyBytes, context.permutation, context.seed, context.delta, phaseX, phaseY);
|
|
679
|
+
if (candidate && (!best || candidate.confidence > best.confidence)) {
|
|
680
|
+
candidate.scale = scale;
|
|
681
|
+
best = candidate;
|
|
682
|
+
if (search === 'fast' || candidate.confidence >= 0.98) {
|
|
683
|
+
return candidate;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
candidatesSinceYield += 1;
|
|
687
|
+
if (candidatesSinceYield >= 16) {
|
|
688
|
+
candidatesSinceYield = 0;
|
|
689
|
+
await yieldToEventLoop();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return best;
|
|
696
|
+
}
|
|
697
|
+
function detectResizedCandidate(buffer, context, scale) {
|
|
698
|
+
const observations = observeGrid(buffer, 0, 0);
|
|
699
|
+
for (const factor of INVISIBLE_WATERMARK_RESIZE_DELTA_FACTORS) {
|
|
700
|
+
const candidate = decodeCandidate(observations, context.keyBytes, context.permutation, context.seed, context.delta * factor, 0, 0);
|
|
701
|
+
if (candidate) {
|
|
702
|
+
candidate.scale = scale;
|
|
703
|
+
return candidate;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
async function detectResizedCandidateAsync(buffer, context, scale) {
|
|
709
|
+
const observations = await observeGridAsync(buffer, 0, 0);
|
|
710
|
+
for (const factor of INVISIBLE_WATERMARK_RESIZE_DELTA_FACTORS) {
|
|
711
|
+
const candidate = decodeCandidate(observations, context.keyBytes, context.permutation, context.seed, context.delta * factor, 0, 0);
|
|
712
|
+
if (candidate) {
|
|
713
|
+
candidate.scale = scale;
|
|
714
|
+
return candidate;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function resizeTarget(buffer, scale) {
|
|
720
|
+
const width = Math.max(1, Math.round(buffer.width / scale));
|
|
721
|
+
const height = Math.max(1, Math.round(buffer.height / scale));
|
|
722
|
+
return width >= INVISIBLE_WATERMARK_MIN_WIDTH &&
|
|
723
|
+
height >= INVISIBLE_WATERMARK_MIN_HEIGHT
|
|
724
|
+
? { width, height }
|
|
725
|
+
: null;
|
|
726
|
+
}
|
|
727
|
+
export function resizeInvisibleWatermarkPixels(buffer, width, height) {
|
|
728
|
+
if (!Number.isInteger(width) ||
|
|
729
|
+
!Number.isInteger(height) ||
|
|
730
|
+
width <= 0 ||
|
|
731
|
+
height <= 0) {
|
|
732
|
+
throw new Error('Invisible watermark resize dimensions must be positive integers.');
|
|
733
|
+
}
|
|
734
|
+
const output = new Uint8ClampedArray(width * height * 4);
|
|
735
|
+
const scaleX = buffer.width / width;
|
|
736
|
+
const scaleY = buffer.height / height;
|
|
737
|
+
for (let targetY = 0; targetY < height; targetY += 1) {
|
|
738
|
+
const sourceY = (targetY + 0.5) * scaleY - 0.5;
|
|
739
|
+
const top = Math.max(0, Math.min(buffer.height - 1, Math.floor(sourceY)));
|
|
740
|
+
const bottom = Math.min(buffer.height - 1, top + 1);
|
|
741
|
+
const weightY = Math.max(0, Math.min(1, sourceY - top));
|
|
742
|
+
for (let targetX = 0; targetX < width; targetX += 1) {
|
|
743
|
+
const sourceX = (targetX + 0.5) * scaleX - 0.5;
|
|
744
|
+
const left = Math.max(0, Math.min(buffer.width - 1, Math.floor(sourceX)));
|
|
745
|
+
const right = Math.min(buffer.width - 1, left + 1);
|
|
746
|
+
const weightX = Math.max(0, Math.min(1, sourceX - left));
|
|
747
|
+
const topLeft = (top * buffer.width + left) * 4;
|
|
748
|
+
const topRight = (top * buffer.width + right) * 4;
|
|
749
|
+
const bottomLeft = (bottom * buffer.width + left) * 4;
|
|
750
|
+
const bottomRight = (bottom * buffer.width + right) * 4;
|
|
751
|
+
const target = (targetY * width + targetX) * 4;
|
|
752
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
753
|
+
const topValue = buffer.data[topLeft + channel] * (1 - weightX) +
|
|
754
|
+
buffer.data[topRight + channel] * weightX;
|
|
755
|
+
const bottomValue = buffer.data[bottomLeft + channel] * (1 - weightX) +
|
|
756
|
+
buffer.data[bottomRight + channel] * weightX;
|
|
757
|
+
output[target + channel] =
|
|
758
|
+
topValue * (1 - weightY) + bottomValue * weightY;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { data: output, width, height };
|
|
763
|
+
}
|
|
764
|
+
function resizeInvisibleWatermarkPixelsNearest(buffer, width, height) {
|
|
765
|
+
const output = new Uint8ClampedArray(width * height * 4);
|
|
766
|
+
for (let targetY = 0; targetY < height; targetY += 1) {
|
|
767
|
+
const sourceY = Math.min(buffer.height - 1, Math.floor(((targetY + 0.5) * buffer.height) / height));
|
|
768
|
+
for (let targetX = 0; targetX < width; targetX += 1) {
|
|
769
|
+
const sourceX = Math.min(buffer.width - 1, Math.floor(((targetX + 0.5) * buffer.width) / width));
|
|
770
|
+
const source = (sourceY * buffer.width + sourceX) * 4;
|
|
771
|
+
const target = (targetY * width + targetX) * 4;
|
|
772
|
+
output[target] = buffer.data[source];
|
|
773
|
+
output[target + 1] = buffer.data[source + 1];
|
|
774
|
+
output[target + 2] = buffer.data[source + 2];
|
|
775
|
+
output[target + 3] = buffer.data[source + 3];
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return { data: output, width, height };
|
|
779
|
+
}
|
|
780
|
+
async function resizeInvisibleWatermarkPixelsAsync(buffer, width, height) {
|
|
781
|
+
const output = new Uint8ClampedArray(width * height * 4);
|
|
782
|
+
const scaleX = buffer.width / width;
|
|
783
|
+
const scaleY = buffer.height / height;
|
|
784
|
+
for (let targetY = 0; targetY < height; targetY += 1) {
|
|
785
|
+
const sourceY = (targetY + 0.5) * scaleY - 0.5;
|
|
786
|
+
const top = Math.max(0, Math.min(buffer.height - 1, Math.floor(sourceY)));
|
|
787
|
+
const bottom = Math.min(buffer.height - 1, top + 1);
|
|
788
|
+
const weightY = Math.max(0, Math.min(1, sourceY - top));
|
|
789
|
+
for (let targetX = 0; targetX < width; targetX += 1) {
|
|
790
|
+
const sourceX = (targetX + 0.5) * scaleX - 0.5;
|
|
791
|
+
const left = Math.max(0, Math.min(buffer.width - 1, Math.floor(sourceX)));
|
|
792
|
+
const right = Math.min(buffer.width - 1, left + 1);
|
|
793
|
+
const weightX = Math.max(0, Math.min(1, sourceX - left));
|
|
794
|
+
const topLeft = (top * buffer.width + left) * 4;
|
|
795
|
+
const topRight = (top * buffer.width + right) * 4;
|
|
796
|
+
const bottomLeft = (bottom * buffer.width + left) * 4;
|
|
797
|
+
const bottomRight = (bottom * buffer.width + right) * 4;
|
|
798
|
+
const target = (targetY * width + targetX) * 4;
|
|
799
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
800
|
+
const topValue = buffer.data[topLeft + channel] * (1 - weightX) +
|
|
801
|
+
buffer.data[topRight + channel] * weightX;
|
|
802
|
+
const bottomValue = buffer.data[bottomLeft + channel] * (1 - weightX) +
|
|
803
|
+
buffer.data[bottomRight + channel] * weightX;
|
|
804
|
+
output[target + channel] =
|
|
805
|
+
topValue * (1 - weightY) + bottomValue * weightY;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if ((targetY + 1) % 16 === 0) {
|
|
809
|
+
await yieldToEventLoop();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { data: output, width, height };
|
|
813
|
+
}
|
|
814
|
+
async function resizeInvisibleWatermarkPixelsNearestAsync(buffer, width, height) {
|
|
815
|
+
const output = new Uint8ClampedArray(width * height * 4);
|
|
816
|
+
for (let targetY = 0; targetY < height; targetY += 1) {
|
|
817
|
+
const sourceY = Math.min(buffer.height - 1, Math.floor(((targetY + 0.5) * buffer.height) / height));
|
|
818
|
+
for (let targetX = 0; targetX < width; targetX += 1) {
|
|
819
|
+
const sourceX = Math.min(buffer.width - 1, Math.floor(((targetX + 0.5) * buffer.width) / width));
|
|
820
|
+
const source = (sourceY * buffer.width + sourceX) * 4;
|
|
821
|
+
const target = (targetY * width + targetX) * 4;
|
|
822
|
+
output[target] = buffer.data[source];
|
|
823
|
+
output[target + 1] = buffer.data[source + 1];
|
|
824
|
+
output[target + 2] = buffer.data[source + 2];
|
|
825
|
+
output[target + 3] = buffer.data[source + 3];
|
|
826
|
+
}
|
|
827
|
+
if ((targetY + 1) % 32 === 0) {
|
|
828
|
+
await yieldToEventLoop();
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return { data: output, width, height };
|
|
832
|
+
}
|
|
833
|
+
function emptyDetectionResult() {
|
|
834
|
+
return {
|
|
835
|
+
detected: false,
|
|
836
|
+
confidence: 0,
|
|
837
|
+
algorithm: INVISIBLE_WATERMARK_ALGORITHM,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
export function detectInvisibleWatermarkPixels(buffer, options) {
|
|
841
|
+
validatePixelBuffer(buffer);
|
|
842
|
+
const context = createDetectionContext(options);
|
|
843
|
+
const search = validateSearch(options.search);
|
|
844
|
+
const scales = search === 'robust'
|
|
845
|
+
? [1, ...INVISIBLE_WATERMARK_RESIZE_SCALES]
|
|
846
|
+
: [1];
|
|
847
|
+
for (const scale of scales) {
|
|
848
|
+
const target = scale === 1 ? null : resizeTarget(buffer, scale);
|
|
849
|
+
if (scale !== 1 && !target)
|
|
850
|
+
continue;
|
|
851
|
+
const candidateBuffer = target
|
|
852
|
+
? scale < 1
|
|
853
|
+
? resizeInvisibleWatermarkPixelsNearest(buffer, target.width, target.height)
|
|
854
|
+
: resizeInvisibleWatermarkPixels(buffer, target.width, target.height)
|
|
855
|
+
: buffer;
|
|
856
|
+
const candidate = target
|
|
857
|
+
? detectResizedCandidate(candidateBuffer, context, scale)
|
|
858
|
+
: detectAtScale(candidateBuffer, context, 'fast', scale);
|
|
859
|
+
if (candidate)
|
|
860
|
+
return toPublicDetectionResult(candidate);
|
|
861
|
+
}
|
|
862
|
+
if (search === 'robust') {
|
|
863
|
+
const candidate = detectAtScale(buffer, context, 'robust');
|
|
864
|
+
if (candidate)
|
|
865
|
+
return toPublicDetectionResult(candidate);
|
|
866
|
+
}
|
|
867
|
+
return emptyDetectionResult();
|
|
868
|
+
}
|
|
869
|
+
export async function detectInvisibleWatermarkPixelsAsync(buffer, options) {
|
|
870
|
+
validatePixelBuffer(buffer);
|
|
871
|
+
const context = createDetectionContext(options);
|
|
872
|
+
const search = validateSearch(options.search);
|
|
873
|
+
const scales = search === 'robust'
|
|
874
|
+
? [1, ...INVISIBLE_WATERMARK_RESIZE_SCALES]
|
|
875
|
+
: [1];
|
|
876
|
+
for (const scale of scales) {
|
|
877
|
+
const target = scale === 1 ? null : resizeTarget(buffer, scale);
|
|
878
|
+
if (scale !== 1 && !target)
|
|
879
|
+
continue;
|
|
880
|
+
const candidateBuffer = target
|
|
881
|
+
? scale < 1
|
|
882
|
+
? await resizeInvisibleWatermarkPixelsNearestAsync(buffer, target.width, target.height)
|
|
883
|
+
: await resizeInvisibleWatermarkPixelsAsync(buffer, target.width, target.height)
|
|
884
|
+
: buffer;
|
|
885
|
+
const candidate = target
|
|
886
|
+
? await detectResizedCandidateAsync(candidateBuffer, context, scale)
|
|
887
|
+
: await detectAtScaleAsync(candidateBuffer, context, 'fast', scale);
|
|
888
|
+
if (candidate)
|
|
889
|
+
return toPublicDetectionResult(candidate);
|
|
890
|
+
}
|
|
891
|
+
if (search === 'robust') {
|
|
892
|
+
const candidate = await detectAtScaleAsync(buffer, context, 'robust');
|
|
893
|
+
if (candidate)
|
|
894
|
+
return toPublicDetectionResult(candidate);
|
|
895
|
+
}
|
|
896
|
+
return emptyDetectionResult();
|
|
897
|
+
}
|