@sythos/js_barcode_universal 1.5.4 → 1.5.5
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/README.md +20 -2
- package/bundle/sythos-barcode.esm.js +137 -10
- package/bundle/sythos-barcode.js +137 -10
- package/package.json +1 -1
- package/src/index.js +51 -5
- package/src/oned/reader.js +85 -4
package/README.md
CHANGED
|
@@ -118,8 +118,8 @@ The `unpkg` and `jsdelivr` fields point at the IIFE bundle, so a CDN needs no in
|
|
|
118
118
|
|
|
119
119
|
```html
|
|
120
120
|
<script src="https://unpkg.com/@sythos/js_barcode_universal"></script>
|
|
121
|
-
<script src="https://unpkg.com/@sythos/js_barcode_universal@1.5.
|
|
122
|
-
<script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.
|
|
121
|
+
<script src="https://unpkg.com/@sythos/js_barcode_universal@1.5.5"></script>
|
|
122
|
+
<script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.5"></script>
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
Pin the version for anything you ship; the unpinned form resolves to `latest` and will move under
|
|
@@ -196,6 +196,24 @@ for (const hit of decode(ctx.getImageData(0, 0, canvas.width, canvas.height))) {
|
|
|
196
196
|
outcome for a camera loop, not an error, so the common case needs no `try`/`catch`. Use
|
|
197
197
|
`decodeStrict` when absence really is a failure.
|
|
198
198
|
|
|
199
|
+
### Strict camera profile
|
|
200
|
+
|
|
201
|
+
For a live camera loop, opt into the stricter 1D policy:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
decode(frame, { formats, profile: 'camera', tryHarder: true })
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The profile requires a compatible quiet zone and the same complete 1D symbol on at least two
|
|
208
|
+
scan samples. It retries only the two quarter-turn orientations needed for 1D symbols when the
|
|
209
|
+
native orientation has no validated read. Code 11 and MSI require a verified check digit in this
|
|
210
|
+
profile; other formats retain their own structural and checksum validation. A frame without a
|
|
211
|
+
validated barcode still returns `[]`.
|
|
212
|
+
|
|
213
|
+
Camera-profile 1D results add `confidence` (0–1), `bounds`, `rotation`, and
|
|
214
|
+
`quality: { quietZone, checksum, rows, consistency }`. `bounds` is reported in the raster
|
|
215
|
+
orientation that was scanned; unavailable quality data is represented by `null` where applicable.
|
|
216
|
+
|
|
199
217
|
---
|
|
200
218
|
|
|
201
219
|
## Supported formats
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Sythos Barcode Suite v1.5.
|
|
2
|
+
* Sythos Barcode Suite v1.5.5
|
|
3
3
|
*
|
|
4
4
|
* MIT License
|
|
5
5
|
*
|
|
@@ -3935,6 +3935,51 @@ function withoutEANAddon(result) {
|
|
|
3935
3935
|
return parent;
|
|
3936
3936
|
}
|
|
3937
3937
|
|
|
3938
|
+
/** @param {Uint8Array} row @returns {{x:number, width:number, quietZone:boolean}|null} */
|
|
3939
|
+
function cameraRowGeometry(row) {
|
|
3940
|
+
let first = 0;
|
|
3941
|
+
while (first < row.length && row[first] === 0) first++;
|
|
3942
|
+
if (first === row.length) return null;
|
|
3943
|
+
let last = row.length - 1;
|
|
3944
|
+
while (last >= 0 && row[last] === 0) last--;
|
|
3945
|
+
return {
|
|
3946
|
+
x: first,
|
|
3947
|
+
width: last - first + 1,
|
|
3948
|
+
quietZone: first >= 2 && row.length - 1 - last >= 2,
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
/** @param {string} format @param {object} options @returns {boolean|null} */
|
|
3953
|
+
function checksumStatus(format, options) {
|
|
3954
|
+
if (format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce' ||
|
|
3955
|
+
format === 'code93' || format === 'code128' || format === 'gs1128' ||
|
|
3956
|
+
format === 'gs1databar14') return true;
|
|
3957
|
+
if (format === 'code11' || format === 'msi' || format === 'code39') {
|
|
3958
|
+
return options.profile === 'camera' || options.checkDigit === true ? true : null;
|
|
3959
|
+
}
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
/** @param {object} result @param {object} geometry @param {Set<number>} rows @param {object} options @returns {object} */
|
|
3964
|
+
function cameraMetadata(result, geometry, rows, options) {
|
|
3965
|
+
const checksum = checksumStatus(result.format, options);
|
|
3966
|
+
const consistency = Math.min(1, rows.size / 3);
|
|
3967
|
+
const confidence = Math.min(1, 0.4 + (geometry.quietZone ? 0.2 : 0) +
|
|
3968
|
+
(checksum === true ? 0.2 : 0) + consistency * 0.2);
|
|
3969
|
+
return {
|
|
3970
|
+
...result,
|
|
3971
|
+
confidence,
|
|
3972
|
+
bounds: {
|
|
3973
|
+
x: geometry.x,
|
|
3974
|
+
y: Math.min(...rows),
|
|
3975
|
+
width: geometry.width,
|
|
3976
|
+
height: Math.max(...rows) - Math.min(...rows) + 1,
|
|
3977
|
+
},
|
|
3978
|
+
rotation: options.cameraRotation ?? 0,
|
|
3979
|
+
quality: { quietZone: geometry.quietZone, checksum, rows: rows.size, consistency },
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3938
3983
|
/**
|
|
3939
3984
|
* Read every linear symbol found in a binarized image.
|
|
3940
3985
|
*
|
|
@@ -3943,10 +3988,13 @@ function withoutEANAddon(result) {
|
|
|
3943
3988
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
3944
3989
|
* @param {number} [options.rows] How many horizontal slices to try.
|
|
3945
3990
|
* @param {boolean} [options.tryHarder] Also scan reversed rows, for mirrored symbols.
|
|
3991
|
+
* @param {'camera'} [options.profile] Require stable, quiet-zone-qualified reads.
|
|
3992
|
+
* @param {0|90|180|270} [options.cameraRotation] Orientation already normalized by the caller.
|
|
3946
3993
|
* @returns {Array<{format: string, text: string, row: number}>}
|
|
3947
3994
|
*/
|
|
3948
3995
|
function decodeOneD(image, options = {}) {
|
|
3949
|
-
const { formats = null, rows = 15, tryHarder = true } = options;
|
|
3996
|
+
const { formats = null, rows = 15, tryHarder = true, profile = null } = options;
|
|
3997
|
+
const cameraProfile = profile === 'camera';
|
|
3950
3998
|
const enabled = formats ? new Set(formats) : null;
|
|
3951
3999
|
const active = DECODERS.filter(([id]) => {
|
|
3952
4000
|
if (!enabled) return true;
|
|
@@ -3965,13 +4013,15 @@ function decodeOneD(image, options = {}) {
|
|
|
3965
4013
|
const seen = new Set();
|
|
3966
4014
|
const height = image.height;
|
|
3967
4015
|
const buffer = new Uint8Array(image.width);
|
|
4016
|
+
const cameraCandidates = new Map();
|
|
3968
4017
|
|
|
3969
4018
|
// Sample rows from the middle outward: symbols are usually centred, and the
|
|
3970
4019
|
// middle of a linear barcode is the part least likely to be clipped.
|
|
3971
4020
|
const middle = height >> 1;
|
|
3972
|
-
const
|
|
4021
|
+
const sampleRows = cameraProfile ? Math.min(height, Math.max(rows, 48)) : rows;
|
|
4022
|
+
const step = Math.max(1, Math.round(height / sampleRows));
|
|
3973
4023
|
|
|
3974
|
-
for (let attempt = 0; attempt <
|
|
4024
|
+
for (let attempt = 0; attempt < sampleRows; attempt++) {
|
|
3975
4025
|
const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
|
|
3976
4026
|
const y = middle + delta;
|
|
3977
4027
|
if (y < 0 || y >= height) continue;
|
|
@@ -3984,7 +4034,12 @@ function decodeOneD(image, options = {}) {
|
|
|
3984
4034
|
for (const [id, decoder] of active) {
|
|
3985
4035
|
let result = null;
|
|
3986
4036
|
try {
|
|
3987
|
-
|
|
4037
|
+
// Code 11 and MSI checks are optional in their base standards, but
|
|
4038
|
+
// a camera frame cannot safely promote their short unchecked forms.
|
|
4039
|
+
const decoderOptions = cameraProfile && (id === 'code11' || id === 'msi')
|
|
4040
|
+
? { ...options, checkDigit: true }
|
|
4041
|
+
: options;
|
|
4042
|
+
result = decoder(scan, decoderOptions);
|
|
3988
4043
|
} catch {
|
|
3989
4044
|
result = null; // a malformed candidate is not an error
|
|
3990
4045
|
}
|
|
@@ -4016,6 +4071,20 @@ function decodeOneD(image, options = {}) {
|
|
|
4016
4071
|
|
|
4017
4072
|
const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
|
|
4018
4073
|
const key = `${result.format}:${result.text}${addonKey}`;
|
|
4074
|
+
if (cameraProfile) {
|
|
4075
|
+
const geometry = cameraRowGeometry(row);
|
|
4076
|
+
// Do not promote partial row fragments from a camera frame.
|
|
4077
|
+
if (!geometry || !geometry.quietZone) continue;
|
|
4078
|
+
const candidate = cameraCandidates.get(key) ?? {
|
|
4079
|
+
result,
|
|
4080
|
+
geometry,
|
|
4081
|
+
rows: new Set(),
|
|
4082
|
+
rotation: ((options.cameraRotation ?? 0) + (pass ? 180 : 0)) % 360,
|
|
4083
|
+
};
|
|
4084
|
+
candidate.rows.add(y);
|
|
4085
|
+
cameraCandidates.set(key, candidate);
|
|
4086
|
+
continue;
|
|
4087
|
+
}
|
|
4019
4088
|
if (seen.has(key)) continue;
|
|
4020
4089
|
seen.add(key);
|
|
4021
4090
|
results.push({ ...result, row: y });
|
|
@@ -4024,6 +4093,18 @@ function decodeOneD(image, options = {}) {
|
|
|
4024
4093
|
}
|
|
4025
4094
|
}
|
|
4026
4095
|
|
|
4096
|
+
if (cameraProfile) {
|
|
4097
|
+
for (const candidate of cameraCandidates.values()) {
|
|
4098
|
+
// A complete symbol must survive at least two nearby scan samples. This
|
|
4099
|
+
// rejects isolated run coincidences without imposing a payload length.
|
|
4100
|
+
if (candidate.rows.size < 2) continue;
|
|
4101
|
+
results.push(cameraMetadata(candidate.result, candidate.geometry, candidate.rows, {
|
|
4102
|
+
...options,
|
|
4103
|
+
cameraRotation: candidate.rotation,
|
|
4104
|
+
}));
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4027
4108
|
// A valid EAN/UPC parent is substantially more constrained than a generic
|
|
4028
4109
|
// narrow/wide candidate. Suppress competing interpretations of the same
|
|
4029
4110
|
// scanline, while retaining symbols detected on other rows.
|
|
@@ -16620,6 +16701,10 @@ function encode(text, options = {}) {
|
|
|
16620
16701
|
* @property {boolean} [certified] Whether the profile is certified by its originator.
|
|
16621
16702
|
* @property {object} [canvas] Canvas reservation metadata for the Sythos profile.
|
|
16622
16703
|
* @property {{format:'ean2'|'ean5', text:string, parity:string, checksum?:number}} [addon] Attached EAN/UPC supplement.
|
|
16704
|
+
* @property {number} [confidence] Camera-profile confidence from 0 to 1.
|
|
16705
|
+
* @property {{x:number,y:number,width:number,height:number}} [bounds] Camera-profile bounds in the scanned orientation.
|
|
16706
|
+
* @property {0|90|180|270} [rotation] Camera-profile orientation in degrees.
|
|
16707
|
+
* @property {{quietZone:boolean,checksum:boolean|null,rows:number|null,consistency:number|null}} [quality] Camera-profile validation evidence.
|
|
16623
16708
|
* @property {boolean} [gs1] Whether the physical symbol is classified as GS1.
|
|
16624
16709
|
* @property {string} [symbologyIdentifier] GS1 symbology identifier.
|
|
16625
16710
|
* @property {Array<{ai:string,value:string,fixed?:boolean}>} [elements] Parsed GS1 Application Identifier fields.
|
|
@@ -16640,12 +16725,13 @@ function encode(text, options = {}) {
|
|
|
16640
16725
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
16641
16726
|
* @param {boolean} [options.tryHarder] Retry inverted and rotated. Default true.
|
|
16642
16727
|
* @param {'global'|'hybrid'|'auto'} [options.binarizer]
|
|
16728
|
+
* @param {'camera'} [options.profile] Opt-in strict camera profile for validated 1D reads.
|
|
16643
16729
|
* @param {object} [options.frameqr] Sythos Canvas QR detector options when
|
|
16644
16730
|
* the profile marker is not preserved through image rendering.
|
|
16645
16731
|
* @returns {DecodeResult[]}
|
|
16646
16732
|
*/
|
|
16647
16733
|
function decode(image, options = {}) {
|
|
16648
|
-
const { formats = null, tryHarder = true, binarizer = 'auto' } = options;
|
|
16734
|
+
const { formats = null, tryHarder = true, binarizer = 'auto', profile = null } = options;
|
|
16649
16735
|
const want = formats ? new Set(formats.map((f) => f.toLowerCase())) : null;
|
|
16650
16736
|
const wantQR = !want || want.has('qr') || want.has('qrcode');
|
|
16651
16737
|
const wantDataMatrix = !want || want.has('datamatrix') || want.has('data-matrix');
|
|
@@ -16785,7 +16871,24 @@ function decode(image, options = {}) {
|
|
|
16785
16871
|
const oneDFormats = want
|
|
16786
16872
|
? [...want].filter((f) => f in ONED_FORMATS || oneDAliases.has(f))
|
|
16787
16873
|
: null;
|
|
16788
|
-
|
|
16874
|
+
const oneDPasses = [{ bits, rotation: 0 }];
|
|
16875
|
+
// A linear symbol rotated by 90° has no usable horizontal scanline.
|
|
16876
|
+
// The strict camera profile adds exactly two normalized orientations,
|
|
16877
|
+
// only when the native orientation found no validated 1D result.
|
|
16878
|
+
const readOneD = (candidateBits, rotation) => decodeOneD(candidateBits, {
|
|
16879
|
+
...options, formats: oneDFormats, tryHarder, profile, cameraRotation: rotation,
|
|
16880
|
+
});
|
|
16881
|
+
let oneDResults = readOneD(bits, 0);
|
|
16882
|
+
if (profile === 'camera' && oneDResults.length === 0) {
|
|
16883
|
+
oneDPasses.push(
|
|
16884
|
+
{ bits: rotateBitMatrix90(bits, false), rotation: 90 },
|
|
16885
|
+
{ bits: rotateBitMatrix90(bits, true), rotation: 270 },
|
|
16886
|
+
);
|
|
16887
|
+
for (let i = 1; i < oneDPasses.length && oneDResults.length === 0; i++) {
|
|
16888
|
+
oneDResults = readOneD(oneDPasses[i].bits, oneDPasses[i].rotation);
|
|
16889
|
+
}
|
|
16890
|
+
}
|
|
16891
|
+
for (const found of oneDResults) {
|
|
16789
16892
|
const { row, ...publicFound } = found;
|
|
16790
16893
|
void row;
|
|
16791
16894
|
if (publicFound.gs1) {
|
|
@@ -16830,7 +16933,11 @@ function decode(image, options = {}) {
|
|
|
16830
16933
|
&& (binarizer === 'auto' || binarizer === 'hybrid')
|
|
16831
16934
|
&& retryFormats.length > 0;
|
|
16832
16935
|
|
|
16833
|
-
if (!shouldRetryGlobal)
|
|
16936
|
+
if (!shouldRetryGlobal) {
|
|
16937
|
+
return profile === 'camera'
|
|
16938
|
+
? unique.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
16939
|
+
: unique;
|
|
16940
|
+
}
|
|
16834
16941
|
|
|
16835
16942
|
const fallback = decode(image, {
|
|
16836
16943
|
...options,
|
|
@@ -16838,12 +16945,32 @@ function decode(image, options = {}) {
|
|
|
16838
16945
|
binarizer: 'global',
|
|
16839
16946
|
});
|
|
16840
16947
|
const fallbackSeen = new Set();
|
|
16841
|
-
|
|
16948
|
+
const merged = [...unique, ...fallback].filter((r) => {
|
|
16842
16949
|
const key = `${r.format}:${r.text}`;
|
|
16843
16950
|
if (fallbackSeen.has(key)) return false;
|
|
16844
16951
|
fallbackSeen.add(key);
|
|
16845
16952
|
return true;
|
|
16846
16953
|
});
|
|
16954
|
+
return profile === 'camera'
|
|
16955
|
+
? merged.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
16956
|
+
: merged;
|
|
16957
|
+
}
|
|
16958
|
+
|
|
16959
|
+
/**
|
|
16960
|
+
* @param {BitMatrix} matrix
|
|
16961
|
+
* @param {boolean} clockwise
|
|
16962
|
+
* @returns {BitMatrix}
|
|
16963
|
+
*/
|
|
16964
|
+
function rotateBitMatrix90(matrix, clockwise) {
|
|
16965
|
+
const rotated = new BitMatrix(matrix.height, matrix.width);
|
|
16966
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
16967
|
+
for (let x = 0; x < matrix.width; x++) {
|
|
16968
|
+
if (!matrix.get(x, y)) continue;
|
|
16969
|
+
if (clockwise) rotated.set(matrix.height - 1 - y, x);
|
|
16970
|
+
else rotated.set(y, matrix.width - 1 - x);
|
|
16971
|
+
}
|
|
16972
|
+
}
|
|
16973
|
+
return rotated;
|
|
16847
16974
|
}
|
|
16848
16975
|
|
|
16849
16976
|
/**
|
|
@@ -16860,7 +16987,7 @@ function decodeStrict(image, options) {
|
|
|
16860
16987
|
}
|
|
16861
16988
|
|
|
16862
16989
|
/** Library version, matching package.json. */
|
|
16863
|
-
const VERSION = '1.5.
|
|
16990
|
+
const VERSION = '1.5.5';
|
|
16864
16991
|
|
|
16865
16992
|
__exports.listFormats = listFormats;
|
|
16866
16993
|
__exports.encode = encode;
|
package/bundle/sythos-barcode.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Sythos Barcode Suite v1.5.
|
|
2
|
+
* Sythos Barcode Suite v1.5.5
|
|
3
3
|
*
|
|
4
4
|
* MIT License
|
|
5
5
|
*
|
|
@@ -3936,6 +3936,51 @@ function withoutEANAddon(result) {
|
|
|
3936
3936
|
return parent;
|
|
3937
3937
|
}
|
|
3938
3938
|
|
|
3939
|
+
/** @param {Uint8Array} row @returns {{x:number, width:number, quietZone:boolean}|null} */
|
|
3940
|
+
function cameraRowGeometry(row) {
|
|
3941
|
+
let first = 0;
|
|
3942
|
+
while (first < row.length && row[first] === 0) first++;
|
|
3943
|
+
if (first === row.length) return null;
|
|
3944
|
+
let last = row.length - 1;
|
|
3945
|
+
while (last >= 0 && row[last] === 0) last--;
|
|
3946
|
+
return {
|
|
3947
|
+
x: first,
|
|
3948
|
+
width: last - first + 1,
|
|
3949
|
+
quietZone: first >= 2 && row.length - 1 - last >= 2,
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
/** @param {string} format @param {object} options @returns {boolean|null} */
|
|
3954
|
+
function checksumStatus(format, options) {
|
|
3955
|
+
if (format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce' ||
|
|
3956
|
+
format === 'code93' || format === 'code128' || format === 'gs1128' ||
|
|
3957
|
+
format === 'gs1databar14') return true;
|
|
3958
|
+
if (format === 'code11' || format === 'msi' || format === 'code39') {
|
|
3959
|
+
return options.profile === 'camera' || options.checkDigit === true ? true : null;
|
|
3960
|
+
}
|
|
3961
|
+
return null;
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
/** @param {object} result @param {object} geometry @param {Set<number>} rows @param {object} options @returns {object} */
|
|
3965
|
+
function cameraMetadata(result, geometry, rows, options) {
|
|
3966
|
+
const checksum = checksumStatus(result.format, options);
|
|
3967
|
+
const consistency = Math.min(1, rows.size / 3);
|
|
3968
|
+
const confidence = Math.min(1, 0.4 + (geometry.quietZone ? 0.2 : 0) +
|
|
3969
|
+
(checksum === true ? 0.2 : 0) + consistency * 0.2);
|
|
3970
|
+
return {
|
|
3971
|
+
...result,
|
|
3972
|
+
confidence,
|
|
3973
|
+
bounds: {
|
|
3974
|
+
x: geometry.x,
|
|
3975
|
+
y: Math.min(...rows),
|
|
3976
|
+
width: geometry.width,
|
|
3977
|
+
height: Math.max(...rows) - Math.min(...rows) + 1,
|
|
3978
|
+
},
|
|
3979
|
+
rotation: options.cameraRotation ?? 0,
|
|
3980
|
+
quality: { quietZone: geometry.quietZone, checksum, rows: rows.size, consistency },
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3939
3984
|
/**
|
|
3940
3985
|
* Read every linear symbol found in a binarized image.
|
|
3941
3986
|
*
|
|
@@ -3944,10 +3989,13 @@ function withoutEANAddon(result) {
|
|
|
3944
3989
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
3945
3990
|
* @param {number} [options.rows] How many horizontal slices to try.
|
|
3946
3991
|
* @param {boolean} [options.tryHarder] Also scan reversed rows, for mirrored symbols.
|
|
3992
|
+
* @param {'camera'} [options.profile] Require stable, quiet-zone-qualified reads.
|
|
3993
|
+
* @param {0|90|180|270} [options.cameraRotation] Orientation already normalized by the caller.
|
|
3947
3994
|
* @returns {Array<{format: string, text: string, row: number}>}
|
|
3948
3995
|
*/
|
|
3949
3996
|
function decodeOneD(image, options = {}) {
|
|
3950
|
-
const { formats = null, rows = 15, tryHarder = true } = options;
|
|
3997
|
+
const { formats = null, rows = 15, tryHarder = true, profile = null } = options;
|
|
3998
|
+
const cameraProfile = profile === 'camera';
|
|
3951
3999
|
const enabled = formats ? new Set(formats) : null;
|
|
3952
4000
|
const active = DECODERS.filter(([id]) => {
|
|
3953
4001
|
if (!enabled) return true;
|
|
@@ -3966,13 +4014,15 @@ function decodeOneD(image, options = {}) {
|
|
|
3966
4014
|
const seen = new Set();
|
|
3967
4015
|
const height = image.height;
|
|
3968
4016
|
const buffer = new Uint8Array(image.width);
|
|
4017
|
+
const cameraCandidates = new Map();
|
|
3969
4018
|
|
|
3970
4019
|
// Sample rows from the middle outward: symbols are usually centred, and the
|
|
3971
4020
|
// middle of a linear barcode is the part least likely to be clipped.
|
|
3972
4021
|
const middle = height >> 1;
|
|
3973
|
-
const
|
|
4022
|
+
const sampleRows = cameraProfile ? Math.min(height, Math.max(rows, 48)) : rows;
|
|
4023
|
+
const step = Math.max(1, Math.round(height / sampleRows));
|
|
3974
4024
|
|
|
3975
|
-
for (let attempt = 0; attempt <
|
|
4025
|
+
for (let attempt = 0; attempt < sampleRows; attempt++) {
|
|
3976
4026
|
const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
|
|
3977
4027
|
const y = middle + delta;
|
|
3978
4028
|
if (y < 0 || y >= height) continue;
|
|
@@ -3985,7 +4035,12 @@ function decodeOneD(image, options = {}) {
|
|
|
3985
4035
|
for (const [id, decoder] of active) {
|
|
3986
4036
|
let result = null;
|
|
3987
4037
|
try {
|
|
3988
|
-
|
|
4038
|
+
// Code 11 and MSI checks are optional in their base standards, but
|
|
4039
|
+
// a camera frame cannot safely promote their short unchecked forms.
|
|
4040
|
+
const decoderOptions = cameraProfile && (id === 'code11' || id === 'msi')
|
|
4041
|
+
? { ...options, checkDigit: true }
|
|
4042
|
+
: options;
|
|
4043
|
+
result = decoder(scan, decoderOptions);
|
|
3989
4044
|
} catch {
|
|
3990
4045
|
result = null; // a malformed candidate is not an error
|
|
3991
4046
|
}
|
|
@@ -4017,6 +4072,20 @@ function decodeOneD(image, options = {}) {
|
|
|
4017
4072
|
|
|
4018
4073
|
const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
|
|
4019
4074
|
const key = `${result.format}:${result.text}${addonKey}`;
|
|
4075
|
+
if (cameraProfile) {
|
|
4076
|
+
const geometry = cameraRowGeometry(row);
|
|
4077
|
+
// Do not promote partial row fragments from a camera frame.
|
|
4078
|
+
if (!geometry || !geometry.quietZone) continue;
|
|
4079
|
+
const candidate = cameraCandidates.get(key) ?? {
|
|
4080
|
+
result,
|
|
4081
|
+
geometry,
|
|
4082
|
+
rows: new Set(),
|
|
4083
|
+
rotation: ((options.cameraRotation ?? 0) + (pass ? 180 : 0)) % 360,
|
|
4084
|
+
};
|
|
4085
|
+
candidate.rows.add(y);
|
|
4086
|
+
cameraCandidates.set(key, candidate);
|
|
4087
|
+
continue;
|
|
4088
|
+
}
|
|
4020
4089
|
if (seen.has(key)) continue;
|
|
4021
4090
|
seen.add(key);
|
|
4022
4091
|
results.push({ ...result, row: y });
|
|
@@ -4025,6 +4094,18 @@ function decodeOneD(image, options = {}) {
|
|
|
4025
4094
|
}
|
|
4026
4095
|
}
|
|
4027
4096
|
|
|
4097
|
+
if (cameraProfile) {
|
|
4098
|
+
for (const candidate of cameraCandidates.values()) {
|
|
4099
|
+
// A complete symbol must survive at least two nearby scan samples. This
|
|
4100
|
+
// rejects isolated run coincidences without imposing a payload length.
|
|
4101
|
+
if (candidate.rows.size < 2) continue;
|
|
4102
|
+
results.push(cameraMetadata(candidate.result, candidate.geometry, candidate.rows, {
|
|
4103
|
+
...options,
|
|
4104
|
+
cameraRotation: candidate.rotation,
|
|
4105
|
+
}));
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4028
4109
|
// A valid EAN/UPC parent is substantially more constrained than a generic
|
|
4029
4110
|
// narrow/wide candidate. Suppress competing interpretations of the same
|
|
4030
4111
|
// scanline, while retaining symbols detected on other rows.
|
|
@@ -16621,6 +16702,10 @@ function encode(text, options = {}) {
|
|
|
16621
16702
|
* @property {boolean} [certified] Whether the profile is certified by its originator.
|
|
16622
16703
|
* @property {object} [canvas] Canvas reservation metadata for the Sythos profile.
|
|
16623
16704
|
* @property {{format:'ean2'|'ean5', text:string, parity:string, checksum?:number}} [addon] Attached EAN/UPC supplement.
|
|
16705
|
+
* @property {number} [confidence] Camera-profile confidence from 0 to 1.
|
|
16706
|
+
* @property {{x:number,y:number,width:number,height:number}} [bounds] Camera-profile bounds in the scanned orientation.
|
|
16707
|
+
* @property {0|90|180|270} [rotation] Camera-profile orientation in degrees.
|
|
16708
|
+
* @property {{quietZone:boolean,checksum:boolean|null,rows:number|null,consistency:number|null}} [quality] Camera-profile validation evidence.
|
|
16624
16709
|
* @property {boolean} [gs1] Whether the physical symbol is classified as GS1.
|
|
16625
16710
|
* @property {string} [symbologyIdentifier] GS1 symbology identifier.
|
|
16626
16711
|
* @property {Array<{ai:string,value:string,fixed?:boolean}>} [elements] Parsed GS1 Application Identifier fields.
|
|
@@ -16641,12 +16726,13 @@ function encode(text, options = {}) {
|
|
|
16641
16726
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
16642
16727
|
* @param {boolean} [options.tryHarder] Retry inverted and rotated. Default true.
|
|
16643
16728
|
* @param {'global'|'hybrid'|'auto'} [options.binarizer]
|
|
16729
|
+
* @param {'camera'} [options.profile] Opt-in strict camera profile for validated 1D reads.
|
|
16644
16730
|
* @param {object} [options.frameqr] Sythos Canvas QR detector options when
|
|
16645
16731
|
* the profile marker is not preserved through image rendering.
|
|
16646
16732
|
* @returns {DecodeResult[]}
|
|
16647
16733
|
*/
|
|
16648
16734
|
function decode(image, options = {}) {
|
|
16649
|
-
const { formats = null, tryHarder = true, binarizer = 'auto' } = options;
|
|
16735
|
+
const { formats = null, tryHarder = true, binarizer = 'auto', profile = null } = options;
|
|
16650
16736
|
const want = formats ? new Set(formats.map((f) => f.toLowerCase())) : null;
|
|
16651
16737
|
const wantQR = !want || want.has('qr') || want.has('qrcode');
|
|
16652
16738
|
const wantDataMatrix = !want || want.has('datamatrix') || want.has('data-matrix');
|
|
@@ -16786,7 +16872,24 @@ function decode(image, options = {}) {
|
|
|
16786
16872
|
const oneDFormats = want
|
|
16787
16873
|
? [...want].filter((f) => f in ONED_FORMATS || oneDAliases.has(f))
|
|
16788
16874
|
: null;
|
|
16789
|
-
|
|
16875
|
+
const oneDPasses = [{ bits, rotation: 0 }];
|
|
16876
|
+
// A linear symbol rotated by 90° has no usable horizontal scanline.
|
|
16877
|
+
// The strict camera profile adds exactly two normalized orientations,
|
|
16878
|
+
// only when the native orientation found no validated 1D result.
|
|
16879
|
+
const readOneD = (candidateBits, rotation) => decodeOneD(candidateBits, {
|
|
16880
|
+
...options, formats: oneDFormats, tryHarder, profile, cameraRotation: rotation,
|
|
16881
|
+
});
|
|
16882
|
+
let oneDResults = readOneD(bits, 0);
|
|
16883
|
+
if (profile === 'camera' && oneDResults.length === 0) {
|
|
16884
|
+
oneDPasses.push(
|
|
16885
|
+
{ bits: rotateBitMatrix90(bits, false), rotation: 90 },
|
|
16886
|
+
{ bits: rotateBitMatrix90(bits, true), rotation: 270 },
|
|
16887
|
+
);
|
|
16888
|
+
for (let i = 1; i < oneDPasses.length && oneDResults.length === 0; i++) {
|
|
16889
|
+
oneDResults = readOneD(oneDPasses[i].bits, oneDPasses[i].rotation);
|
|
16890
|
+
}
|
|
16891
|
+
}
|
|
16892
|
+
for (const found of oneDResults) {
|
|
16790
16893
|
const { row, ...publicFound } = found;
|
|
16791
16894
|
void row;
|
|
16792
16895
|
if (publicFound.gs1) {
|
|
@@ -16831,7 +16934,11 @@ function decode(image, options = {}) {
|
|
|
16831
16934
|
&& (binarizer === 'auto' || binarizer === 'hybrid')
|
|
16832
16935
|
&& retryFormats.length > 0;
|
|
16833
16936
|
|
|
16834
|
-
if (!shouldRetryGlobal)
|
|
16937
|
+
if (!shouldRetryGlobal) {
|
|
16938
|
+
return profile === 'camera'
|
|
16939
|
+
? unique.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
16940
|
+
: unique;
|
|
16941
|
+
}
|
|
16835
16942
|
|
|
16836
16943
|
const fallback = decode(image, {
|
|
16837
16944
|
...options,
|
|
@@ -16839,12 +16946,32 @@ function decode(image, options = {}) {
|
|
|
16839
16946
|
binarizer: 'global',
|
|
16840
16947
|
});
|
|
16841
16948
|
const fallbackSeen = new Set();
|
|
16842
|
-
|
|
16949
|
+
const merged = [...unique, ...fallback].filter((r) => {
|
|
16843
16950
|
const key = `${r.format}:${r.text}`;
|
|
16844
16951
|
if (fallbackSeen.has(key)) return false;
|
|
16845
16952
|
fallbackSeen.add(key);
|
|
16846
16953
|
return true;
|
|
16847
16954
|
});
|
|
16955
|
+
return profile === 'camera'
|
|
16956
|
+
? merged.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
16957
|
+
: merged;
|
|
16958
|
+
}
|
|
16959
|
+
|
|
16960
|
+
/**
|
|
16961
|
+
* @param {BitMatrix} matrix
|
|
16962
|
+
* @param {boolean} clockwise
|
|
16963
|
+
* @returns {BitMatrix}
|
|
16964
|
+
*/
|
|
16965
|
+
function rotateBitMatrix90(matrix, clockwise) {
|
|
16966
|
+
const rotated = new BitMatrix(matrix.height, matrix.width);
|
|
16967
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
16968
|
+
for (let x = 0; x < matrix.width; x++) {
|
|
16969
|
+
if (!matrix.get(x, y)) continue;
|
|
16970
|
+
if (clockwise) rotated.set(matrix.height - 1 - y, x);
|
|
16971
|
+
else rotated.set(y, matrix.width - 1 - x);
|
|
16972
|
+
}
|
|
16973
|
+
}
|
|
16974
|
+
return rotated;
|
|
16848
16975
|
}
|
|
16849
16976
|
|
|
16850
16977
|
/**
|
|
@@ -16861,7 +16988,7 @@ function decodeStrict(image, options) {
|
|
|
16861
16988
|
}
|
|
16862
16989
|
|
|
16863
16990
|
/** Library version, matching package.json. */
|
|
16864
|
-
const VERSION = '1.5.
|
|
16991
|
+
const VERSION = '1.5.5';
|
|
16865
16992
|
|
|
16866
16993
|
__exports.listFormats = listFormats;
|
|
16867
16994
|
__exports.encode = encode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sythos/js_barcode_universal",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Read and write barcodes in JavaScript with zero runtime dependencies. QR Code, Micro QR, rMQR, FrameQR Code, Aztec Code and Rune, PDF417 variants, GS1 DataBar Omnidirectional/Truncated, EAN supplements and one-dimensional formats.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sythos",
|
package/src/index.js
CHANGED
|
@@ -348,6 +348,10 @@ export function encode(text, options = {}) {
|
|
|
348
348
|
* @property {boolean} [certified] Whether the profile is certified by its originator.
|
|
349
349
|
* @property {object} [canvas] Canvas reservation metadata for the FrameQR Code profile.
|
|
350
350
|
* @property {{format:'ean2'|'ean5', text:string, parity:string, checksum?:number}} [addon] Attached EAN/UPC supplement.
|
|
351
|
+
* @property {number} [confidence] Camera-profile confidence from 0 to 1.
|
|
352
|
+
* @property {{x:number,y:number,width:number,height:number}} [bounds] Camera-profile bounds in the scanned orientation.
|
|
353
|
+
* @property {0|90|180|270} [rotation] Camera-profile orientation in degrees.
|
|
354
|
+
* @property {{quietZone:boolean,checksum:boolean|null,rows:number|null,consistency:number|null}} [quality] Camera-profile validation evidence.
|
|
351
355
|
* @property {boolean} [gs1] Whether the physical symbol is classified as GS1.
|
|
352
356
|
* @property {string} [symbologyIdentifier] GS1 symbology identifier.
|
|
353
357
|
* @property {Array<{ai:string,value:string,fixed?:boolean}>} [elements] Parsed GS1 Application Identifier fields.
|
|
@@ -368,12 +372,13 @@ export function encode(text, options = {}) {
|
|
|
368
372
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
369
373
|
* @param {boolean} [options.tryHarder] Retry inverted and rotated. Default true.
|
|
370
374
|
* @param {'global'|'hybrid'|'auto'} [options.binarizer]
|
|
375
|
+
* @param {'camera'} [options.profile] Opt-in strict camera profile for validated 1D reads.
|
|
371
376
|
* @param {object} [options.frameqr] FrameQR Code detector options when
|
|
372
377
|
* the profile marker is not preserved through image rendering.
|
|
373
378
|
* @returns {DecodeResult[]}
|
|
374
379
|
*/
|
|
375
380
|
export function decode(image, options = {}) {
|
|
376
|
-
const { formats = null, tryHarder = true, binarizer = 'auto' } = options;
|
|
381
|
+
const { formats = null, tryHarder = true, binarizer = 'auto', profile = null } = options;
|
|
377
382
|
const want = formats ? new Set(formats.map((f) => f.toLowerCase())) : null;
|
|
378
383
|
const wantQR = !want || want.has('qr') || want.has('qrcode');
|
|
379
384
|
const wantDataMatrix = !want || want.has('datamatrix') || want.has('data-matrix');
|
|
@@ -513,7 +518,24 @@ export function decode(image, options = {}) {
|
|
|
513
518
|
const oneDFormats = want
|
|
514
519
|
? [...want].filter((f) => f in ONED_FORMATS || oneDAliases.has(f))
|
|
515
520
|
: null;
|
|
516
|
-
|
|
521
|
+
const oneDPasses = [{ bits, rotation: 0 }];
|
|
522
|
+
// A linear symbol rotated by 90° has no usable horizontal scanline.
|
|
523
|
+
// The strict camera profile adds exactly two normalized orientations,
|
|
524
|
+
// only when the native orientation found no validated 1D result.
|
|
525
|
+
const readOneD = (candidateBits, rotation) => decodeOneD(candidateBits, {
|
|
526
|
+
...options, formats: oneDFormats, tryHarder, profile, cameraRotation: rotation,
|
|
527
|
+
});
|
|
528
|
+
let oneDResults = readOneD(bits, 0);
|
|
529
|
+
if (profile === 'camera' && oneDResults.length === 0) {
|
|
530
|
+
oneDPasses.push(
|
|
531
|
+
{ bits: rotateBitMatrix90(bits, false), rotation: 90 },
|
|
532
|
+
{ bits: rotateBitMatrix90(bits, true), rotation: 270 },
|
|
533
|
+
);
|
|
534
|
+
for (let i = 1; i < oneDPasses.length && oneDResults.length === 0; i++) {
|
|
535
|
+
oneDResults = readOneD(oneDPasses[i].bits, oneDPasses[i].rotation);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const found of oneDResults) {
|
|
517
539
|
const { row, ...publicFound } = found;
|
|
518
540
|
void row;
|
|
519
541
|
if (publicFound.gs1) {
|
|
@@ -558,7 +580,11 @@ export function decode(image, options = {}) {
|
|
|
558
580
|
&& (binarizer === 'auto' || binarizer === 'hybrid')
|
|
559
581
|
&& retryFormats.length > 0;
|
|
560
582
|
|
|
561
|
-
if (!shouldRetryGlobal)
|
|
583
|
+
if (!shouldRetryGlobal) {
|
|
584
|
+
return profile === 'camera'
|
|
585
|
+
? unique.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
586
|
+
: unique;
|
|
587
|
+
}
|
|
562
588
|
|
|
563
589
|
const fallback = decode(image, {
|
|
564
590
|
...options,
|
|
@@ -566,12 +592,32 @@ export function decode(image, options = {}) {
|
|
|
566
592
|
binarizer: 'global',
|
|
567
593
|
});
|
|
568
594
|
const fallbackSeen = new Set();
|
|
569
|
-
|
|
595
|
+
const merged = [...unique, ...fallback].filter((r) => {
|
|
570
596
|
const key = `${r.format}:${r.text}`;
|
|
571
597
|
if (fallbackSeen.has(key)) return false;
|
|
572
598
|
fallbackSeen.add(key);
|
|
573
599
|
return true;
|
|
574
600
|
});
|
|
601
|
+
return profile === 'camera'
|
|
602
|
+
? merged.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
603
|
+
: merged;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @param {BitMatrix} matrix
|
|
608
|
+
* @param {boolean} clockwise
|
|
609
|
+
* @returns {BitMatrix}
|
|
610
|
+
*/
|
|
611
|
+
function rotateBitMatrix90(matrix, clockwise) {
|
|
612
|
+
const rotated = new BitMatrix(matrix.height, matrix.width);
|
|
613
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
614
|
+
for (let x = 0; x < matrix.width; x++) {
|
|
615
|
+
if (!matrix.get(x, y)) continue;
|
|
616
|
+
if (clockwise) rotated.set(matrix.height - 1 - y, x);
|
|
617
|
+
else rotated.set(y, matrix.width - 1 - x);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return rotated;
|
|
575
621
|
}
|
|
576
622
|
|
|
577
623
|
/**
|
|
@@ -588,4 +634,4 @@ export function decodeStrict(image, options) {
|
|
|
588
634
|
}
|
|
589
635
|
|
|
590
636
|
/** Library version, matching package.json. */
|
|
591
|
-
export const VERSION = '1.5.
|
|
637
|
+
export const VERSION = '1.5.5';
|
package/src/oned/reader.js
CHANGED
|
@@ -1170,6 +1170,51 @@ function withoutEANAddon(result) {
|
|
|
1170
1170
|
return parent;
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
|
+
/** @param {Uint8Array} row @returns {{x:number, width:number, quietZone:boolean}|null} */
|
|
1174
|
+
function cameraRowGeometry(row) {
|
|
1175
|
+
let first = 0;
|
|
1176
|
+
while (first < row.length && row[first] === 0) first++;
|
|
1177
|
+
if (first === row.length) return null;
|
|
1178
|
+
let last = row.length - 1;
|
|
1179
|
+
while (last >= 0 && row[last] === 0) last--;
|
|
1180
|
+
return {
|
|
1181
|
+
x: first,
|
|
1182
|
+
width: last - first + 1,
|
|
1183
|
+
quietZone: first >= 2 && row.length - 1 - last >= 2,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/** @param {string} format @param {object} options @returns {boolean|null} */
|
|
1188
|
+
function checksumStatus(format, options) {
|
|
1189
|
+
if (format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce' ||
|
|
1190
|
+
format === 'code93' || format === 'code128' || format === 'gs1128' ||
|
|
1191
|
+
format === 'gs1databar14') return true;
|
|
1192
|
+
if (format === 'code11' || format === 'msi' || format === 'code39') {
|
|
1193
|
+
return options.profile === 'camera' || options.checkDigit === true ? true : null;
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** @param {object} result @param {object} geometry @param {Set<number>} rows @param {object} options @returns {object} */
|
|
1199
|
+
function cameraMetadata(result, geometry, rows, options) {
|
|
1200
|
+
const checksum = checksumStatus(result.format, options);
|
|
1201
|
+
const consistency = Math.min(1, rows.size / 3);
|
|
1202
|
+
const confidence = Math.min(1, 0.4 + (geometry.quietZone ? 0.2 : 0) +
|
|
1203
|
+
(checksum === true ? 0.2 : 0) + consistency * 0.2);
|
|
1204
|
+
return {
|
|
1205
|
+
...result,
|
|
1206
|
+
confidence,
|
|
1207
|
+
bounds: {
|
|
1208
|
+
x: geometry.x,
|
|
1209
|
+
y: Math.min(...rows),
|
|
1210
|
+
width: geometry.width,
|
|
1211
|
+
height: Math.max(...rows) - Math.min(...rows) + 1,
|
|
1212
|
+
},
|
|
1213
|
+
rotation: options.cameraRotation ?? 0,
|
|
1214
|
+
quality: { quietZone: geometry.quietZone, checksum, rows: rows.size, consistency },
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1173
1218
|
/**
|
|
1174
1219
|
* Read every linear symbol found in a binarized image.
|
|
1175
1220
|
*
|
|
@@ -1178,10 +1223,13 @@ function withoutEANAddon(result) {
|
|
|
1178
1223
|
* @param {string[]} [options.formats] Restrict to these format ids.
|
|
1179
1224
|
* @param {number} [options.rows] How many horizontal slices to try.
|
|
1180
1225
|
* @param {boolean} [options.tryHarder] Also scan reversed rows, for mirrored symbols.
|
|
1226
|
+
* @param {'camera'} [options.profile] Require stable, quiet-zone-qualified reads.
|
|
1227
|
+
* @param {0|90|180|270} [options.cameraRotation] Orientation already normalized by the caller.
|
|
1181
1228
|
* @returns {Array<{format: string, text: string, row: number}>}
|
|
1182
1229
|
*/
|
|
1183
1230
|
export function decodeOneD(image, options = {}) {
|
|
1184
|
-
const { formats = null, rows = 15, tryHarder = true } = options;
|
|
1231
|
+
const { formats = null, rows = 15, tryHarder = true, profile = null } = options;
|
|
1232
|
+
const cameraProfile = profile === 'camera';
|
|
1185
1233
|
const enabled = formats ? new Set(formats) : null;
|
|
1186
1234
|
const active = DECODERS.filter(([id]) => {
|
|
1187
1235
|
if (!enabled) return true;
|
|
@@ -1200,13 +1248,15 @@ export function decodeOneD(image, options = {}) {
|
|
|
1200
1248
|
const seen = new Set();
|
|
1201
1249
|
const height = image.height;
|
|
1202
1250
|
const buffer = new Uint8Array(image.width);
|
|
1251
|
+
const cameraCandidates = new Map();
|
|
1203
1252
|
|
|
1204
1253
|
// Sample rows from the middle outward: symbols are usually centred, and the
|
|
1205
1254
|
// middle of a linear barcode is the part least likely to be clipped.
|
|
1206
1255
|
const middle = height >> 1;
|
|
1207
|
-
const
|
|
1256
|
+
const sampleRows = cameraProfile ? Math.min(height, Math.max(rows, 48)) : rows;
|
|
1257
|
+
const step = Math.max(1, Math.round(height / sampleRows));
|
|
1208
1258
|
|
|
1209
|
-
for (let attempt = 0; attempt <
|
|
1259
|
+
for (let attempt = 0; attempt < sampleRows; attempt++) {
|
|
1210
1260
|
const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
|
|
1211
1261
|
const y = middle + delta;
|
|
1212
1262
|
if (y < 0 || y >= height) continue;
|
|
@@ -1219,7 +1269,12 @@ export function decodeOneD(image, options = {}) {
|
|
|
1219
1269
|
for (const [id, decoder] of active) {
|
|
1220
1270
|
let result = null;
|
|
1221
1271
|
try {
|
|
1222
|
-
|
|
1272
|
+
// Code 11 and MSI checks are optional in their base standards, but
|
|
1273
|
+
// a camera frame cannot safely promote their short unchecked forms.
|
|
1274
|
+
const decoderOptions = cameraProfile && (id === 'code11' || id === 'msi')
|
|
1275
|
+
? { ...options, checkDigit: true }
|
|
1276
|
+
: options;
|
|
1277
|
+
result = decoder(scan, decoderOptions);
|
|
1223
1278
|
} catch {
|
|
1224
1279
|
result = null; // a malformed candidate is not an error
|
|
1225
1280
|
}
|
|
@@ -1251,6 +1306,20 @@ export function decodeOneD(image, options = {}) {
|
|
|
1251
1306
|
|
|
1252
1307
|
const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
|
|
1253
1308
|
const key = `${result.format}:${result.text}${addonKey}`;
|
|
1309
|
+
if (cameraProfile) {
|
|
1310
|
+
const geometry = cameraRowGeometry(row);
|
|
1311
|
+
// Do not promote partial row fragments from a camera frame.
|
|
1312
|
+
if (!geometry || !geometry.quietZone) continue;
|
|
1313
|
+
const candidate = cameraCandidates.get(key) ?? {
|
|
1314
|
+
result,
|
|
1315
|
+
geometry,
|
|
1316
|
+
rows: new Set(),
|
|
1317
|
+
rotation: ((options.cameraRotation ?? 0) + (pass ? 180 : 0)) % 360,
|
|
1318
|
+
};
|
|
1319
|
+
candidate.rows.add(y);
|
|
1320
|
+
cameraCandidates.set(key, candidate);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1254
1323
|
if (seen.has(key)) continue;
|
|
1255
1324
|
seen.add(key);
|
|
1256
1325
|
results.push({ ...result, row: y });
|
|
@@ -1259,6 +1328,18 @@ export function decodeOneD(image, options = {}) {
|
|
|
1259
1328
|
}
|
|
1260
1329
|
}
|
|
1261
1330
|
|
|
1331
|
+
if (cameraProfile) {
|
|
1332
|
+
for (const candidate of cameraCandidates.values()) {
|
|
1333
|
+
// A complete symbol must survive at least two nearby scan samples. This
|
|
1334
|
+
// rejects isolated run coincidences without imposing a payload length.
|
|
1335
|
+
if (candidate.rows.size < 2) continue;
|
|
1336
|
+
results.push(cameraMetadata(candidate.result, candidate.geometry, candidate.rows, {
|
|
1337
|
+
...options,
|
|
1338
|
+
cameraRotation: candidate.rotation,
|
|
1339
|
+
}));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1262
1343
|
// A valid EAN/UPC parent is substantially more constrained than a generic
|
|
1263
1344
|
// narrow/wide candidate. Suppress competing interpretations of the same
|
|
1264
1345
|
// scanline, while retaining symbols detected on other rows.
|