@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,164 @@
|
|
|
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
|
+
* Resample a distorted symbol in the image into an upright module grid.
|
|
33
|
+
*
|
|
34
|
+
* Given a transform that maps grid coordinates to image coordinates, this
|
|
35
|
+
* samples the centre of every module. Sampling centres rather than averaging
|
|
36
|
+
* whole cells is deliberate: module edges are where blur and bleed live, and
|
|
37
|
+
* including them turns a marginal symbol into an unreadable one.
|
|
38
|
+
*
|
|
39
|
+
* @module image/grid-sampler
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
43
|
+
import { NotFoundError } from '../core/errors.js';
|
|
44
|
+
import { PerspectiveTransform } from './perspective.js';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sample a `dimension` x `dimension` grid (or `width` x `height`).
|
|
48
|
+
*
|
|
49
|
+
* @param {BitMatrix} image Binarized source image.
|
|
50
|
+
* @param {number} width Modules across.
|
|
51
|
+
* @param {number} height Modules down.
|
|
52
|
+
* @param {PerspectiveTransform} transform Grid space -> image space.
|
|
53
|
+
* @returns {BitMatrix}
|
|
54
|
+
* @throws {NotFoundError} If the grid falls outside the image.
|
|
55
|
+
*/
|
|
56
|
+
export function sampleGrid(image, width, height, transform) {
|
|
57
|
+
const out = new BitMatrix(width, height);
|
|
58
|
+
const points = new Float32Array(width * 2);
|
|
59
|
+
|
|
60
|
+
for (let y = 0; y < height; y++) {
|
|
61
|
+
// Module centres: offset by half a module in both axes.
|
|
62
|
+
const gridY = y + 0.5;
|
|
63
|
+
for (let x = 0; x < width; x++) {
|
|
64
|
+
points[x * 2] = x + 0.5;
|
|
65
|
+
points[x * 2 + 1] = gridY;
|
|
66
|
+
}
|
|
67
|
+
transform.transform(points);
|
|
68
|
+
|
|
69
|
+
for (let x = 0; x < width; x++) {
|
|
70
|
+
const px = points[x * 2] | 0;
|
|
71
|
+
const py = points[x * 2 + 1] | 0;
|
|
72
|
+
if (px < 0 || py < 0 || px >= image.width || py >= image.height) {
|
|
73
|
+
throw new NotFoundError(
|
|
74
|
+
`Sampling grid escapes the image at module (${x}, ${y})`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (image.get(px, py)) out.set(x, y);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sample with a 3x3 majority vote per module.
|
|
86
|
+
*
|
|
87
|
+
* Slower, and worth it when a single-point sample lands on a speck of noise or
|
|
88
|
+
* a JPEG artefact. Readers fall back to this after a clean sample fails to
|
|
89
|
+
* decode, rather than paying for it on every attempt.
|
|
90
|
+
*
|
|
91
|
+
* @param {BitMatrix} image
|
|
92
|
+
* @param {number} width
|
|
93
|
+
* @param {number} height
|
|
94
|
+
* @param {PerspectiveTransform} transform
|
|
95
|
+
* @returns {BitMatrix}
|
|
96
|
+
*/
|
|
97
|
+
export function sampleGridVoting(image, width, height, transform) {
|
|
98
|
+
const out = new BitMatrix(width, height);
|
|
99
|
+
|
|
100
|
+
// Spacing between module centres, measured in image pixels, so the vote
|
|
101
|
+
// spreads across the module rather than a fixed pixel radius that would be
|
|
102
|
+
// meaningless at a different scale.
|
|
103
|
+
const p0 = transform.transformPoint(0.5, 0.5);
|
|
104
|
+
const p1 = transform.transformPoint(1.5, 0.5);
|
|
105
|
+
const p2 = transform.transformPoint(0.5, 1.5);
|
|
106
|
+
const stepX = Math.hypot(p1.x - p0.x, p1.y - p0.y);
|
|
107
|
+
const stepY = Math.hypot(p2.x - p0.x, p2.y - p0.y);
|
|
108
|
+
const rx = Math.max(1, Math.round(stepX / 4));
|
|
109
|
+
const ry = Math.max(1, Math.round(stepY / 4));
|
|
110
|
+
|
|
111
|
+
for (let y = 0; y < height; y++) {
|
|
112
|
+
for (let x = 0; x < width; x++) {
|
|
113
|
+
const c = transform.transformPoint(x + 0.5, y + 0.5);
|
|
114
|
+
const cx = c.x | 0;
|
|
115
|
+
const cy = c.y | 0;
|
|
116
|
+
if (cx < 0 || cy < 0 || cx >= image.width || cy >= image.height) {
|
|
117
|
+
throw new NotFoundError(
|
|
118
|
+
`Sampling grid escapes the image at module (${x}, ${y})`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let dark = 0;
|
|
123
|
+
let total = 0;
|
|
124
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
125
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
126
|
+
const sx = cx + dx * rx;
|
|
127
|
+
const sy = cy + dy * ry;
|
|
128
|
+
if (sx < 0 || sy < 0 || sx >= image.width || sy >= image.height) continue;
|
|
129
|
+
total++;
|
|
130
|
+
if (image.get(sx, sy)) dark++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (total > 0 && dark * 2 > total) out.set(x, y);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the transform for a symbol whose four corners are known, and sample it.
|
|
142
|
+
*
|
|
143
|
+
* Corners are in reading order: top-left, top-right, bottom-right, bottom-left.
|
|
144
|
+
*
|
|
145
|
+
* @param {BitMatrix} image
|
|
146
|
+
* @param {number} dimension Modules per side.
|
|
147
|
+
* @param {Array<{x: number, y: number}>} corners
|
|
148
|
+
* @param {boolean} [voting]
|
|
149
|
+
* @returns {BitMatrix}
|
|
150
|
+
*/
|
|
151
|
+
export function sampleQuad(image, dimension, corners, voting = false) {
|
|
152
|
+
if (corners.length !== 4) throw new NotFoundError('sampleQuad needs exactly 4 corners');
|
|
153
|
+
const [tl, tr, br, bl] = corners;
|
|
154
|
+
const d = dimension;
|
|
155
|
+
|
|
156
|
+
const transform = PerspectiveTransform.quadToQuad(
|
|
157
|
+
0, 0, d, 0, d, d, 0, d,
|
|
158
|
+
tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return voting
|
|
162
|
+
? sampleGridVoting(image, d, d, transform)
|
|
163
|
+
: sampleGrid(image, d, d, transform);
|
|
164
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
* Image pipeline, re-exported.
|
|
33
|
+
*
|
|
34
|
+
* @module image
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export { LuminanceSource } from './luminance.js';
|
|
38
|
+
export { binarize, binarizeGlobal, binarizeHybrid } from './binarizer.js';
|
|
39
|
+
export { PerspectiveTransform } from './perspective.js';
|
|
40
|
+
export { sampleGrid, sampleGridVoting, sampleQuad } from './grid-sampler.js';
|
|
@@ -0,0 +1,196 @@
|
|
|
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 conversion — the boundary between "an image" and "our problem".
|
|
33
|
+
*
|
|
34
|
+
* The whole library accepts exactly one image shape: `{ data, width, height }`
|
|
35
|
+
* with `data` in RGBA order. That is what `ImageData` is, so a `<canvas>`, an
|
|
36
|
+
* `OffscreenCanvas`, `createImageBitmap`, sharp, jimp and node-canvas all
|
|
37
|
+
* satisfy it without an adapter. Nothing below this file knows about the DOM,
|
|
38
|
+
* the filesystem, or any image codec.
|
|
39
|
+
*
|
|
40
|
+
* @module image/luminance
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { NotFoundError } from '../core/errors.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} ImageLike
|
|
47
|
+
* @property {Uint8ClampedArray | Uint8Array | number[]} data RGBA, 4 bytes per pixel.
|
|
48
|
+
* @property {number} width
|
|
49
|
+
* @property {number} height
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
export class LuminanceSource {
|
|
53
|
+
/**
|
|
54
|
+
* @param {Uint8Array} grey One byte per pixel.
|
|
55
|
+
* @param {number} width
|
|
56
|
+
* @param {number} height
|
|
57
|
+
*/
|
|
58
|
+
constructor(grey, width, height) {
|
|
59
|
+
this.grey = grey;
|
|
60
|
+
this.width = width;
|
|
61
|
+
this.height = height;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build from any ImageData-shaped object.
|
|
66
|
+
*
|
|
67
|
+
* Transparent pixels are composited over white rather than read as black:
|
|
68
|
+
* a PNG barcode with a transparent background is common, and treating alpha
|
|
69
|
+
* as ink turns the entire quiet zone into a solid dark field.
|
|
70
|
+
*
|
|
71
|
+
* @param {ImageLike} image
|
|
72
|
+
* @returns {LuminanceSource}
|
|
73
|
+
*/
|
|
74
|
+
static fromImageData(image) {
|
|
75
|
+
const { data, width, height } = image;
|
|
76
|
+
if (!data || !width || !height) {
|
|
77
|
+
throw new NotFoundError('Image must be { data, width, height } with RGBA data');
|
|
78
|
+
}
|
|
79
|
+
if (data.length < width * height * 4) {
|
|
80
|
+
throw new NotFoundError(
|
|
81
|
+
`Image data too short: ${data.length} bytes for ${width}x${height} RGBA ` +
|
|
82
|
+
`(expected ${width * height * 4})`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const grey = new Uint8Array(width * height);
|
|
87
|
+
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
88
|
+
const a = data[p + 3];
|
|
89
|
+
let r = data[p], g = data[p + 1], b = data[p + 2];
|
|
90
|
+
if (a !== 255) {
|
|
91
|
+
// Composite over white.
|
|
92
|
+
const inv = 255 - a;
|
|
93
|
+
r = (r * a + 255 * inv) / 255;
|
|
94
|
+
g = (g * a + 255 * inv) / 255;
|
|
95
|
+
b = (b * a + 255 * inv) / 255;
|
|
96
|
+
}
|
|
97
|
+
// Integer luma approximation of Rec. 601: 0.299/0.587/0.114 scaled by 256.
|
|
98
|
+
grey[i] = (r * 77 + g * 150 + b * 29) >> 8;
|
|
99
|
+
}
|
|
100
|
+
return new LuminanceSource(grey, width, height);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build directly from single-channel data, skipping conversion.
|
|
105
|
+
*
|
|
106
|
+
* @param {Uint8Array} grey
|
|
107
|
+
* @param {number} width
|
|
108
|
+
* @param {number} height
|
|
109
|
+
* @returns {LuminanceSource}
|
|
110
|
+
*/
|
|
111
|
+
static fromGrey(grey, width, height) {
|
|
112
|
+
if (grey.length < width * height) {
|
|
113
|
+
throw new NotFoundError('Greyscale buffer shorter than width * height');
|
|
114
|
+
}
|
|
115
|
+
return new LuminanceSource(grey, width, height);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {number} x @param {number} y
|
|
120
|
+
* @returns {number} 0-255.
|
|
121
|
+
*/
|
|
122
|
+
get(x, y) {
|
|
123
|
+
return this.grey[y * this.width + x];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {number} y
|
|
128
|
+
* @param {Uint8Array} [out]
|
|
129
|
+
* @returns {Uint8Array}
|
|
130
|
+
*/
|
|
131
|
+
getRow(y, out) {
|
|
132
|
+
const row = out && out.length >= this.width ? out : new Uint8Array(this.width);
|
|
133
|
+
row.set(this.grey.subarray(y * this.width, (y + 1) * this.width));
|
|
134
|
+
return row;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Rotate 90 degrees clockwise.
|
|
139
|
+
*
|
|
140
|
+
* The 1D readers scan horizontally, so this is how they find vertically
|
|
141
|
+
* oriented barcodes: scan, rotate, scan again.
|
|
142
|
+
*
|
|
143
|
+
* @returns {LuminanceSource}
|
|
144
|
+
*/
|
|
145
|
+
rotate90() {
|
|
146
|
+
const { width: w, height: h, grey } = this;
|
|
147
|
+
const out = new Uint8Array(grey.length);
|
|
148
|
+
for (let y = 0; y < h; y++) {
|
|
149
|
+
for (let x = 0; x < w; x++) {
|
|
150
|
+
out[x * h + (h - 1 - y)] = grey[y * w + x];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return new LuminanceSource(out, h, w);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Downscale by an integer factor with box averaging.
|
|
158
|
+
*
|
|
159
|
+
* Large camera frames are slow to scan and no more informative than a
|
|
160
|
+
* half-size copy; detectors use this to find candidates cheaply.
|
|
161
|
+
*
|
|
162
|
+
* @param {number} factor
|
|
163
|
+
* @returns {LuminanceSource}
|
|
164
|
+
*/
|
|
165
|
+
downscale(factor) {
|
|
166
|
+
const f = Math.max(1, Math.floor(factor));
|
|
167
|
+
if (f === 1) return this;
|
|
168
|
+
const w = Math.floor(this.width / f);
|
|
169
|
+
const h = Math.floor(this.height / f);
|
|
170
|
+
const out = new Uint8Array(w * h);
|
|
171
|
+
const area = f * f;
|
|
172
|
+
for (let y = 0; y < h; y++) {
|
|
173
|
+
for (let x = 0; x < w; x++) {
|
|
174
|
+
let sum = 0;
|
|
175
|
+
for (let dy = 0; dy < f; dy++) {
|
|
176
|
+
const base = (y * f + dy) * this.width + x * f;
|
|
177
|
+
for (let dx = 0; dx < f; dx++) sum += this.grey[base + dx];
|
|
178
|
+
}
|
|
179
|
+
out[y * w + x] = (sum / area) | 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return new LuminanceSource(out, w, h);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Invert. Some symbols are printed light-on-dark, and readers retry inverted
|
|
187
|
+
* when a first pass finds nothing.
|
|
188
|
+
*
|
|
189
|
+
* @returns {LuminanceSource}
|
|
190
|
+
*/
|
|
191
|
+
invert() {
|
|
192
|
+
const out = new Uint8Array(this.grey.length);
|
|
193
|
+
for (let i = 0; i < out.length; i++) out[i] = 255 - this.grey[i];
|
|
194
|
+
return new LuminanceSource(out, this.width, this.height);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
* Projective (perspective) transforms.
|
|
33
|
+
*
|
|
34
|
+
* A 2D symbol photographed off-axis is not a rotated square — it is a
|
|
35
|
+
* quadrilateral with converging edges. Correcting that needs a full projective
|
|
36
|
+
* map, not an affine one; an affine approximation reads the near edge of a
|
|
37
|
+
* tilted symbol correctly and drifts a module or more by the far edge.
|
|
38
|
+
*
|
|
39
|
+
* The map is a 3x3 homogeneous matrix. Points are transformed as
|
|
40
|
+
* (x, y, 1) * M, then divided through by the resulting w.
|
|
41
|
+
*
|
|
42
|
+
* @module image/perspective
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export class PerspectiveTransform {
|
|
46
|
+
/* eslint-disable-next-line max-params */
|
|
47
|
+
constructor(a11, a21, a31, a12, a22, a32, a13, a23, a33) {
|
|
48
|
+
this.a11 = a11; this.a21 = a21; this.a31 = a31;
|
|
49
|
+
this.a12 = a12; this.a22 = a22; this.a32 = a32;
|
|
50
|
+
this.a13 = a13; this.a23 = a23; this.a33 = a33;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Transform points in place.
|
|
55
|
+
*
|
|
56
|
+
* @param {Float32Array | number[]} points Interleaved [x0, y0, x1, y1, ...].
|
|
57
|
+
* @returns {Float32Array | number[]} The same array.
|
|
58
|
+
*/
|
|
59
|
+
transform(points) {
|
|
60
|
+
const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
|
|
61
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
62
|
+
const x = points[i];
|
|
63
|
+
const y = points[i + 1];
|
|
64
|
+
const w = a13 * x + a23 * y + a33;
|
|
65
|
+
points[i] = (a11 * x + a21 * y + a31) / w;
|
|
66
|
+
points[i + 1] = (a12 * x + a22 * y + a32) / w;
|
|
67
|
+
}
|
|
68
|
+
return points;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Transform a single point.
|
|
73
|
+
*
|
|
74
|
+
* @param {number} x @param {number} y
|
|
75
|
+
* @returns {{x: number, y: number}}
|
|
76
|
+
*/
|
|
77
|
+
transformPoint(x, y) {
|
|
78
|
+
const w = this.a13 * x + this.a23 * y + this.a33;
|
|
79
|
+
return {
|
|
80
|
+
x: (this.a11 * x + this.a21 * y + this.a31) / w,
|
|
81
|
+
y: (this.a12 * x + this.a22 * y + this.a32) / w,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Map the unit square — (0,0), (1,0), (1,1), (0,1) — onto an arbitrary quad.
|
|
87
|
+
*
|
|
88
|
+
* Corners are given in that same order, i.e. going around the quad, not
|
|
89
|
+
* as opposite pairs.
|
|
90
|
+
*
|
|
91
|
+
* @returns {PerspectiveTransform}
|
|
92
|
+
*/
|
|
93
|
+
/* eslint-disable-next-line max-params */
|
|
94
|
+
static squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3) {
|
|
95
|
+
const dx3 = x0 - x1 + x2 - x3;
|
|
96
|
+
const dy3 = y0 - y1 + y2 - y3;
|
|
97
|
+
|
|
98
|
+
if (dx3 === 0 && dy3 === 0) {
|
|
99
|
+
// The quad is a parallelogram, so the map is affine and the projective
|
|
100
|
+
// terms vanish. Worth special-casing: it is the common case for flat
|
|
101
|
+
// scans, and the general solution divides by zero here.
|
|
102
|
+
return new PerspectiveTransform(
|
|
103
|
+
x1 - x0, x2 - x1, x0,
|
|
104
|
+
y1 - y0, y2 - y1, y0,
|
|
105
|
+
0, 0, 1
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dx1 = x1 - x2;
|
|
110
|
+
const dx2 = x3 - x2;
|
|
111
|
+
const dy1 = y1 - y2;
|
|
112
|
+
const dy2 = y3 - y2;
|
|
113
|
+
const denominator = dx1 * dy2 - dx2 * dy1;
|
|
114
|
+
const a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
|
|
115
|
+
const a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
|
|
116
|
+
|
|
117
|
+
return new PerspectiveTransform(
|
|
118
|
+
x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0,
|
|
119
|
+
y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0,
|
|
120
|
+
a13, a23, 1
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Map an arbitrary quad onto the unit square — the inverse of
|
|
126
|
+
* {@link squareToQuad}, via the adjugate.
|
|
127
|
+
*
|
|
128
|
+
* @returns {PerspectiveTransform}
|
|
129
|
+
*/
|
|
130
|
+
/* eslint-disable-next-line max-params */
|
|
131
|
+
static quadToSquare(x0, y0, x1, y1, x2, y2, x3, y3) {
|
|
132
|
+
return PerspectiveTransform.squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3).inverse();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Map one quad onto another, corner for corner.
|
|
137
|
+
*
|
|
138
|
+
* This is what turns four detected finder corners into a sampling grid:
|
|
139
|
+
* compose "detected quad -> unit square" with "unit square -> ideal grid".
|
|
140
|
+
*
|
|
141
|
+
* @returns {PerspectiveTransform}
|
|
142
|
+
*/
|
|
143
|
+
/* eslint-disable-next-line max-params */
|
|
144
|
+
static quadToQuad(
|
|
145
|
+
sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3,
|
|
146
|
+
dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3
|
|
147
|
+
) {
|
|
148
|
+
const toSquare = PerspectiveTransform.quadToSquare(sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
|
|
149
|
+
const toQuad = PerspectiveTransform.squareToQuad(dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3);
|
|
150
|
+
return toSquare.times(toQuad);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Adjugate — the inverse up to a scale factor, which is irrelevant in
|
|
155
|
+
* homogeneous coordinates because the division by w cancels it.
|
|
156
|
+
*
|
|
157
|
+
* @returns {PerspectiveTransform}
|
|
158
|
+
*/
|
|
159
|
+
inverse() {
|
|
160
|
+
const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
|
|
161
|
+
return new PerspectiveTransform(
|
|
162
|
+
a22 * a33 - a23 * a32,
|
|
163
|
+
a23 * a31 - a21 * a33,
|
|
164
|
+
a21 * a32 - a22 * a31,
|
|
165
|
+
a13 * a32 - a12 * a33,
|
|
166
|
+
a11 * a33 - a13 * a31,
|
|
167
|
+
a12 * a31 - a11 * a32,
|
|
168
|
+
a12 * a23 - a13 * a22,
|
|
169
|
+
a13 * a21 - a11 * a23,
|
|
170
|
+
a11 * a22 - a12 * a21
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Matrix product: apply `this` first, then `other`.
|
|
176
|
+
*
|
|
177
|
+
* @param {PerspectiveTransform} other
|
|
178
|
+
* @returns {PerspectiveTransform}
|
|
179
|
+
*/
|
|
180
|
+
times(other) {
|
|
181
|
+
const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
|
|
182
|
+
const o = other;
|
|
183
|
+
return new PerspectiveTransform(
|
|
184
|
+
o.a11 * a11 + o.a21 * a12 + o.a31 * a13,
|
|
185
|
+
o.a11 * a21 + o.a21 * a22 + o.a31 * a23,
|
|
186
|
+
o.a11 * a31 + o.a21 * a32 + o.a31 * a33,
|
|
187
|
+
o.a12 * a11 + o.a22 * a12 + o.a32 * a13,
|
|
188
|
+
o.a12 * a21 + o.a22 * a22 + o.a32 * a23,
|
|
189
|
+
o.a12 * a31 + o.a22 * a32 + o.a32 * a33,
|
|
190
|
+
o.a13 * a11 + o.a23 * a12 + o.a33 * a13,
|
|
191
|
+
o.a13 * a21 + o.a23 * a22 + o.a33 * a23,
|
|
192
|
+
o.a13 * a31 + o.a23 * a32 + o.a33 * a33
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|