@sythos/js_barcode_universal 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/LICENSE +215 -0
- package/NOTICE.md +106 -0
- package/README.md +433 -0
- package/bundle/sythos-barcode.esm.js +7998 -0
- package/bundle/sythos-barcode.js +7948 -0
- package/examples/create.html +731 -0
- package/examples/read.html +341 -0
- package/licenses/README.md +42 -0
- package/licenses/codabar.license +74 -0
- package/licenses/code-11.license +69 -0
- package/licenses/code-128.license +69 -0
- package/licenses/code-39.license +70 -0
- package/licenses/code-93.license +71 -0
- package/licenses/ean-13.license +70 -0
- package/licenses/ean-8.license +70 -0
- package/licenses/gs1-128.license +71 -0
- package/licenses/isbn.license +76 -0
- package/licenses/itf-14.license +69 -0
- package/licenses/itf.license +70 -0
- package/licenses/msi-plessey.license +72 -0
- package/licenses/pharmacode.license +71 -0
- package/licenses/qr-code.license +75 -0
- package/licenses/upc-a.license +72 -0
- package/licenses/upc-e.license +69 -0
- package/package.json +89 -0
- package/src/core/bit-buffer.js +174 -0
- package/src/core/bit-matrix.js +241 -0
- package/src/core/errors.js +61 -0
- package/src/core/galois-field.js +204 -0
- package/src/core/index.js +56 -0
- package/src/core/reed-solomon.js +313 -0
- package/src/image/binarizer.js +270 -0
- package/src/image/grid-sampler.js +164 -0
- package/src/image/index.js +40 -0
- package/src/image/luminance.js +196 -0
- package/src/image/perspective.js +195 -0
- package/src/index.js +240 -0
- package/src/oned/index.js +89 -0
- package/src/oned/patterns.js +384 -0
- package/src/oned/reader.js +918 -0
- package/src/oned/writers.js +741 -0
- package/src/qr/decoder.js +575 -0
- package/src/qr/detector.js +630 -0
- package/src/qr/encoder.js +958 -0
- package/src/qr/index.js +44 -0
- package/src/qr/tables.js +737 -0
- package/src/render/image-data.js +125 -0
- package/src/render/index.js +130 -0
- package/src/render/options.js +160 -0
- package/src/render/png.js +295 -0
- package/src/render/svg.js +120 -0
- package/src/render/webgl.js +206 -0
- package/src/render/webgpu.js +369 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reed-Solomon encoding and decoding over an arbitrary finite field.
|
|
33
|
+
*
|
|
34
|
+
* Systematic encoding: the output is the message followed by parity symbols,
|
|
35
|
+
* so the data is readable without decoding when the symbol is undamaged.
|
|
36
|
+
*
|
|
37
|
+
* Decoding is syndromes -> Berlekamp-Massey -> Chien search -> Forney.
|
|
38
|
+
* It corrects up to floor(eccLen / 2) symbol errors at unknown positions.
|
|
39
|
+
*
|
|
40
|
+
* Every arithmetic operation routes through the field object. There is
|
|
41
|
+
* deliberately not a single bare `^` in this file: XOR is correct for binary
|
|
42
|
+
* fields and wrong for GF(929), and the resulting bug is invisible to any test
|
|
43
|
+
* that only exercises QR or Data Matrix. See core/galois-field.js.
|
|
44
|
+
*
|
|
45
|
+
* Polynomial convention in this module: **coefficient index 0 is the highest
|
|
46
|
+
* degree**, matching the wire order of a codeword. The decoder converts to
|
|
47
|
+
* degree-ascending internally where the algorithms are stated that way.
|
|
48
|
+
*
|
|
49
|
+
* @module core/reed-solomon
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { ChecksumError } from './errors.js';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the generator polynomial for `eccLen` parity symbols.
|
|
56
|
+
*
|
|
57
|
+
* g(x) = product over i of (x - a^(base + i)), i = 0 .. eccLen-1
|
|
58
|
+
*
|
|
59
|
+
* `base` is 0 for QR, Data Matrix and Aztec; 1 for PDF417.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} eccLen
|
|
62
|
+
* @param {import('./galois-field.js').GaloisField} field
|
|
63
|
+
* @param {number} [base]
|
|
64
|
+
* @returns {number[]} Monic, degree-descending, length eccLen + 1.
|
|
65
|
+
*/
|
|
66
|
+
export function generatorPoly(eccLen, field, base = 0) {
|
|
67
|
+
let g = [1];
|
|
68
|
+
for (let i = 0; i < eccLen; i++) {
|
|
69
|
+
const root = field.exp(base + i);
|
|
70
|
+
const next = new Array(g.length + 1).fill(0);
|
|
71
|
+
for (let j = 0; j < g.length; j++) {
|
|
72
|
+
// g[j]*x lands at next[j]; g[j]*(-root) lands at next[j+1].
|
|
73
|
+
next[j] = field.add(next[j], g[j]);
|
|
74
|
+
next[j + 1] = field.sub(next[j + 1], field.mul(g[j], root));
|
|
75
|
+
}
|
|
76
|
+
g = next;
|
|
77
|
+
}
|
|
78
|
+
return g;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const generatorCache = new Map();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cached {@link generatorPoly}. Encoding the same format repeatedly is the
|
|
85
|
+
* common case and rebuilding the polynomial each time is pure waste.
|
|
86
|
+
*
|
|
87
|
+
* @param {number} eccLen
|
|
88
|
+
* @param {import('./galois-field.js').GaloisField} field
|
|
89
|
+
* @param {number} [base]
|
|
90
|
+
* @returns {number[]}
|
|
91
|
+
*/
|
|
92
|
+
function cachedGenerator(eccLen, field, base) {
|
|
93
|
+
const key = `${field.name}|${eccLen}|${base}`;
|
|
94
|
+
let g = generatorCache.get(key);
|
|
95
|
+
if (!g) {
|
|
96
|
+
g = generatorPoly(eccLen, field, base);
|
|
97
|
+
generatorCache.set(key, g);
|
|
98
|
+
}
|
|
99
|
+
return g;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compute `eccLen` parity symbols for `data`.
|
|
104
|
+
*
|
|
105
|
+
* @param {ArrayLike<number>} data
|
|
106
|
+
* @param {number} eccLen
|
|
107
|
+
* @param {import('./galois-field.js').GaloisField} field
|
|
108
|
+
* @param {number} [base]
|
|
109
|
+
* @returns {number[]} The parity symbols alone, length eccLen.
|
|
110
|
+
*/
|
|
111
|
+
export function rsEncode(data, eccLen, field, base = 0) {
|
|
112
|
+
if (eccLen <= 0) return [];
|
|
113
|
+
const gen = cachedGenerator(eccLen, field, base);
|
|
114
|
+
const res = new Array(data.length + eccLen).fill(0);
|
|
115
|
+
for (let i = 0; i < data.length; i++) res[i] = data[i];
|
|
116
|
+
|
|
117
|
+
// Synthetic division by a monic divisor: the leading coefficient is
|
|
118
|
+
// annihilated each step and its multiple subtracted from the tail.
|
|
119
|
+
for (let i = 0; i < data.length; i++) {
|
|
120
|
+
const coef = res[i];
|
|
121
|
+
if (coef === 0) continue;
|
|
122
|
+
for (let j = 1; j <= eccLen; j++) {
|
|
123
|
+
res[i + j] = field.sub(res[i + j], field.mul(gen[j], coef));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const remainder = res.slice(data.length);
|
|
128
|
+
|
|
129
|
+
// The codeword is data(x)*x^eccLen MINUS the remainder, so the parity
|
|
130
|
+
// symbols are the negated remainder. In a binary field negation is the
|
|
131
|
+
// identity and this is invisible; in GF(929) omitting it produces a
|
|
132
|
+
// codeword that is not divisible by the generator, and every symbol fails
|
|
133
|
+
// to decode. Exactly the class of bug the field abstraction exists to stop.
|
|
134
|
+
if (field.prime) {
|
|
135
|
+
for (let i = 0; i < remainder.length; i++) remainder[i] = field.neg(remainder[i]);
|
|
136
|
+
}
|
|
137
|
+
return remainder;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Evaluate a degree-descending polynomial at x (Horner).
|
|
142
|
+
*
|
|
143
|
+
* @param {ArrayLike<number>} poly
|
|
144
|
+
* @param {number} x
|
|
145
|
+
* @param {import('./galois-field.js').GaloisField} field
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function evalPoly(poly, x, field) {
|
|
149
|
+
let acc = 0;
|
|
150
|
+
for (let i = 0; i < poly.length; i++) {
|
|
151
|
+
acc = field.add(field.mul(acc, x), poly[i]);
|
|
152
|
+
}
|
|
153
|
+
return acc;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Correct errors in a received codeword, in place.
|
|
158
|
+
*
|
|
159
|
+
* @param {number[]} received Data followed by parity, degree-descending.
|
|
160
|
+
* @param {number} eccLen
|
|
161
|
+
* @param {import('./galois-field.js').GaloisField} field
|
|
162
|
+
* @param {number} [base]
|
|
163
|
+
* @returns {number} Number of symbols corrected.
|
|
164
|
+
* @throws {ChecksumError} If the damage exceeds the correction capacity.
|
|
165
|
+
*/
|
|
166
|
+
export function rsDecode(received, eccLen, field, base = 0) {
|
|
167
|
+
const n = received.length;
|
|
168
|
+
|
|
169
|
+
// --- Syndromes. S[i] = R(a^(base+i)); all zero means an intact codeword.
|
|
170
|
+
const syn = new Array(eccLen).fill(0);
|
|
171
|
+
let damaged = false;
|
|
172
|
+
for (let i = 0; i < eccLen; i++) {
|
|
173
|
+
const s = evalPoly(received, field.exp(base + i), field);
|
|
174
|
+
syn[i] = s;
|
|
175
|
+
if (s !== 0) damaged = true;
|
|
176
|
+
}
|
|
177
|
+
if (!damaged) return 0;
|
|
178
|
+
|
|
179
|
+
// --- Berlekamp-Massey. Degree-ascending here: lambda[k] is the coefficient
|
|
180
|
+
// of x^k, which is how the recurrence is naturally stated.
|
|
181
|
+
const lambda = new Array(eccLen + 1).fill(0);
|
|
182
|
+
const prev = new Array(eccLen + 1).fill(0);
|
|
183
|
+
const tmp = new Array(eccLen + 1).fill(0);
|
|
184
|
+
lambda[0] = 1;
|
|
185
|
+
prev[0] = 1;
|
|
186
|
+
let errCount = 0; // current LFSR length
|
|
187
|
+
let shift = 1; // steps since `prev` was last updated
|
|
188
|
+
let lastDisc = 1; // discrepancy at that update
|
|
189
|
+
|
|
190
|
+
for (let step = 0; step < eccLen; step++) {
|
|
191
|
+
let disc = syn[step];
|
|
192
|
+
for (let i = 1; i <= errCount; i++) {
|
|
193
|
+
disc = field.add(disc, field.mul(lambda[i], syn[step - i]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (disc === 0) {
|
|
197
|
+
shift++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const scale = field.div(disc, lastDisc);
|
|
202
|
+
tmp.fill(0);
|
|
203
|
+
for (let i = 0; i <= eccLen; i++) tmp[i] = lambda[i];
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i + shift <= eccLen; i++) {
|
|
206
|
+
if (prev[i] === 0) continue;
|
|
207
|
+
lambda[i + shift] = field.sub(lambda[i + shift], field.mul(scale, prev[i]));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (2 * errCount <= step) {
|
|
211
|
+
errCount = step + 1 - errCount;
|
|
212
|
+
for (let i = 0; i <= eccLen; i++) prev[i] = tmp[i];
|
|
213
|
+
lastDisc = disc;
|
|
214
|
+
shift = 1;
|
|
215
|
+
} else {
|
|
216
|
+
shift++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (errCount === 0 || errCount > eccLen / 2) {
|
|
221
|
+
throw new ChecksumError(
|
|
222
|
+
`Reed-Solomon: ${errCount} errors exceeds correction capacity ` +
|
|
223
|
+
`${Math.floor(eccLen / 2)} (${field.name})`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Chien search. Position p (counted from the low-order end) is in error
|
|
228
|
+
// when lambda(a^-p) == 0.
|
|
229
|
+
const positions = [];
|
|
230
|
+
for (let p = 0; p < n; p++) {
|
|
231
|
+
const xInv = field.exp(-p);
|
|
232
|
+
let acc = 0;
|
|
233
|
+
let term = 1;
|
|
234
|
+
for (let i = 0; i <= errCount; i++) {
|
|
235
|
+
acc = field.add(acc, field.mul(lambda[i], term));
|
|
236
|
+
term = field.mul(term, xInv);
|
|
237
|
+
}
|
|
238
|
+
if (acc === 0) positions.push(p);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (positions.length !== errCount) {
|
|
242
|
+
throw new ChecksumError(
|
|
243
|
+
`Reed-Solomon: located ${positions.length} of ${errCount} error positions`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- Error evaluator. omega(x) = [S(x) * lambda(x)] mod x^eccLen,
|
|
248
|
+
// with S degree-ascending.
|
|
249
|
+
const omega = new Array(eccLen).fill(0);
|
|
250
|
+
for (let i = 0; i < eccLen; i++) {
|
|
251
|
+
let acc = 0;
|
|
252
|
+
for (let j = 0; j <= i && j <= errCount; j++) {
|
|
253
|
+
acc = field.add(acc, field.mul(lambda[j], syn[i - j]));
|
|
254
|
+
}
|
|
255
|
+
omega[i] = acc;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Forney. For an error at position p, with X = a^p:
|
|
259
|
+
// magnitude = -X^(1-base) * omega(X^-1) / lambda'(X^-1)
|
|
260
|
+
// The sign is a no-op in binary fields and load-bearing in GF(929).
|
|
261
|
+
let corrected = 0;
|
|
262
|
+
for (const p of positions) {
|
|
263
|
+
const xInv = field.exp(-p);
|
|
264
|
+
|
|
265
|
+
let num = 0;
|
|
266
|
+
let term = 1;
|
|
267
|
+
for (let i = 0; i < eccLen; i++) {
|
|
268
|
+
num = field.add(num, field.mul(omega[i], term));
|
|
269
|
+
term = field.mul(term, xInv);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Formal derivative: only odd-index terms survive in a binary field, but
|
|
273
|
+
// in a prime field every term contributes with an integer multiplier.
|
|
274
|
+
let den = 0;
|
|
275
|
+
term = 1;
|
|
276
|
+
for (let i = 1; i <= errCount; i++) {
|
|
277
|
+
if (field.prime) {
|
|
278
|
+
// i * lambda[i] * x^(i-1), where `i` is repeated addition.
|
|
279
|
+
let mult = 0;
|
|
280
|
+
const t = field.mul(lambda[i], term);
|
|
281
|
+
for (let k = 0; k < i; k++) mult = field.add(mult, t);
|
|
282
|
+
den = field.add(den, mult);
|
|
283
|
+
} else if (i % 2 === 1) {
|
|
284
|
+
den = field.add(den, field.mul(lambda[i], term));
|
|
285
|
+
}
|
|
286
|
+
term = field.mul(term, xInv);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (den === 0) {
|
|
290
|
+
throw new ChecksumError('Reed-Solomon: singular error locator derivative');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let magnitude = field.div(num, den);
|
|
294
|
+
// X^(1-base): one factor of X when base is 0, none when base is 1.
|
|
295
|
+
if (base === 0) magnitude = field.mul(magnitude, field.exp(p));
|
|
296
|
+
else if (base !== 1) magnitude = field.mul(magnitude, field.exp(p * (1 - base)));
|
|
297
|
+
magnitude = field.neg(magnitude);
|
|
298
|
+
|
|
299
|
+
const idx = n - 1 - p;
|
|
300
|
+
received[idx] = field.sub(received[idx], magnitude);
|
|
301
|
+
corrected++;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Verify: a genuine correction zeroes every syndrome. Without this check,
|
|
305
|
+
// damage beyond capacity can produce a plausible-looking wrong answer.
|
|
306
|
+
for (let i = 0; i < eccLen; i++) {
|
|
307
|
+
if (evalPoly(received, field.exp(base + i), field) !== 0) {
|
|
308
|
+
throw new ChecksumError('Reed-Solomon: correction failed verification');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return corrected;
|
|
313
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Greyscale to black-and-white.
|
|
33
|
+
*
|
|
34
|
+
* This is the single biggest determinant of whether a reader works on real
|
|
35
|
+
* photographs. Decoding logic is exact and either right or wrong; binarization
|
|
36
|
+
* is a judgement call made a million times per image, and every downstream
|
|
37
|
+
* stage inherits its mistakes. A symbol lost here is lost permanently.
|
|
38
|
+
*
|
|
39
|
+
* Two strategies:
|
|
40
|
+
*
|
|
41
|
+
* global — one threshold for the whole image. Fast, and correct for clean
|
|
42
|
+
* synthetic images: screenshots, generated PNGs, flat scans.
|
|
43
|
+
* hybrid — a threshold per 8x8 block, smoothed across neighbours. Handles
|
|
44
|
+
* the uneven lighting that dominates camera input: shadows,
|
|
45
|
+
* glare, vignetting, a page curving away from the lens.
|
|
46
|
+
*
|
|
47
|
+
* @module image/binarizer
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
51
|
+
import { NotFoundError } from '../core/errors.js';
|
|
52
|
+
|
|
53
|
+
/** Side of a block, in pixels. */
|
|
54
|
+
const BLOCK = 8;
|
|
55
|
+
const BLOCK_SHIFT = 3;
|
|
56
|
+
|
|
57
|
+
/** Below this spread, a block is treated as uniform rather than as edges. */
|
|
58
|
+
const MIN_DYNAMIC_RANGE = 24;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One threshold for the entire image, chosen from the luminance histogram.
|
|
62
|
+
*
|
|
63
|
+
* Finds the two strongest peaks — ideally ink and paper — and cuts at the
|
|
64
|
+
* point of lowest population between them, weighted by distance so a narrow
|
|
65
|
+
* secondary peak does not drag the threshold onto a shoulder.
|
|
66
|
+
*
|
|
67
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
68
|
+
* @returns {BitMatrix} Set bit = dark module.
|
|
69
|
+
*/
|
|
70
|
+
export function binarizeGlobal(source) {
|
|
71
|
+
const { grey, width, height } = source;
|
|
72
|
+
const buckets = new Int32Array(32);
|
|
73
|
+
for (let i = 0; i < grey.length; i++) buckets[grey[i] >> 3]++;
|
|
74
|
+
|
|
75
|
+
const threshold = pickThreshold(buckets);
|
|
76
|
+
const out = new BitMatrix(width, height);
|
|
77
|
+
for (let y = 0; y < height; y++) {
|
|
78
|
+
const base = y * width;
|
|
79
|
+
for (let x = 0; x < width; x++) {
|
|
80
|
+
if (grey[base + x] < threshold) out.set(x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Int32Array} buckets
|
|
88
|
+
* @returns {number} Threshold in 0-255.
|
|
89
|
+
*/
|
|
90
|
+
function pickThreshold(buckets) {
|
|
91
|
+
const n = buckets.length;
|
|
92
|
+
|
|
93
|
+
let firstPeak = 0, firstPeakSize = 0;
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
if (buckets[i] > firstPeakSize) {
|
|
96
|
+
firstPeakSize = buckets[i];
|
|
97
|
+
firstPeak = i;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Score candidates by population scaled by squared distance from the first
|
|
102
|
+
// peak: the second peak must be both populous and clearly separated.
|
|
103
|
+
let secondPeak = 0, secondScore = 0;
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
const d = i - firstPeak;
|
|
106
|
+
const score = buckets[i] * d * d;
|
|
107
|
+
if (score > secondScore) {
|
|
108
|
+
secondScore = score;
|
|
109
|
+
secondPeak = i;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let low = Math.min(firstPeak, secondPeak);
|
|
114
|
+
let high = Math.max(firstPeak, secondPeak);
|
|
115
|
+
|
|
116
|
+
if (high - low <= n >> 4) {
|
|
117
|
+
// Effectively unimodal — a blank region, or an image with no ink. Fall
|
|
118
|
+
// back to the midpoint of the occupied range rather than inventing edges.
|
|
119
|
+
let lo = 0, hi = n - 1;
|
|
120
|
+
while (lo < n && buckets[lo] === 0) lo++;
|
|
121
|
+
while (hi > 0 && buckets[hi] === 0) hi--;
|
|
122
|
+
return (((lo + hi) >> 1) << 3) + 4;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Deepest valley between the peaks, biased toward the middle.
|
|
126
|
+
let valley = low, valleyScore = -1;
|
|
127
|
+
for (let i = low + 1; i < high; i++) {
|
|
128
|
+
const fromLow = i - low;
|
|
129
|
+
const fromHigh = high - i;
|
|
130
|
+
const score = fromLow * fromHigh * (firstPeakSize - buckets[i]);
|
|
131
|
+
if (score > valleyScore) {
|
|
132
|
+
valleyScore = score;
|
|
133
|
+
valley = i;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return (valley << 3) + 4;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Locally adaptive thresholding.
|
|
141
|
+
*
|
|
142
|
+
* Per 8x8 block: compute min, max and mean. A block with real contrast gets
|
|
143
|
+
* its own mean as the threshold. A block that is flat is ambiguous on its own
|
|
144
|
+
* — solid paper and solid ink look identical from the inside — so it inherits
|
|
145
|
+
* from its neighbourhood, which is what stops large quiet zones from being
|
|
146
|
+
* speckled into noise.
|
|
147
|
+
*
|
|
148
|
+
* Thresholds are then averaged over a 5x5 block window, so lighting gradients
|
|
149
|
+
* are followed smoothly instead of producing visible block seams that the
|
|
150
|
+
* detectors would read as edges.
|
|
151
|
+
*
|
|
152
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
153
|
+
* @returns {BitMatrix}
|
|
154
|
+
*/
|
|
155
|
+
export function binarizeHybrid(source) {
|
|
156
|
+
const { grey, width, height } = source;
|
|
157
|
+
|
|
158
|
+
// Too small to block up meaningfully; the global pass is strictly better.
|
|
159
|
+
if (width < BLOCK * 5 || height < BLOCK * 5) return binarizeGlobal(source);
|
|
160
|
+
|
|
161
|
+
const bw = (width + BLOCK - 1) >> BLOCK_SHIFT;
|
|
162
|
+
const bh = (height + BLOCK - 1) >> BLOCK_SHIFT;
|
|
163
|
+
const means = new Int32Array(bw * bh);
|
|
164
|
+
const ranges = new Int32Array(bw * bh);
|
|
165
|
+
|
|
166
|
+
for (let by = 0; by < bh; by++) {
|
|
167
|
+
const y0 = by << BLOCK_SHIFT;
|
|
168
|
+
const y1 = Math.min(y0 + BLOCK, height);
|
|
169
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
170
|
+
const x0 = bx << BLOCK_SHIFT;
|
|
171
|
+
const x1 = Math.min(x0 + BLOCK, width);
|
|
172
|
+
|
|
173
|
+
let sum = 0, min = 255, max = 0, count = 0;
|
|
174
|
+
for (let y = y0; y < y1; y++) {
|
|
175
|
+
const base = y * width;
|
|
176
|
+
for (let x = x0; x < x1; x++) {
|
|
177
|
+
const v = grey[base + x];
|
|
178
|
+
sum += v;
|
|
179
|
+
if (v < min) min = v;
|
|
180
|
+
if (v > max) max = v;
|
|
181
|
+
count++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const idx = by * bw + bx;
|
|
185
|
+
means[idx] = count ? (sum / count) | 0 : 128;
|
|
186
|
+
ranges[idx] = max - min;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Flat blocks adopt a threshold from context. Looking left and up is enough
|
|
191
|
+
// because those neighbours are already resolved, and it makes the pass
|
|
192
|
+
// single-shot rather than iterative.
|
|
193
|
+
for (let by = 0; by < bh; by++) {
|
|
194
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
195
|
+
const idx = by * bw + bx;
|
|
196
|
+
if (ranges[idx] >= MIN_DYNAMIC_RANGE) continue;
|
|
197
|
+
|
|
198
|
+
let inherited = means[idx];
|
|
199
|
+
if (bx > 0 && by > 0) {
|
|
200
|
+
const neighbours = [
|
|
201
|
+
means[idx - 1],
|
|
202
|
+
means[idx - bw],
|
|
203
|
+
means[idx - bw - 1],
|
|
204
|
+
];
|
|
205
|
+
const avg = (neighbours[0] + neighbours[1] + neighbours[2]) / 3;
|
|
206
|
+
// A flat block darker than its surroundings is ink; lighter is paper.
|
|
207
|
+
// Either way the local minimum is the safer threshold than the mean.
|
|
208
|
+
inherited = Math.min(avg, means[idx]);
|
|
209
|
+
}
|
|
210
|
+
means[idx] = inherited | 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const out = new BitMatrix(width, height);
|
|
215
|
+
const R = 2; // 5x5 block window
|
|
216
|
+
|
|
217
|
+
for (let by = 0; by < bh; by++) {
|
|
218
|
+
const y0 = by << BLOCK_SHIFT;
|
|
219
|
+
const y1 = Math.min(y0 + BLOCK, height);
|
|
220
|
+
const byLo = Math.max(0, by - R);
|
|
221
|
+
const byHi = Math.min(bh - 1, by + R);
|
|
222
|
+
|
|
223
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
224
|
+
const bxLo = Math.max(0, bx - R);
|
|
225
|
+
const bxHi = Math.min(bw - 1, bx + R);
|
|
226
|
+
|
|
227
|
+
let sum = 0, count = 0;
|
|
228
|
+
for (let ny = byLo; ny <= byHi; ny++) {
|
|
229
|
+
for (let nx = bxLo; nx <= bxHi; nx++) {
|
|
230
|
+
sum += means[ny * bw + nx];
|
|
231
|
+
count++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const threshold = sum / count;
|
|
235
|
+
|
|
236
|
+
const x0 = bx << BLOCK_SHIFT;
|
|
237
|
+
const x1 = Math.min(x0 + BLOCK, width);
|
|
238
|
+
for (let y = y0; y < y1; y++) {
|
|
239
|
+
const base = y * width;
|
|
240
|
+
for (let x = x0; x < x1; x++) {
|
|
241
|
+
if (grey[base + x] < threshold) out.set(x, y);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Binarize with the named strategy.
|
|
252
|
+
*
|
|
253
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
254
|
+
* @param {'global' | 'hybrid' | 'auto'} [strategy]
|
|
255
|
+
* @returns {BitMatrix}
|
|
256
|
+
*/
|
|
257
|
+
export function binarize(source, strategy = 'auto') {
|
|
258
|
+
if (!source || !source.grey) throw new NotFoundError('binarize: no luminance source');
|
|
259
|
+
switch (strategy) {
|
|
260
|
+
case 'global': return binarizeGlobal(source);
|
|
261
|
+
case 'hybrid': return binarizeHybrid(source);
|
|
262
|
+
case 'auto':
|
|
263
|
+
// Small images are almost always generated rather than photographed.
|
|
264
|
+
return source.width * source.height < 200 * 200
|
|
265
|
+
? binarizeGlobal(source)
|
|
266
|
+
: binarizeHybrid(source);
|
|
267
|
+
default:
|
|
268
|
+
throw new NotFoundError(`Unknown binarizer strategy: ${strategy}`);
|
|
269
|
+
}
|
|
270
|
+
}
|