@sythos/js_barcode_universal 0.1.0 → 1.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Sythos Barcode Suite v0.1.0
2
+ * Sythos Barcode Suite v1.1.0
3
3
  *
4
4
  * MIT License
5
5
  *
@@ -2752,155 +2752,6 @@ __modules["oned/index.js"] = function (__require, __exports) {
2752
2752
  __exports.ONED_FORMATS = ONED_FORMATS;
2753
2753
  };
2754
2754
 
2755
- __modules["core/bit-buffer.js"] = function (__require, __exports) {
2756
- /**
2757
- * Bit-level writing and reading, MSB-first.
2758
- *
2759
- * Every 2D symbology serialises its payload as a bitstream that does not
2760
- * respect byte boundaries — QR alone mixes 4-bit mode indicators, 10-bit
2761
- * character-count fields and 11-bit alphanumeric pairs. These two classes are
2762
- * the write and read halves of that.
2763
- *
2764
- * @module core/bit-buffer
2765
- */
2766
- const { FormatError } = __require("core/errors.js");
2767
-
2768
- /** Growable MSB-first bit writer. */
2769
- class BitWriter {
2770
- constructor() {
2771
- /** @type {number[]} Packed bytes; the last one may be partially filled. */
2772
- this.bytes = [];
2773
- this.bitLength = 0;
2774
- }
2775
-
2776
- /** @returns {number} Bits written so far. */
2777
- get length() {
2778
- return this.bitLength;
2779
- }
2780
-
2781
- /**
2782
- * Append the low `count` bits of `value`, most significant first.
2783
- *
2784
- * @param {number} value
2785
- * @param {number} count
2786
- */
2787
- put(value, count) {
2788
- for (let i = count - 1; i >= 0; i--) {
2789
- this.putBit(((value >>> i) & 1) === 1);
2790
- }
2791
- }
2792
-
2793
- /** @param {boolean} bit */
2794
- putBit(bit) {
2795
- const idx = this.bitLength >>> 3;
2796
- if (this.bytes.length <= idx) this.bytes.push(0);
2797
- if (bit) this.bytes[idx] |= 0x80 >>> (this.bitLength & 7);
2798
- this.bitLength++;
2799
- }
2800
-
2801
- /** @param {ArrayLike<number>} data */
2802
- putBytes(data) {
2803
- for (let i = 0; i < data.length; i++) this.put(data[i], 8);
2804
- }
2805
-
2806
- /** Pad with zero bits until the length is a multiple of 8. */
2807
- padToByte() {
2808
- while (this.bitLength & 7) this.putBit(false);
2809
- }
2810
-
2811
- /**
2812
- * @returns {Uint8Array} Byte view; trailing bits of the final byte are zero.
2813
- */
2814
- toBytes() {
2815
- return Uint8Array.from(this.bytes);
2816
- }
2817
-
2818
- /** @returns {string} Debug view, e.g. "0100 0011 0101". */
2819
- toString() {
2820
- let s = '';
2821
- for (let i = 0; i < this.bitLength; i++) {
2822
- if (i && i % 4 === 0) s += ' ';
2823
- s += (this.bytes[i >>> 3] >>> (7 - (i & 7))) & 1;
2824
- }
2825
- return s;
2826
- }
2827
- }
2828
-
2829
- /** MSB-first bit reader over a byte array. */
2830
- class BitReader {
2831
- /** @param {ArrayLike<number>} bytes */
2832
- constructor(bytes) {
2833
- this.bytes = bytes;
2834
- this.byteOffset = 0;
2835
- this.bitOffset = 0;
2836
- }
2837
-
2838
- /** @returns {number} Bits not yet consumed. */
2839
- available() {
2840
- return 8 * (this.bytes.length - this.byteOffset) - this.bitOffset;
2841
- }
2842
-
2843
- /**
2844
- * Read `count` bits (1..32) as an unsigned integer, most significant first.
2845
- *
2846
- * @param {number} count
2847
- * @returns {number}
2848
- * @throws {FormatError} If the stream is exhausted.
2849
- */
2850
- read(count) {
2851
- if (count < 1 || count > 32) {
2852
- throw new FormatError(`BitReader: cannot read ${count} bits`);
2853
- }
2854
- if (count > this.available()) {
2855
- throw new FormatError(
2856
- `BitReader: needed ${count} bits, ${this.available()} remain`
2857
- );
2858
- }
2859
-
2860
- let result = 0;
2861
- let remaining = count;
2862
-
2863
- // Finish the partially consumed byte first, then take whole bytes.
2864
- if (this.bitOffset > 0) {
2865
- const inCurrent = 8 - this.bitOffset;
2866
- const take = Math.min(remaining, inCurrent);
2867
- const shift = inCurrent - take;
2868
- const mask = (0xff >> this.bitOffset) & ~((1 << shift) - 1);
2869
- result = (this.bytes[this.byteOffset] & mask) >> shift;
2870
- remaining -= take;
2871
- this.bitOffset += take;
2872
- if (this.bitOffset === 8) {
2873
- this.bitOffset = 0;
2874
- this.byteOffset++;
2875
- }
2876
- }
2877
-
2878
- while (remaining >= 8) {
2879
- result = (result << 8) | (this.bytes[this.byteOffset] & 0xff);
2880
- this.byteOffset++;
2881
- remaining -= 8;
2882
- }
2883
-
2884
- if (remaining > 0) {
2885
- const shift = 8 - remaining;
2886
- const mask = ~((1 << shift) - 1) & 0xff;
2887
- result = (result << remaining) | ((this.bytes[this.byteOffset] & mask) >> shift);
2888
- this.bitOffset += remaining;
2889
- }
2890
-
2891
- return result >>> 0;
2892
- }
2893
-
2894
- /** @returns {boolean} */
2895
- readBit() {
2896
- return this.read(1) === 1;
2897
- }
2898
- }
2899
-
2900
- __exports.BitWriter = BitWriter;
2901
- __exports.BitReader = BitReader;
2902
- };
2903
-
2904
2755
  __modules["core/galois-field.js"] = function (__require, __exports) {
2905
2756
  /**
2906
2757
  * Finite field arithmetic.
@@ -3067,6 +2918,9 @@ const GF256_QR = new GaloisField({ size: 256, primitive: 0x011d, name: 'GF(256)/
3067
2918
  /** Data Matrix ECC200. x^8 + x^5 + x^3 + x^2 + 1 */
3068
2919
  const GF256_DM = new GaloisField({ size: 256, primitive: 0x012d, name: 'GF(256)/DataMatrix' });
3069
2920
 
2921
+ /** Aztec's eight-bit data field is algebraically identical to Data Matrix's. */
2922
+ const GF256_AZTEC = GF256_DM;
2923
+
3070
2924
  /** PDF417. Prime field; 3 is a primitive root modulo 929. */
3071
2925
  const GF929 = new GaloisField({ size: 929, prime: true, generator: 3, name: 'GF(929)' });
3072
2926
 
@@ -3079,6 +2933,7 @@ const GF4096 = new GaloisField({ size: 4096, primitive: 0x1069, name: 'GF(4096)'
3079
2933
  __exports.GaloisField = GaloisField;
3080
2934
  __exports.GF256_QR = GF256_QR;
3081
2935
  __exports.GF256_DM = GF256_DM;
2936
+ __exports.GF256_AZTEC = GF256_AZTEC;
3082
2937
  __exports.GF929 = GF929;
3083
2938
  __exports.GF16 = GF16;
3084
2939
  __exports.GF64 = GF64;
@@ -3114,7 +2969,7 @@ const { ChecksumError } = __require("core/errors.js");
3114
2969
  *
3115
2970
  * g(x) = product over i of (x - a^(base + i)), i = 0 .. eccLen-1
3116
2971
  *
3117
- * `base` is 0 for QR, Data Matrix and Aztec; 1 for PDF417.
2972
+ * `base` is 0 for QR; 1 for Aztec, Data Matrix and PDF417.
3118
2973
  *
3119
2974
  * @param {number} eccLen
3120
2975
  * @param {import('./galois-field.js').GaloisField} field
@@ -3375,349 +3230,1521 @@ __exports.rsEncode = rsEncode;
3375
3230
  __exports.rsDecode = rsDecode;
3376
3231
  };
3377
3232
 
3378
- __modules["qr/tables.js"] = function (__require, __exports) {
3233
+ __modules["datamatrix/tables.js"] = function (__require, __exports) {
3379
3234
  /**
3380
- * QR Code structural tables.
3381
- *
3382
- * The design principle here is that as little as possible is *recalled* and as
3383
- * much as possible is *derived*, because a barcode table is the one place where
3384
- * a single mistyped digit produces a symbol that looks perfect and scans as
3385
- * garbage — or, worse, scans correctly for the payload you tested and fails for
3386
- * the payload your user sends.
3387
- *
3388
- * So:
3235
+ * Data Matrix ECC 200 symbol parameters.
3389
3236
  *
3390
- * - Symbol size, function-pattern layout and total codeword capacity are
3391
- * computed from geometry. Nothing is tabulated that the module grid already
3392
- * knows.
3393
- * - Alignment centres come from the spec's spacing rule, not a 40-row table.
3394
- * - The group-1 / group-2 block split is arithmetic, not data.
3395
- *
3396
- * That leaves exactly three recalled numbers per (version, level): the error
3397
- * correction codewords per block, the block count, and the total data codeword
3398
- * count. Those three are deliberately redundant — they must satisfy
3399
- *
3400
- * blocks * eccPerBlock + totalDataCodewords === geometricTotalCodewords(v)
3401
- *
3402
- * for all 160 combinations, where the right-hand side is counted off the module
3403
- * grid. Any single typo on either side breaks the identity. {@link validateTables}
3404
- * enforces it, and the test suite asserts it returns no problems.
3237
+ * Width and height include finder borders. `regionWidth` and `regionHeight`
3238
+ * describe the usable modules inside one data region. The last three columns
3239
+ * make the Reed-Solomon block split explicit instead of hiding the 144x144
3240
+ * exception in encoder control flow.
3405
3241
  *
3406
- * @module qr/tables
3242
+ * @module datamatrix/tables
3407
3243
  */
3244
+
3245
+ function symbol(width, height, dataRegionWidth, dataRegionHeight, dataCodewords, errorCodewords, dataBlockLengths) {
3246
+ const blockCount = dataBlockLengths.length;
3247
+ return Object.freeze({
3248
+ width, height, rows: height, columns: width,
3249
+ // Region dimensions include their one-module finder border on each side;
3250
+ // dataRegion* expose the inner placement lattice explicitly.
3251
+ regionWidth: dataRegionWidth + 2, regionHeight: dataRegionHeight + 2,
3252
+ dataRegionWidth, dataRegionHeight,
3253
+ dataRegionRows: dataRegionHeight, dataRegionColumns: dataRegionWidth,
3254
+ dataCodewords, errorCodewords, blockCount,
3255
+ eccPerBlock: errorCodewords / blockCount,
3256
+ dataBlockLengths: Object.freeze(dataBlockLengths),
3257
+ });
3258
+ }
3259
+
3260
+ /** Classic ISO/IEC 16022 ECC 200 symbols; DMRE is deliberately excluded. */
3261
+ const DATAMATRIX_SYMBOLS = Object.freeze([
3262
+ symbol(10, 10, 8, 8, 3, 5, [3]),
3263
+ symbol(12, 12, 10, 10, 5, 7, [5]),
3264
+ symbol(14, 14, 12, 12, 8, 10, [8]),
3265
+ symbol(16, 16, 14, 14, 12, 12, [12]),
3266
+ symbol(18, 18, 16, 16, 18, 14, [18]),
3267
+ symbol(20, 20, 18, 18, 22, 18, [22]),
3268
+ symbol(22, 22, 20, 20, 30, 20, [30]),
3269
+ symbol(24, 24, 22, 22, 36, 24, [36]),
3270
+ symbol(26, 26, 24, 24, 44, 28, [44]),
3271
+ symbol(32, 32, 14, 14, 62, 36, [62]),
3272
+ symbol(36, 36, 16, 16, 86, 42, [86]),
3273
+ symbol(40, 40, 18, 18, 114, 48, [114]),
3274
+ symbol(44, 44, 20, 20, 144, 56, [144]),
3275
+ symbol(48, 48, 22, 22, 174, 68, [174]),
3276
+ symbol(52, 52, 24, 24, 204, 84, [102, 102]),
3277
+ symbol(64, 64, 14, 14, 280, 112, [140, 140]),
3278
+ symbol(72, 72, 16, 16, 368, 144, [92, 92, 92, 92]),
3279
+ symbol(80, 80, 18, 18, 456, 192, [114, 114, 114, 114]),
3280
+ symbol(88, 88, 20, 20, 576, 224, [144, 144, 144, 144]),
3281
+ symbol(96, 96, 22, 22, 696, 272, [174, 174, 174, 174]),
3282
+ symbol(104, 104, 24, 24, 816, 336, [136, 136, 136, 136, 136, 136]),
3283
+ symbol(120, 120, 18, 18, 1050, 408, [175, 175, 175, 175, 175, 175]),
3284
+ symbol(132, 132, 20, 20, 1304, 496, [163, 163, 163, 163, 163, 163, 163, 163]),
3285
+ symbol(144, 144, 22, 22, 1558, 620, [156, 156, 156, 156, 156, 156, 156, 156, 155, 155]),
3286
+ symbol(18, 8, 16, 6, 5, 7, [5]),
3287
+ symbol(32, 8, 14, 6, 10, 11, [10]),
3288
+ symbol(26, 12, 24, 10, 16, 14, [16]),
3289
+ symbol(36, 12, 16, 10, 22, 18, [22]),
3290
+ symbol(36, 16, 16, 14, 32, 24, [32]),
3291
+ symbol(48, 16, 22, 14, 49, 28, [49]),
3292
+ ]);
3293
+
3294
+ /** Compatibility alias. */
3295
+ const SYMBOLS = DATAMATRIX_SYMBOLS;
3296
+
3297
+ /** Return the smallest permitted symbol that holds `count` data codewords. */
3298
+ function symbolForDataCodewords(count, shape = 'any') {
3299
+ for (const s of DATAMATRIX_SYMBOLS) {
3300
+ const rectangular = s.width !== s.height;
3301
+ if ((shape === 'square' && rectangular) || (shape === 'rectangular' && !rectangular)) continue;
3302
+ if (count <= s.dataCodewords) return s;
3303
+ }
3304
+ throw new RangeError(`Data Matrix: ${count} data codewords do not fit an ECC 200 ${shape} symbol`);
3305
+ }
3306
+
3307
+ /** Check redundant geometry and block identities in the static table. */
3308
+ function validateDataMatrixTables() {
3309
+ const issues = [];
3310
+ for (const s of DATAMATRIX_SYMBOLS) {
3311
+ const regionsX = s.width / s.regionWidth;
3312
+ const regionsY = s.height / s.regionHeight;
3313
+ if (!Number.isInteger(regionsX) || !Number.isInteger(regionsY)) issues.push(`${s.width}x${s.height}: non-integral regions`);
3314
+ const modules = regionsX * regionsY * s.dataRegionWidth * s.dataRegionHeight;
3315
+ // Annex F reserves four terminal modules on a few lattice dimensions.
3316
+ // They are set to dark after codeword placement and do not carry data.
3317
+ const unused = modules - (s.dataCodewords + s.errorCodewords) * 8;
3318
+ if (unused !== 0 && unused !== 4) issues.push(`${s.width}x${s.height}: geometry/codeword mismatch`);
3319
+ if (s.dataBlockLengths.reduce((a, b) => a + b, 0) !== s.dataCodewords) issues.push(`${s.width}x${s.height}: data block mismatch`);
3320
+ if (s.eccPerBlock * s.blockCount !== s.errorCodewords) issues.push(`${s.width}x${s.height}: ecc block mismatch`);
3321
+ }
3322
+ return issues;
3323
+ }
3324
+
3325
+ /** Compatibility alias. */
3326
+ const validateTables = validateDataMatrixTables;
3327
+
3328
+ __exports.DATAMATRIX_SYMBOLS = DATAMATRIX_SYMBOLS;
3329
+ __exports.SYMBOLS = SYMBOLS;
3330
+ __exports.symbolForDataCodewords = symbolForDataCodewords;
3331
+ __exports.validateDataMatrixTables = validateDataMatrixTables;
3332
+ __exports.validateTables = validateTables;
3333
+ };
3334
+
3335
+ __modules["datamatrix/encoder.js"] = function (__require, __exports) {
3336
+ /** Data Matrix ECC 200 encoder: ASCII/Base256, RS interleaving and Annex F placement. */
3408
3337
  const { BitMatrix } = __require("core/bit-matrix.js");
3338
+ const { EncodeError } = __require("core/errors.js");
3339
+ const { GF256_DM } = __require("core/galois-field.js");
3340
+ const { rsEncode } = __require("core/reed-solomon.js");
3341
+ const { symbolForDataCodewords } = __require("datamatrix/tables.js");
3409
3342
 
3410
- /** Error correction levels, weakest to strongest. */
3411
- const ECC_LEVELS = ['L', 'M', 'Q', 'H'];
3343
+ function asciiCodewords(text) {
3344
+ const out = [];
3345
+ for (let i = 0; i < text.length;) {
3346
+ const a = text.charCodeAt(i);
3347
+ if (a > 255) throw new EncodeError('Data Matrix ASCII: characters must fit ISO-8859-1; use Base256 for UTF-8');
3348
+ if (i + 1 < text.length) {
3349
+ const b = text.charCodeAt(i + 1);
3350
+ if (a >= 48 && a <= 57 && b >= 48 && b <= 57) { out.push(130 + (a - 48) * 10 + b - 48); i += 2; continue; }
3351
+ }
3352
+ if (a <= 127) out.push(a + 1);
3353
+ else out.push(235, a - 127);
3354
+ i++;
3355
+ }
3356
+ return out;
3357
+ }
3412
3358
 
3413
- /**
3414
- * Two-bit level indicator used in the format information.
3415
- * Note this is *not* the L/M/Q/H ordering — the spec assigns them out of order.
3416
- */
3417
- const ECC_LEVEL_BITS = { L: 0b01, M: 0b00, Q: 0b11, H: 0b10 };
3359
+ function bytesFor(value) {
3360
+ if (value instanceof Uint8Array) return value;
3361
+ if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
3362
+ if (typeof value !== 'string') throw new EncodeError('Data Matrix: value must be a string or byte array');
3363
+ return new TextEncoder().encode(value);
3364
+ }
3418
3365
 
3419
- /** Inverse of {@link ECC_LEVEL_BITS}, indexed by the 2-bit value. */
3420
- const ECC_LEVEL_BY_BITS = ['M', 'L', 'H', 'Q'];
3421
- const MIN_VERSION = 1;
3422
- const MAX_VERSION = 40;
3366
+ function randomize255(value, position) {
3367
+ const pseudo = (149 * position) % 255 + 1;
3368
+ return value + pseudo <= 255 ? value + pseudo : value + pseudo - 256;
3369
+ }
3423
3370
 
3424
- /** Version at and above which an 18-bit version information block is carried. */
3425
- const VERSION_INFO_MIN = 7;
3371
+ function base256Codewords(value, prefixLength = 0) {
3372
+ const bytes = bytesFor(value);
3373
+ if (bytes.length > 1555) throw new EncodeError('Data Matrix Base256: payload exceeds ECC 200 capacity');
3374
+ const out = [231];
3375
+ if (bytes.length <= 249) out.push(bytes.length);
3376
+ else out.push(Math.floor(bytes.length / 250) + 249, bytes.length % 250);
3377
+ // Base256 randomization uses the absolute 1-based codeword position in the
3378
+ // symbol. A leading GS1 FNC1 therefore shifts every randomized codeword.
3379
+ for (let i = 1; i < out.length; i++) out[i] = randomize255(out[i], prefixLength + i + 1);
3380
+ for (const b of bytes) out.push(randomize255(b, prefixLength + out.length + 1));
3381
+ return out;
3382
+ }
3426
3383
 
3427
- /** Mode indicator nibbles. */
3428
- const MODE = {
3429
- TERMINATOR: 0x0,
3430
- NUMERIC: 0x1,
3431
- ALPHANUMERIC: 0x2,
3432
- STRUCTURED_APPEND: 0x3,
3433
- BYTE: 0x4,
3434
- FNC1_FIRST: 0x5,
3435
- ECI: 0x7,
3436
- KANJI: 0x8,
3437
- FNC1_SECOND: 0x9,
3438
- };
3384
+ function pad(data, capacity) {
3385
+ const out = data.slice();
3386
+ if (out.length < capacity) out.push(129);
3387
+ while (out.length < capacity) {
3388
+ const position = out.length + 1;
3389
+ const pseudo = (149 * position) % 253 + 1;
3390
+ const v = 129 + pseudo;
3391
+ out.push(v <= 254 ? v : v - 254);
3392
+ }
3393
+ return out;
3394
+ }
3439
3395
 
3440
- /**
3441
- * Character count indicator width, in bits, by mode and version band.
3442
- *
3443
- * The bands are versions 1-9, 10-26 and 27-40. They are the reason segment
3444
- * selection and version selection are mutually dependent: widening the count
3445
- * field can push a payload over a version boundary, which widens it again.
3446
- */
3447
- const COUNT_BITS = {
3448
- [MODE.NUMERIC]: [10, 12, 14],
3449
- [MODE.ALPHANUMERIC]: [9, 11, 13],
3450
- [MODE.BYTE]: [8, 16, 16],
3451
- [MODE.KANJI]: [8, 10, 12],
3452
- };
3396
+ function interleave(data, symbol) {
3397
+ const blocks = symbol.dataBlockLengths.map((length) => ({ data: new Array(length), ecc: null }));
3398
+ let at = 0;
3399
+ const longest = Math.max(...symbol.dataBlockLengths);
3400
+ // Data codewords are dealt across the RS blocks by column. Splitting the
3401
+ // stream into consecutive chunks and then interleaving those chunks looks
3402
+ // self-consistent to a decoder doing the same inverse operation, but it is
3403
+ // not ECC 200's wire order once a symbol has multiple blocks.
3404
+ for (let i = 0; i < longest; i++) {
3405
+ for (const block of blocks) if (i < block.data.length) block.data[i] = data[at++];
3406
+ }
3407
+ for (const block of blocks) {
3408
+ // ECC 200 starts its generator roots at alpha^1 (generator base 1).
3409
+ block.ecc = rsEncode(block.data, symbol.eccPerBlock, GF256_DM, 1);
3410
+ }
3453
3411
 
3454
- /**
3455
- * @param {number} version 1-40
3456
- * @returns {number} Modules per side.
3457
- */
3458
- function versionSize(version) {
3459
- return 17 + 4 * version;
3412
+ const out = [];
3413
+ for (let i = 0; i < longest; i++) for (const b of blocks) if (i < b.data.length) out.push(b.data[i]);
3414
+ // The 144x144 symbol has eight long and two short data blocks. Its parity
3415
+ // interleave begins with the first short block; deriving the rotation from
3416
+ // the declarative lengths keeps that exception out of a size-specific test.
3417
+ const eccOffset = blocks.findIndex((block) => block.data.length < longest);
3418
+ const rotation = eccOffset < 0 ? 0 : eccOffset;
3419
+ for (let i = 0; i < symbol.eccPerBlock; i++) {
3420
+ for (let b = 0; b < blocks.length; b++) out.push(blocks[(b + rotation) % blocks.length].ecc[i]);
3421
+ }
3422
+ return out;
3460
3423
  }
3461
3424
 
3425
+ function place(codewords, rows, cols) {
3426
+ const cells = new Int8Array(rows * cols).fill(-1);
3427
+ const bit = (row, col, pos, n) => {
3428
+ if (row < 0) { row += rows; col += 4 - ((rows + 4) % 8); }
3429
+ if (col < 0) { col += cols; row += 4 - ((cols + 4) % 8); }
3430
+ cells[row * cols + col] = (codewords[pos] >>> (8 - n)) & 1;
3431
+ };
3432
+ const utah = (r, c, p) => { bit(r - 2, c - 2, p, 1); bit(r - 2, c - 1, p, 2); bit(r - 1, c - 2, p, 3); bit(r - 1, c - 1, p, 4); bit(r - 1, c, p, 5); bit(r, c - 2, p, 6); bit(r, c - 1, p, 7); bit(r, c, p, 8); };
3433
+ const corner1 = (p) => { bit(rows - 1, 0, p, 1); bit(rows - 1, 1, p, 2); bit(rows - 1, 2, p, 3); bit(0, cols - 2, p, 4); bit(0, cols - 1, p, 5); bit(1, cols - 1, p, 6); bit(2, cols - 1, p, 7); bit(3, cols - 1, p, 8); };
3434
+ const corner2 = (p) => { bit(rows - 3, 0, p, 1); bit(rows - 2, 0, p, 2); bit(rows - 1, 0, p, 3); bit(0, cols - 4, p, 4); bit(0, cols - 3, p, 5); bit(0, cols - 2, p, 6); bit(0, cols - 1, p, 7); bit(1, cols - 1, p, 8); };
3435
+ const corner3 = (p) => { bit(rows - 3, 0, p, 1); bit(rows - 2, 0, p, 2); bit(rows - 1, 0, p, 3); bit(0, cols - 2, p, 4); bit(0, cols - 1, p, 5); bit(1, cols - 1, p, 6); bit(2, cols - 1, p, 7); bit(3, cols - 1, p, 8); };
3436
+ const corner4 = (p) => { bit(rows - 1, 0, p, 1); bit(rows - 1, cols - 1, p, 2); bit(0, cols - 3, p, 3); bit(0, cols - 2, p, 4); bit(0, cols - 1, p, 5); bit(1, cols - 3, p, 6); bit(1, cols - 2, p, 7); bit(1, cols - 1, p, 8); };
3437
+ let row = 4, col = 0, pos = 0;
3438
+ do {
3439
+ if (row === rows && col === 0) corner1(pos++);
3440
+ if (row === rows - 2 && col === 0 && cols % 4 !== 0) corner2(pos++);
3441
+ if (row === rows - 2 && col === 0 && cols % 8 === 4) corner3(pos++);
3442
+ if (row === rows + 4 && col === 2 && cols % 8 === 0) corner4(pos++);
3443
+ do { if (row < rows && col >= 0 && cells[row * cols + col] < 0) utah(row, col, pos++); row -= 2; col += 2; } while (row >= 0 && col < cols);
3444
+ row += 1; col += 3;
3445
+ do { if (row >= 0 && col < cols && cells[row * cols + col] < 0) utah(row, col, pos++); row += 2; col -= 2; } while (row < rows && col >= 0);
3446
+ row += 3; col += 1;
3447
+ } while (row < rows || col < cols);
3448
+ if (cells[cells.length - 1] < 0) { cells[cells.length - 1] = 1; cells[cells.length - cols - 2] = 1; }
3449
+ if (pos !== codewords.length) throw new EncodeError(`Data Matrix: placement consumed ${pos} of ${codewords.length} codewords`);
3450
+ return cells;
3451
+ }
3452
+
3453
+ function buildMatrix(codewords, symbol) {
3454
+ const regionCols = symbol.width / symbol.regionWidth;
3455
+ const regionRows = symbol.height / symbol.regionHeight;
3456
+ const dataWidth = symbol.dataRegionColumns;
3457
+ const dataHeight = symbol.dataRegionRows;
3458
+ const data = place(codewords, regionRows * dataHeight, regionCols * dataWidth);
3459
+ const matrix = new BitMatrix(symbol.width, symbol.height);
3460
+ for (let ry = 0; ry < regionRows; ry++) for (let rx = 0; rx < regionCols; rx++) {
3461
+ const x0 = rx * symbol.regionWidth, y0 = ry * symbol.regionHeight;
3462
+ for (let x = 0; x < symbol.regionWidth; x++) { if ((x & 1) === 0) matrix.set(x0 + x, y0); matrix.set(x0 + x, y0 + dataHeight + 1); }
3463
+ // The top and right timing borders are complementary: top-left is dark,
3464
+ // top-right is light, and the solid bottom-right corner remains dark.
3465
+ for (let y = 0; y < symbol.regionHeight; y++) { matrix.set(x0, y0 + y); if ((y & 1) === 1) matrix.set(x0 + dataWidth + 1, y0 + y); }
3466
+ for (let y = 0; y < dataHeight; y++) for (let x = 0; x < dataWidth; x++) if (data[(ry * dataHeight + y) * (regionCols * dataWidth) + rx * dataWidth + x]) matrix.set(x0 + 1 + x, y0 + 1 + y);
3467
+ }
3468
+ return matrix;
3469
+ }
3470
+
3471
+ /** Encode a string (ASCII mode) or byte payload (Base256) into Data Matrix ECC 200. */
3472
+ function encodeDataMatrix(value, options = {}) {
3473
+ const encoding = options.encoding ?? (value instanceof Uint8Array ? 'base256' : 'ascii');
3474
+ let raw;
3475
+ if (encoding === 'ascii') {
3476
+ if (typeof value !== 'string') throw new EncodeError('Data Matrix ASCII: value must be a string');
3477
+ raw = asciiCodewords(value);
3478
+ } else if (encoding === 'base256') raw = base256Codewords(value, options.gs1 === true ? 1 : 0);
3479
+ else throw new EncodeError(`Data Matrix: unsupported encoding "${encoding}"`);
3480
+ // GS1 DataMatrix is ECC 200 with FNC1 in the first codeword position.
3481
+ if (options.gs1 === true) raw.unshift(232);
3482
+ const shape = options.shape ?? 'any';
3483
+ if (shape !== 'any' && shape !== 'square' && shape !== 'rectangular') throw new EncodeError(`Data Matrix: invalid shape "${shape}"`);
3484
+ const symbol = symbolForDataCodewords(raw.length, shape);
3485
+ if (!symbol) throw new EncodeError(`Data Matrix: ${raw.length} data codewords do not fit an ECC 200 ${shape} symbol`);
3486
+ return buildMatrix(interleave(pad(raw, symbol.dataCodewords), symbol), symbol);
3487
+ }
3488
+
3489
+ /** Encode already compacted ASCII/Base256 codewords, primarily for conformance tests. */
3490
+ function encodeDataMatrixCodewords(codewords, options = {}) {
3491
+ if (!Array.isArray(codewords) && !(codewords instanceof Uint8Array)) throw new EncodeError('Data Matrix: codewords must be an array');
3492
+ for (const c of codewords) if (!Number.isInteger(c) || c < 0 || c > 255) throw new EncodeError('Data Matrix: codewords must be bytes');
3493
+ const symbol = symbolForDataCodewords(codewords.length, options.shape ?? 'any');
3494
+ if (!symbol) throw new EncodeError('Data Matrix: codewords do not fit ECC 200');
3495
+ return buildMatrix(interleave(pad(Array.from(codewords), symbol.dataCodewords), symbol), symbol);
3496
+ }
3497
+
3498
+ __exports.encodeDataMatrix = encodeDataMatrix;
3499
+ __exports.encodeDataMatrixCodewords = encodeDataMatrixCodewords;
3500
+ };
3501
+
3502
+ __modules["datamatrix/decoder.js"] = function (__require, __exports) {
3462
3503
  /**
3463
- * Bits in the character count indicator.
3504
+ * Data Matrix ECC 200 decoder for an already sampled symbol.
3464
3505
  *
3465
- * @param {number} mode One of {@link MODE}.
3466
- * @param {number} version
3467
- * @returns {number}
3506
+ * The detector owns locating, perspective correction and orientation. This
3507
+ * module starts with the complete, upright symbol including its finder borders.
3508
+ * The table entry is deliberately read through a small normalizer so table data
3509
+ * remains declarative: it needs total rows/columns, one data-region's rows and
3510
+ * columns, data/ECC codeword counts, and either a block count or data block
3511
+ * lengths. The standard 144x144 uneven data blocks are supported.
3512
+ *
3513
+ * @module datamatrix/decoder
3468
3514
  */
3469
- function countBits(mode, version) {
3470
- const widths = COUNT_BITS[mode];
3471
- if (!widths) return 0;
3472
- if (version <= 9) return widths[0];
3473
- if (version <= 26) return widths[1];
3474
- return widths[2];
3475
- }
3476
-
3477
- /**
3478
- * Centre coordinates of the alignment patterns for a version.
3479
- *
3480
- * The spec's rule: the first centre is always 6 and the last is always
3481
- * `size - 7`; the count grows by one every seven versions; and the centres are
3482
- * evenly spaced with the *first* gap absorbing the rounding slack. Expressing
3483
- * that as arithmetic rather than a 40-row table means there is no table to
3484
- * mistype, and {@link validateTables} can then assert the shape of the result.
3485
- *
3486
- * @param {number} version
3487
- * @returns {number[]} Ascending centres. Empty for version 1.
3488
- */
3489
- function alignmentCoordinates(version) {
3490
- if (version < 2) return [];
3491
-
3492
- const size = versionSize(version);
3493
- const count = Math.floor(version / 7) + 2;
3494
- const last = size - 7;
3495
-
3496
- // Spacing is rounded up to an even number of modules so every centre lands on
3497
- // the same parity as the timing pattern, which is what keeps the patterns
3498
- // aligned with the module grid rather than straddling it.
3499
- const step = Math.ceil((size - 13) / (2 * count - 2)) * 2;
3500
-
3501
- const coords = [6];
3502
- // Walk backwards from the final centre so the slack lands in the first gap.
3503
- for (let i = count - 1; i >= 1; i--) coords.push(last - (count - 1 - i) * step);
3504
- coords.sort((a, b) => a - b);
3505
- return coords;
3515
+ const { ChecksumError, FormatError } = __require("core/errors.js");
3516
+ const { GF256_DM } = __require("core/galois-field.js");
3517
+ const { rsDecode } = __require("core/reed-solomon.js");
3518
+ const { SYMBOLS } = __require("datamatrix/tables.js");
3519
+
3520
+ const CW_PAD = 129;
3521
+ const CW_BASE256 = 231;
3522
+
3523
+ /** @param {object} entry @param {...string} names @returns {number | undefined} */
3524
+ function numberField(entry, ...names) {
3525
+ for (const name of names) if (Number.isInteger(entry[name])) return entry[name];
3526
+ return undefined;
3527
+ }
3528
+
3529
+ /** Normalize the public table entry into the decoder's geometry contract. */
3530
+ function layoutFor(width, height) {
3531
+ const entry = SYMBOLS.find((s) =>
3532
+ numberField(s, 'columns', 'cols', 'matrixColumns', 'width') === width &&
3533
+ numberField(s, 'rows', 'matrixRows', 'height') === height);
3534
+ if (!entry) throw new FormatError(`Data Matrix: ${width}x${height} is not an ECC 200 symbol size`);
3535
+
3536
+ const regionRows = numberField(entry, 'dataRegionRows', 'regionRows') ??
3537
+ (numberField(entry, 'regionHeight') ? numberField(entry, 'regionHeight') - 2 : undefined);
3538
+ const regionCols = numberField(entry, 'dataRegionColumns', 'dataRegionCols', 'regionColumns') ??
3539
+ (numberField(entry, 'regionWidth') ? numberField(entry, 'regionWidth') - 2 : undefined);
3540
+ const dataCount = numberField(entry, 'dataCodewords', 'dataCapacity');
3541
+ const eccCount = numberField(entry, 'errorCodewords', 'eccCodewords');
3542
+ const blockCount = numberField(entry, 'interleavedBlocks', 'interleavedBlockCount', 'blockCount', 'rsBlocks') || 1;
3543
+ if (!regionRows || !regionCols || dataCount === undefined || eccCount === undefined ||
3544
+ height % (regionRows + 2) || width % (regionCols + 2) || eccCount % blockCount) {
3545
+ throw new FormatError(`Data Matrix: invalid table layout for ${width}x${height}`);
3546
+ }
3547
+
3548
+ const rows = height / (regionRows + 2);
3549
+ const cols = width / (regionCols + 2);
3550
+ const blockData = Array.isArray(entry.blockDataCodewords) ? entry.blockDataCodewords.slice() :
3551
+ Array.isArray(entry.dataCodewordsPerBlock) ? entry.dataCodewordsPerBlock.slice() : null;
3552
+ let dataLengths;
3553
+ if (blockData) {
3554
+ dataLengths = blockData;
3555
+ } else {
3556
+ // The sole uneven ECC 200 distribution is 144x144: its first eight of ten
3557
+ // blocks contain one extra data codeword. This derives it instead of hiding
3558
+ // a magic size check in the deinterleaver.
3559
+ const short = Math.floor(dataCount / blockCount);
3560
+ dataLengths = new Array(blockCount).fill(short);
3561
+ for (let i = 0; i < dataCount % blockCount; i++) dataLengths[i]++;
3562
+ }
3563
+ if (dataLengths.length !== blockCount || dataLengths.reduce((a, b) => a + b, 0) !== dataCount) {
3564
+ throw new FormatError(`Data Matrix: inconsistent block layout for ${width}x${height}`);
3565
+ }
3566
+ return { entry, regionRows, regionCols, regionRowCount: rows, regionColCount: cols,
3567
+ dataRows: rows * regionRows, dataCols: cols * regionCols, dataCount, eccCount,
3568
+ blockCount, eccPerBlock: eccCount / blockCount, dataLengths };
3569
+ }
3570
+
3571
+ /** Remove the L/finders from every data region, retaining only placement modules. */
3572
+ function extractDataModules(matrix, layout) {
3573
+ const data = new Uint8Array(layout.dataRows * layout.dataCols);
3574
+ for (let regionY = 0; regionY < layout.regionRowCount; regionY++) {
3575
+ for (let regionX = 0; regionX < layout.regionColCount; regionX++) {
3576
+ const sourceX = regionX * (layout.regionCols + 2) + 1;
3577
+ const sourceY = regionY * (layout.regionRows + 2) + 1;
3578
+ for (let y = 0; y < layout.regionRows; y++) {
3579
+ const targetY = regionY * layout.regionRows + y;
3580
+ for (let x = 0; x < layout.regionCols; x++) {
3581
+ data[targetY * layout.dataCols + regionX * layout.regionCols + x] =
3582
+ matrix.get(sourceX + x, sourceY + y) ? 1 : 0;
3583
+ }
3584
+ }
3585
+ }
3586
+ }
3587
+ return data;
3506
3588
  }
3507
3589
 
3508
- /**
3509
- * Centres of the alignment patterns actually drawn, as [x, y] pairs.
3510
- *
3511
- * The three combinations that would sit on top of a finder pattern are omitted.
3512
- *
3513
- * @param {number} version
3514
- * @returns {Array<[number, number]>}
3515
- */
3516
- function alignmentCentres(version) {
3517
- const coords = alignmentCoordinates(version);
3518
- if (coords.length === 0) return [];
3519
-
3520
- const size = versionSize(version);
3521
- const lo = 6;
3522
- const hi = size - 7;
3523
- const out = [];
3524
- for (let i = 0; i < coords.length; i++) {
3525
- for (let j = 0; j < coords.length; j++) {
3526
- const x = coords[j];
3527
- const y = coords[i];
3528
- // Skip the three finder corners.
3529
- if (x === lo && y === lo) continue;
3530
- if (x === lo && y === hi) continue;
3531
- if (x === hi && y === lo) continue;
3532
- out.push([x, y]);
3590
+ /** Read placement codewords using the ECC 200 Utah sweep (the inverse writer path). */
3591
+ function readPlacement(modules, rows, cols, count) {
3592
+ const seen = new Uint8Array(rows * cols);
3593
+ const out = new Uint8Array(count);
3594
+ const get = (row, col) => modules[row * cols + col] !== 0;
3595
+ const module = (row, col) => {
3596
+ if (row < 0) { row += rows; col += 4 - ((rows + 4) % 8); }
3597
+ if (col < 0) { col += cols; row += 4 - ((cols + 4) % 8); }
3598
+ if (row < 0 || row >= rows || col < 0 || col >= cols) {
3599
+ throw new FormatError('Data Matrix: placement coordinate escaped data region');
3533
3600
  }
3534
- }
3601
+ seen[row * cols + col] = 1;
3602
+ return get(row, col) ? 1 : 0;
3603
+ };
3604
+ const bits = (coords) => coords.reduce((value, p) => (value << 1) | module(p[0], p[1]), 0);
3605
+ const utah = (row, col) => bits([[row - 2, col - 2], [row - 2, col - 1], [row - 1, col - 2], [row - 1, col - 1],
3606
+ [row - 1, col], [row, col - 2], [row, col - 1], [row, col]]);
3607
+ const corner1 = () => bits([[rows - 1, 0], [rows - 1, 1], [rows - 1, 2], [0, cols - 2], [0, cols - 1], [1, cols - 1], [2, cols - 1], [3, cols - 1]]);
3608
+ const corner2 = () => bits([[rows - 3, 0], [rows - 2, 0], [rows - 1, 0], [0, cols - 4], [0, cols - 3], [0, cols - 2], [0, cols - 1], [1, cols - 1]]);
3609
+ const corner3 = () => bits([[rows - 3, 0], [rows - 2, 0], [rows - 1, 0], [0, cols - 2], [0, cols - 1], [1, cols - 1], [2, cols - 1], [3, cols - 1]]);
3610
+ const corner4 = () => bits([[rows - 1, 0], [rows - 1, cols - 1], [0, cols - 3], [0, cols - 2], [0, cols - 1], [1, cols - 3], [1, cols - 2], [1, cols - 1]]);
3611
+
3612
+ let row = 4, col = 0, n = 0;
3613
+ const put = (value) => { if (n < count) out[n++] = value; };
3614
+ do {
3615
+ if (row === rows && col === 0) put(corner1());
3616
+ if (row === rows - 2 && col === 0 && cols % 4 !== 0) put(corner2());
3617
+ if (row === rows - 2 && col === 0 && cols % 8 === 4) put(corner3());
3618
+ if (row === rows + 4 && col === 2 && cols % 8 === 0) put(corner4());
3619
+ do { if (row < rows && col >= 0 && !seen[row * cols + col]) put(utah(row, col)); row -= 2; col += 2; } while (row >= 0 && col < cols);
3620
+ row += 1; col += 3;
3621
+ do { if (row >= 0 && col < cols && !seen[row * cols + col]) put(utah(row, col)); row += 2; col -= 2; } while (row < rows && col >= 0);
3622
+ row += 3; col += 1;
3623
+ } while (row < rows || col < cols);
3624
+ if (n !== count) throw new FormatError(`Data Matrix: placement yielded ${n}, expected ${count} codewords`);
3535
3625
  return out;
3536
3626
  }
3537
3627
 
3538
- const reservedCache = new Map();
3539
-
3540
- /**
3541
- * Map of modules that carry function patterns rather than payload.
3542
- *
3543
- * A set bit means "reserved": finder, separator, timing, alignment, format
3544
- * information, the dark module, and the version information blocks. This is the
3545
- * single source of truth used by the encoder to skip modules while laying out
3546
- * the bitstream, by the decoder to read them back in the same order, and by
3547
- * {@link geometricTotalCodewords} to count what is left.
3548
- *
3549
- * Deriving capacity this way rather than by hand arithmetic is what makes the
3550
- * awkward cases free: an alignment pattern that overlaps the timing pattern is
3551
- * counted once because it is the same set of modules, not because anyone
3552
- * remembered to subtract five.
3553
- *
3554
- * @param {number} version
3555
- * @returns {BitMatrix} Shared, cached treat as immutable.
3556
- */
3557
- function reservedModules(version) {
3558
- const cached = reservedCache.get(version);
3559
- if (cached) return cached;
3560
-
3561
- const size = versionSize(version);
3562
- const m = new BitMatrix(size, size);
3563
-
3564
- // Finder patterns with their separators: an 8x8 reserved block at each of
3565
- // three corners (7x7 pattern plus a one-module light border on the inner
3566
- // sides, which the corner blocks absorb).
3567
- m.setRegion(0, 0, 8, 8);
3568
- m.setRegion(size - 8, 0, 8, 8);
3569
- m.setRegion(0, size - 8, 8, 8);
3570
-
3571
- // Timing patterns, spanning the gap between the separators.
3572
- for (let i = 8; i < size - 8; i++) {
3573
- m.set(i, 6);
3574
- m.set(6, i);
3575
- }
3576
-
3577
- // Alignment patterns, 5x5 each.
3578
- const centres = alignmentCentres(version);
3579
- for (let i = 0; i < centres.length; i++) {
3580
- m.setRegion(centres[i][0] - 2, centres[i][1] - 2, 5, 5);
3628
+ /** Restore RS blocks, correct them, then concatenate their data portions. */
3629
+ function deinterleaveAndCorrect(codewords, layout) {
3630
+ if (codewords.length !== layout.dataCount + layout.eccCount) throw new FormatError('Data Matrix: codeword count mismatch');
3631
+ const blocks = layout.dataLengths.map((len) => new Uint8Array(len + layout.eccPerBlock));
3632
+ // Data codewords arrive in their original stream order. ECC 200 deals that
3633
+ // stream round-robin across the RS blocks, so the inverse is determined by
3634
+ // the wire index rather than by splitting it into consecutive block-sized
3635
+ // chunks. For 144x144, indices 1550..1557 naturally land in the eight long
3636
+ // blocks while the two short blocks remain at 155 data codewords.
3637
+ for (let i = 0; i < layout.dataCount; i++) {
3638
+ blocks[i % layout.blockCount][Math.floor(i / layout.blockCount)] = codewords[i];
3639
+ }
3640
+
3641
+ // Parity normally begins with block zero. The uneven 144x144 layout rotates
3642
+ // the parity wire order to begin with its first short block; derive the same
3643
+ // mapping from the declarative lengths instead of keying it to dimensions.
3644
+ const longest = Math.max(...layout.dataLengths);
3645
+ const firstShort = layout.dataLengths.findIndex((length) => length < longest);
3646
+ const rotation = firstShort < 0 ? 0 : firstShort;
3647
+ let at = layout.dataCount;
3648
+ for (let i = 0; i < layout.eccPerBlock; i++) {
3649
+ for (let slot = 0; slot < layout.blockCount; slot++) {
3650
+ const block = (slot + rotation) % layout.blockCount;
3651
+ blocks[block][layout.dataLengths[block] + i] = codewords[at++];
3652
+ }
3581
3653
  }
3582
3654
 
3583
- // Format information: two copies plus the dark module. The copies partly
3584
- // fall inside the 8x8 finder blocks already reserved; setting them again is
3585
- // harmless and keeps the intent explicit.
3586
- const [copyA, copyB] = formatInfoPositions(size);
3587
- for (let i = 0; i < 15; i++) {
3588
- m.set(copyA[i][0], copyA[i][1]);
3589
- m.set(copyB[i][0], copyB[i][1]);
3655
+ let corrections = 0;
3656
+ const data = new Uint8Array(layout.dataCount);
3657
+ for (let b = 0; b < blocks.length; b++) {
3658
+ corrections += rsDecode(blocks[b], layout.eccPerBlock, GF256_DM, 1);
3590
3659
  }
3591
- m.set(8, size - 8); // dark module
3592
-
3593
- // Version information, two 6x3 blocks.
3594
- if (version >= VERSION_INFO_MIN) {
3595
- m.setRegion(size - 11, 0, 3, 6);
3596
- m.setRegion(0, size - 11, 6, 3);
3660
+ // Rebuild the original high-level codeword stream after correction. Keeping
3661
+ // this in wire-index order is essential: concatenating block data passes
3662
+ // single-block round trips but scrambles every multi-block payload.
3663
+ for (let i = 0; i < layout.dataCount; i++) {
3664
+ data[i] = blocks[i % layout.blockCount][Math.floor(i / layout.blockCount)];
3597
3665
  }
3598
-
3599
- reservedCache.set(version, m);
3600
- return m;
3666
+ return { data, corrections };
3601
3667
  }
3602
3668
 
3603
- /**
3604
- * Module positions of the two format information copies.
3605
- *
3606
- * Index `i` in each array is bit `i` of the 15-bit format value, bit 0 being
3607
- * the least significant.
3608
- *
3609
- * CAVEAT WORTH READING: the *direction* of this numbering is the one thing in
3610
- * this file that a round-trip test cannot falsify. Encoder and decoder share
3611
- * these tables, so a mirrored layout would pass every test in the suite and
3612
- * fail only against a real scanner. The layout below is the standard one; both
3613
- * sides deliberately consume this single definition so there is no second place
3614
- * for the convention to drift.
3615
- *
3616
- * @param {number} size Modules per side.
3617
- * @returns {[Array<[number, number]>, Array<[number, number]>]} [copyA, copyB]
3618
- */
3619
- function formatInfoPositions(size) {
3620
- /** @type {Array<[number, number]>} */
3621
- const a = [];
3622
- /** @type {Array<[number, number]>} */
3623
- const b = [];
3624
-
3625
- for (let i = 0; i < 15; i++) {
3626
- // Copy A wraps the top-left finder: down column 8, then left along row 8,
3627
- // stepping over the two timing modules.
3628
- if (i < 6) a.push([8, i]);
3629
- else if (i === 6) a.push([8, 7]);
3630
- else if (i === 7) a.push([8, 8]);
3631
- else if (i === 8) a.push([7, 8]);
3632
- else a.push([14 - i, 8]);
3633
-
3634
- // Copy B is split: the low bits run right-to-left along row 8 beside the
3635
- // top-right finder, the high bits run bottom-up beside the bottom-left one.
3636
- if (i < 8) b.push([size - 1 - i, 8]);
3637
- else b.push([8, size - 15 + i]);
3638
- }
3639
-
3640
- return [a, b];
3669
+ function unrandomize(value, position) {
3670
+ const pseudo = ((149 * position) % 255) + 1;
3671
+ return value - pseudo >= 0 ? value - pseudo : value - pseudo + 256;
3641
3672
  }
3642
3673
 
3643
- /**
3644
- * Modules available to data and error correction, counted off the grid.
3645
- *
3646
- * @param {number} version
3647
- * @returns {number}
3648
- */
3649
- function freeModuleCount(version) {
3650
- const reserved = reservedModules(version);
3651
- const size = versionSize(version);
3652
- let free = 0;
3653
- for (let y = 0; y < size; y++) {
3654
- for (let x = 0; x < size; x++) {
3655
- if (!reserved.get(x, y)) free++;
3674
+ /** Decode ASCII plus Base 256, preserving semantic bytes alongside text. */
3675
+ function parseData(data) {
3676
+ let text = '';
3677
+ const bytes = [];
3678
+ let upperShift = false;
3679
+ let gs1 = false;
3680
+ for (let i = 0; i < data.length;) {
3681
+ const cw = data[i++];
3682
+ if (cw === CW_PAD) break;
3683
+ if (cw <= 128) {
3684
+ const value = cw - 1 + (upperShift ? 128 : 0);
3685
+ upperShift = false;
3686
+ text += String.fromCharCode(value); bytes.push(value); continue;
3687
+ }
3688
+ if (cw <= 229) {
3689
+ const pair = cw - 130;
3690
+ const digits = String(pair).padStart(2, '0'); text += digits; bytes.push(digits.charCodeAt(0), digits.charCodeAt(1)); continue;
3691
+ }
3692
+ if (cw === 232) {
3693
+ if (i === 1) gs1 = true;
3694
+ else { text += '\x1d'; bytes.push(29); }
3695
+ continue;
3696
+ }
3697
+ if (cw === 235) { upperShift = true; continue; }
3698
+ if (cw === CW_BASE256) {
3699
+ if (i >= data.length) throw new FormatError('Data Matrix: Base 256 length is missing');
3700
+ let length = unrandomize(data[i], i + 1); i++;
3701
+ if (length === 0) length = data.length - i;
3702
+ else if (length >= 250) {
3703
+ if (i >= data.length) throw new FormatError('Data Matrix: Base 256 extended length is missing');
3704
+ length = 250 * (length - 249) + unrandomize(data[i], i + 1); i++;
3705
+ }
3706
+ if (i + length > data.length) throw new FormatError('Data Matrix: Base 256 segment exceeds data capacity');
3707
+ const segment = new Uint8Array(length);
3708
+ for (let n = 0; n < length; n++, i++) segment[n] = unrandomize(data[i], i + 1);
3709
+ bytes.push(...segment);
3710
+ for (let n = 0; n < segment.length; n++) text += String.fromCharCode(segment[n]);
3711
+ continue;
3656
3712
  }
3713
+ throw new FormatError(`Data Matrix: unsupported encoding codeword ${cw}`);
3657
3714
  }
3658
- return free;
3659
- }
3660
-
3661
- /**
3662
- * Total codewords (data + error correction) a version holds.
3663
- *
3664
- * Geometric, not tabulated — this is the reference the ECC table is checked
3665
- * against.
3666
- *
3667
- * @param {number} version
3668
- * @returns {number}
3669
- */
3670
- function geometricTotalCodewords(version) {
3671
- return Math.floor(freeModuleCount(version) / 8);
3715
+ return { text, bytes: Uint8Array.from(bytes), gs1 };
3672
3716
  }
3673
3717
 
3674
3718
  /**
3675
- * Bits left over after the last whole codeword, written as zeroes.
3719
+ * Decode an upright, sampled Data Matrix ECC 200 symbol.
3676
3720
  *
3677
- * @param {number} version
3678
- * @returns {number} 0, 3, 4 or 7.
3721
+ * @param {import('../core/bit-matrix.js').BitMatrix} matrix Full symbol, no quiet zone.
3722
+ * @returns {{text: string, bytes: Uint8Array, correctedErrors: number, symbol: object}}
3679
3723
  */
3680
- function remainderBits(version) {
3681
- return freeModuleCount(version) % 8;
3724
+ function decodeDataMatrix(matrix) {
3725
+ if (!matrix || !Number.isInteger(matrix.width) || !Number.isInteger(matrix.height)) throw new FormatError('Data Matrix: no matrix supplied');
3726
+ const layout = layoutFor(matrix.width, matrix.height);
3727
+ const placement = readPlacement(extractDataModules(matrix, layout), layout.dataRows, layout.dataCols, layout.dataCount + layout.eccCount);
3728
+ const { data, corrections } = deinterleaveAndCorrect(placement, layout);
3729
+ const result = parseData(data);
3730
+ return { ...result, corrections, correctedErrors: corrections, symbol: layout.entry };
3682
3731
  }
3732
+ __exports.ChecksumError = ChecksumError;
3683
3733
 
3684
- const orderCache = new Map();
3734
+ __exports.decodeDataMatrix = decodeDataMatrix;
3735
+ };
3685
3736
 
3737
+ __modules["image/perspective.js"] = function (__require, __exports) {
3686
3738
  /**
3687
- * Module positions in bitstream order, as interleaved x, y pairs.
3739
+ * Projective (perspective) transforms.
3688
3740
  *
3689
- * The layout walks two-module-wide columns from the bottom-right corner
3690
- * leftward, alternating upward and downward, right module of the pair before
3691
- * the left, skipping the vertical timing column and every reserved module.
3741
+ * A 2D symbol photographed off-axis is not a rotated square — it is a
3742
+ * quadrilateral with converging edges. Correcting that needs a full projective
3743
+ * map, not an affine one; an affine approximation reads the near edge of a
3744
+ * tilted symbol correctly and drifts a module or more by the far edge.
3692
3745
  *
3693
- * Encoder and decoder both consume this one function. That is not tidiness: a
3694
- * placement order that disagrees between the two would still round-trip
3695
- * perfectly within this library while producing symbols no scanner can read.
3696
- * There is only one order because there is only one implementation of it.
3746
+ * The map is a 3x3 homogeneous matrix. Points are transformed as
3747
+ * (x, y, 1) * M, then divided through by the resulting w.
3697
3748
  *
3698
- * @param {number} version
3699
- * @returns {Int32Array} Shared, cached — treat as immutable. Length is
3700
- * `2 * freeModuleCount(version)`.
3749
+ * @module image/perspective
3701
3750
  */
3702
- function dataModuleOrder(version) {
3703
- const cached = orderCache.get(version);
3704
- if (cached) return cached;
3705
-
3706
- const size = versionSize(version);
3707
- const reserved = reservedModules(version);
3708
- const out = new Int32Array(freeModuleCount(version) * 2);
3709
- let n = 0;
3751
+ class PerspectiveTransform {
3752
+ /* eslint-disable-next-line max-params */
3753
+ constructor(a11, a21, a31, a12, a22, a32, a13, a23, a33) {
3754
+ this.a11 = a11; this.a21 = a21; this.a31 = a31;
3755
+ this.a12 = a12; this.a22 = a22; this.a32 = a32;
3756
+ this.a13 = a13; this.a23 = a23; this.a33 = a33;
3757
+ }
3710
3758
 
3711
- let upward = true;
3712
- for (let col = size - 1; col > 0; col -= 2) {
3713
- // Column 6 is the vertical timing pattern. Stepping over it shifts the
3714
- // whole remaining schedule left by one, so the loop variable itself has to
3715
- // move adjusting only the current pair would visit column 4 twice and
3716
- // column 0 never, which is self-consistent between encoder and decoder and
3717
- // therefore invisible to a round-trip test.
3718
- if (col === 6) col--;
3719
- for (let i = 0; i < size; i++) {
3720
- const y = upward ? size - 1 - i : i;
3759
+ /**
3760
+ * Transform points in place.
3761
+ *
3762
+ * @param {Float32Array | number[]} points Interleaved [x0, y0, x1, y1, ...].
3763
+ * @returns {Float32Array | number[]} The same array.
3764
+ */
3765
+ transform(points) {
3766
+ const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
3767
+ for (let i = 0; i < points.length; i += 2) {
3768
+ const x = points[i];
3769
+ const y = points[i + 1];
3770
+ const w = a13 * x + a23 * y + a33;
3771
+ points[i] = (a11 * x + a21 * y + a31) / w;
3772
+ points[i + 1] = (a12 * x + a22 * y + a32) / w;
3773
+ }
3774
+ return points;
3775
+ }
3776
+
3777
+ /**
3778
+ * Transform a single point.
3779
+ *
3780
+ * @param {number} x @param {number} y
3781
+ * @returns {{x: number, y: number}}
3782
+ */
3783
+ transformPoint(x, y) {
3784
+ const w = this.a13 * x + this.a23 * y + this.a33;
3785
+ return {
3786
+ x: (this.a11 * x + this.a21 * y + this.a31) / w,
3787
+ y: (this.a12 * x + this.a22 * y + this.a32) / w,
3788
+ };
3789
+ }
3790
+
3791
+ /**
3792
+ * Map the unit square — (0,0), (1,0), (1,1), (0,1) — onto an arbitrary quad.
3793
+ *
3794
+ * Corners are given in that same order, i.e. going around the quad, not
3795
+ * as opposite pairs.
3796
+ *
3797
+ * @returns {PerspectiveTransform}
3798
+ */
3799
+ /* eslint-disable-next-line max-params */
3800
+ static squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3) {
3801
+ const dx3 = x0 - x1 + x2 - x3;
3802
+ const dy3 = y0 - y1 + y2 - y3;
3803
+
3804
+ if (dx3 === 0 && dy3 === 0) {
3805
+ // The quad is a parallelogram, so the map is affine and the projective
3806
+ // terms vanish. Worth special-casing: it is the common case for flat
3807
+ // scans, and the general solution divides by zero here.
3808
+ return new PerspectiveTransform(
3809
+ x1 - x0, x2 - x1, x0,
3810
+ y1 - y0, y2 - y1, y0,
3811
+ 0, 0, 1
3812
+ );
3813
+ }
3814
+
3815
+ const dx1 = x1 - x2;
3816
+ const dx2 = x3 - x2;
3817
+ const dy1 = y1 - y2;
3818
+ const dy2 = y3 - y2;
3819
+ const denominator = dx1 * dy2 - dx2 * dy1;
3820
+ const a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
3821
+ const a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
3822
+
3823
+ return new PerspectiveTransform(
3824
+ x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0,
3825
+ y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0,
3826
+ a13, a23, 1
3827
+ );
3828
+ }
3829
+
3830
+ /**
3831
+ * Map an arbitrary quad onto the unit square — the inverse of
3832
+ * {@link squareToQuad}, via the adjugate.
3833
+ *
3834
+ * @returns {PerspectiveTransform}
3835
+ */
3836
+ /* eslint-disable-next-line max-params */
3837
+ static quadToSquare(x0, y0, x1, y1, x2, y2, x3, y3) {
3838
+ return PerspectiveTransform.squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3).inverse();
3839
+ }
3840
+
3841
+ /**
3842
+ * Map one quad onto another, corner for corner.
3843
+ *
3844
+ * This is what turns four detected finder corners into a sampling grid:
3845
+ * compose "detected quad -> unit square" with "unit square -> ideal grid".
3846
+ *
3847
+ * @returns {PerspectiveTransform}
3848
+ */
3849
+ /* eslint-disable-next-line max-params */
3850
+ static quadToQuad(
3851
+ sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3,
3852
+ dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3
3853
+ ) {
3854
+ const toSquare = PerspectiveTransform.quadToSquare(sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
3855
+ const toQuad = PerspectiveTransform.squareToQuad(dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3);
3856
+ return toSquare.times(toQuad);
3857
+ }
3858
+
3859
+ /**
3860
+ * Adjugate — the inverse up to a scale factor, which is irrelevant in
3861
+ * homogeneous coordinates because the division by w cancels it.
3862
+ *
3863
+ * @returns {PerspectiveTransform}
3864
+ */
3865
+ inverse() {
3866
+ const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
3867
+ return new PerspectiveTransform(
3868
+ a22 * a33 - a23 * a32,
3869
+ a23 * a31 - a21 * a33,
3870
+ a21 * a32 - a22 * a31,
3871
+ a13 * a32 - a12 * a33,
3872
+ a11 * a33 - a13 * a31,
3873
+ a12 * a31 - a11 * a32,
3874
+ a12 * a23 - a13 * a22,
3875
+ a13 * a21 - a11 * a23,
3876
+ a11 * a22 - a12 * a21
3877
+ );
3878
+ }
3879
+
3880
+ /**
3881
+ * Matrix product: apply `this` first, then `other`.
3882
+ *
3883
+ * @param {PerspectiveTransform} other
3884
+ * @returns {PerspectiveTransform}
3885
+ */
3886
+ times(other) {
3887
+ const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
3888
+ const o = other;
3889
+ return new PerspectiveTransform(
3890
+ o.a11 * a11 + o.a21 * a12 + o.a31 * a13,
3891
+ o.a11 * a21 + o.a21 * a22 + o.a31 * a23,
3892
+ o.a11 * a31 + o.a21 * a32 + o.a31 * a33,
3893
+ o.a12 * a11 + o.a22 * a12 + o.a32 * a13,
3894
+ o.a12 * a21 + o.a22 * a22 + o.a32 * a23,
3895
+ o.a12 * a31 + o.a22 * a32 + o.a32 * a33,
3896
+ o.a13 * a11 + o.a23 * a12 + o.a33 * a13,
3897
+ o.a13 * a21 + o.a23 * a22 + o.a33 * a23,
3898
+ o.a13 * a31 + o.a23 * a32 + o.a33 * a33
3899
+ );
3900
+ }
3901
+ }
3902
+
3903
+ __exports.PerspectiveTransform = PerspectiveTransform;
3904
+ };
3905
+
3906
+ __modules["image/grid-sampler.js"] = function (__require, __exports) {
3907
+ /**
3908
+ * Resample a distorted symbol in the image into an upright module grid.
3909
+ *
3910
+ * Given a transform that maps grid coordinates to image coordinates, this
3911
+ * samples the centre of every module. Sampling centres rather than averaging
3912
+ * whole cells is deliberate: module edges are where blur and bleed live, and
3913
+ * including them turns a marginal symbol into an unreadable one.
3914
+ *
3915
+ * @module image/grid-sampler
3916
+ */
3917
+ const { BitMatrix } = __require("core/bit-matrix.js");
3918
+ const { NotFoundError } = __require("core/errors.js");
3919
+ const { PerspectiveTransform } = __require("image/perspective.js");
3920
+
3921
+ /**
3922
+ * Sample a `dimension` x `dimension` grid (or `width` x `height`).
3923
+ *
3924
+ * @param {BitMatrix} image Binarized source image.
3925
+ * @param {number} width Modules across.
3926
+ * @param {number} height Modules down.
3927
+ * @param {PerspectiveTransform} transform Grid space -> image space.
3928
+ * @returns {BitMatrix}
3929
+ * @throws {NotFoundError} If the grid falls outside the image.
3930
+ */
3931
+ function sampleGrid(image, width, height, transform) {
3932
+ const out = new BitMatrix(width, height);
3933
+ const points = new Float32Array(width * 2);
3934
+
3935
+ for (let y = 0; y < height; y++) {
3936
+ // Module centres: offset by half a module in both axes.
3937
+ const gridY = y + 0.5;
3938
+ for (let x = 0; x < width; x++) {
3939
+ points[x * 2] = x + 0.5;
3940
+ points[x * 2 + 1] = gridY;
3941
+ }
3942
+ transform.transform(points);
3943
+
3944
+ for (let x = 0; x < width; x++) {
3945
+ const px = points[x * 2] | 0;
3946
+ const py = points[x * 2 + 1] | 0;
3947
+ if (px < 0 || py < 0 || px >= image.width || py >= image.height) {
3948
+ throw new NotFoundError(
3949
+ `Sampling grid escapes the image at module (${x}, ${y})`
3950
+ );
3951
+ }
3952
+ if (image.get(px, py)) out.set(x, y);
3953
+ }
3954
+ }
3955
+
3956
+ return out;
3957
+ }
3958
+
3959
+ /**
3960
+ * Sample with a 3x3 majority vote per module.
3961
+ *
3962
+ * Slower, and worth it when a single-point sample lands on a speck of noise or
3963
+ * a JPEG artefact. Readers fall back to this after a clean sample fails to
3964
+ * decode, rather than paying for it on every attempt.
3965
+ *
3966
+ * @param {BitMatrix} image
3967
+ * @param {number} width
3968
+ * @param {number} height
3969
+ * @param {PerspectiveTransform} transform
3970
+ * @returns {BitMatrix}
3971
+ */
3972
+ function sampleGridVoting(image, width, height, transform) {
3973
+ const out = new BitMatrix(width, height);
3974
+
3975
+ // Spacing between module centres, measured in image pixels, so the vote
3976
+ // spreads across the module rather than a fixed pixel radius that would be
3977
+ // meaningless at a different scale.
3978
+ const p0 = transform.transformPoint(0.5, 0.5);
3979
+ const p1 = transform.transformPoint(1.5, 0.5);
3980
+ const p2 = transform.transformPoint(0.5, 1.5);
3981
+ const stepX = Math.hypot(p1.x - p0.x, p1.y - p0.y);
3982
+ const stepY = Math.hypot(p2.x - p0.x, p2.y - p0.y);
3983
+ const rx = Math.max(1, Math.round(stepX / 4));
3984
+ const ry = Math.max(1, Math.round(stepY / 4));
3985
+
3986
+ for (let y = 0; y < height; y++) {
3987
+ for (let x = 0; x < width; x++) {
3988
+ const c = transform.transformPoint(x + 0.5, y + 0.5);
3989
+ const cx = c.x | 0;
3990
+ const cy = c.y | 0;
3991
+ if (cx < 0 || cy < 0 || cx >= image.width || cy >= image.height) {
3992
+ throw new NotFoundError(
3993
+ `Sampling grid escapes the image at module (${x}, ${y})`
3994
+ );
3995
+ }
3996
+
3997
+ let dark = 0;
3998
+ let total = 0;
3999
+ for (let dy = -1; dy <= 1; dy++) {
4000
+ for (let dx = -1; dx <= 1; dx++) {
4001
+ const sx = cx + dx * rx;
4002
+ const sy = cy + dy * ry;
4003
+ if (sx < 0 || sy < 0 || sx >= image.width || sy >= image.height) continue;
4004
+ total++;
4005
+ if (image.get(sx, sy)) dark++;
4006
+ }
4007
+ }
4008
+ if (total > 0 && dark * 2 > total) out.set(x, y);
4009
+ }
4010
+ }
4011
+
4012
+ return out;
4013
+ }
4014
+
4015
+ /**
4016
+ * Build the transform for a symbol whose four corners are known, and sample it.
4017
+ *
4018
+ * Corners are in reading order: top-left, top-right, bottom-right, bottom-left.
4019
+ *
4020
+ * @param {BitMatrix} image
4021
+ * @param {number} dimension Modules per side.
4022
+ * @param {Array<{x: number, y: number}>} corners
4023
+ * @param {boolean} [voting]
4024
+ * @returns {BitMatrix}
4025
+ */
4026
+ function sampleQuad(image, dimension, corners, voting = false) {
4027
+ if (corners.length !== 4) throw new NotFoundError('sampleQuad needs exactly 4 corners');
4028
+ const [tl, tr, br, bl] = corners;
4029
+ const d = dimension;
4030
+
4031
+ const transform = PerspectiveTransform.quadToQuad(
4032
+ 0, 0, d, 0, d, d, 0, d,
4033
+ tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y
4034
+ );
4035
+
4036
+ return voting
4037
+ ? sampleGridVoting(image, d, d, transform)
4038
+ : sampleGrid(image, d, d, transform);
4039
+ }
4040
+
4041
+ __exports.sampleGrid = sampleGrid;
4042
+ __exports.sampleGridVoting = sampleGridVoting;
4043
+ __exports.sampleQuad = sampleQuad;
4044
+ };
4045
+
4046
+ __modules["datamatrix/detector.js"] = function (__require, __exports) {
4047
+ /**
4048
+ * Data Matrix ECC 200 detection in a binarized image.
4049
+ *
4050
+ * An ECC 200 symbol is distinguished by two neighbouring solid finder borders
4051
+ * (the L) and two alternating clock borders. The detector first finds dark
4052
+ * connected components, then scores every legal ECC 200 size and every
4053
+ * quarter-turn of the component's bounding quadrilateral against those four
4054
+ * borders. This deliberately verifies the complete border rather than merely
4055
+ * looking for an L: ordinary text and table rules produce L shapes often.
4056
+ *
4057
+ * The resulting quadrilateral is sampled back into the canonical orientation:
4058
+ * solid borders at left and bottom. It is intentionally independent of the
4059
+ * payload decoder, so geometry can be used by callers that need the matrix.
4060
+ *
4061
+ * @module datamatrix/detector
4062
+ */
4063
+ const { NotFoundError } = __require("core/errors.js");
4064
+ const { sampleGrid, sampleQuad } = __require("image/grid-sampler.js");
4065
+ const { PerspectiveTransform } = __require("image/perspective.js");
4066
+ const { decodeDataMatrix } = __require("datamatrix/decoder.js");
4067
+
4068
+ // ECC 200 dimensions. DMRE is deliberately not included: it uses a separate
4069
+ // size table and is not part of the original ECC 200 family implemented here.
4070
+ const SIZES = [
4071
+ [10, 10], [12, 12], [14, 14], [16, 16], [18, 18], [20, 20], [22, 22], [24, 24], [26, 26],
4072
+ [32, 32], [36, 36], [40, 40], [44, 44], [48, 48], [52, 52], [64, 64], [72, 72], [80, 80],
4073
+ [88, 88], [96, 96], [104, 104], [120, 120], [132, 132], [144, 144],
4074
+ [18, 8], [32, 8], [26, 12], [36, 12], [36, 16], [48, 16],
4075
+ ];
4076
+
4077
+ /** @typedef {{x:number, y:number}} Point */
4078
+ /** @typedef {{corners: Point[], dimension: number, width: number, height: number, moduleSize: number, matrix: import('../core/bit-matrix.js').BitMatrix}} Detection */
4079
+
4080
+ function dark(image, x, y) {
4081
+ return image.get(Math.max(0, Math.min(image.width - 1, Math.round(x))),
4082
+ Math.max(0, Math.min(image.height - 1, Math.round(y))));
4083
+ }
4084
+
4085
+ /** Return components which are large enough to plausibly contain a symbol. */
4086
+ function components(image) {
4087
+ const seen = new Uint8Array(image.width * image.height);
4088
+ const out = [];
4089
+ const push = (x, y, xs, ys) => { xs.push(x); ys.push(y); };
4090
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) {
4091
+ const start = y * image.width + x;
4092
+ if (seen[start] || !image.get(x, y)) continue;
4093
+ const xs = [x], ys = [y]; seen[start] = 1;
4094
+ let head = 0, minX = x, maxX = x, minY = y, maxY = y;
4095
+ while (head < xs.length) {
4096
+ const px = xs[head], py = ys[head++];
4097
+ if (px < minX) minX = px; if (px > maxX) maxX = px;
4098
+ if (py < minY) minY = py; if (py > maxY) maxY = py;
4099
+ for (const [nx, ny] of [[px - 1, py], [px + 1, py], [px, py - 1], [px, py + 1]]) {
4100
+ if (nx < 0 || ny < 0 || nx >= image.width || ny >= image.height) continue;
4101
+ const at = ny * image.width + nx;
4102
+ if (!seen[at] && image.get(nx, ny)) { seen[at] = 1; push(nx, ny, xs, ys); }
4103
+ }
4104
+ }
4105
+ if (maxX - minX >= 7 && maxY - minY >= 7) out.push({ minX, minY, maxX, maxY, pixels: xs.length });
4106
+ }
4107
+ return out.sort((a, b) => b.pixels - a.pixels).slice(0, 40);
4108
+ }
4109
+
4110
+ /** Sample a physical edge at module centres. */
4111
+ function edge(image, a, b, count) {
4112
+ const values = [];
4113
+ for (let i = 0; i < count; i++) {
4114
+ const t = (i + 0.5) / count;
4115
+ values.push(dark(image, a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
4116
+ }
4117
+ return values;
4118
+ }
4119
+
4120
+ function solidScore(values) {
4121
+ let n = 0; for (const value of values) if (value) n++;
4122
+ return n / values.length;
4123
+ }
4124
+
4125
+ function clockScore(values, startsDark) {
4126
+ let n = 0;
4127
+ for (let i = 0; i < values.length; i++) if (values[i] === ((i & 1) === 0 ? startsDark : !startsDark)) n++;
4128
+ return n / values.length;
4129
+ }
4130
+
4131
+ /** Count light/dark changes along a physical edge at approximately one-pixel intervals. */
4132
+ function edgeTransitions(image, a, b) {
4133
+ const steps = Math.max(1, Math.ceil(Math.hypot(b.x - a.x, b.y - a.y)));
4134
+ let previous = dark(image, a.x, a.y);
4135
+ let changes = 0;
4136
+ for (let i = 1; i <= steps; i++) {
4137
+ const t = i / steps;
4138
+ const value = dark(image, a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
4139
+ if (value !== previous) changes++;
4140
+ previous = value;
4141
+ }
4142
+ return changes;
4143
+ }
4144
+
4145
+ /** Reject a smaller harmonic whose module-centre samples happen to alternate. */
4146
+ function transitionCountFits(observed, modules) {
4147
+ const expected = modules - 1;
4148
+ const tolerance = Math.max(2, Math.floor(expected * 0.08));
4149
+ return Math.abs(observed - expected) <= tolerance;
4150
+ }
4151
+
4152
+ function sample(image, width, height, corners, voting) {
4153
+ if (width === height) return sampleQuad(image, width, corners, voting);
4154
+ const [tl, tr, br, bl] = corners;
4155
+ const transform = PerspectiveTransform.quadToQuad(0, 0, width, 0, width, height, 0, height,
4156
+ tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y);
4157
+ return sampleGrid(image, width, height, transform);
4158
+ }
4159
+
4160
+ /**
4161
+ * Find Data Matrix symbols in a binarized image.
4162
+ *
4163
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
4164
+ * @returns {Detection | null} The strongest candidate, or null when absent.
4165
+ */
4166
+ function detectDataMatrix(binaryImage) {
4167
+ if (!binaryImage || !binaryImage.width || !binaryImage.height) {
4168
+ throw new NotFoundError('detectDataMatrix: no image supplied');
4169
+ }
4170
+ const detections = [];
4171
+ const used = new Set();
4172
+ for (const box of components(binaryImage)) {
4173
+ const base = [
4174
+ { x: box.minX, y: box.minY }, { x: box.maxX + 1, y: box.minY },
4175
+ { x: box.maxX + 1, y: box.maxY + 1 }, { x: box.minX, y: box.maxY + 1 },
4176
+ ];
4177
+ // Profile the actual ink, rather than the outer sampling quadrilateral:
4178
+ // its far x/y boundary lies one pixel beyond the last dark pixel.
4179
+ const ink = [
4180
+ { x: box.minX, y: box.minY }, { x: box.maxX, y: box.minY },
4181
+ { x: box.maxX, y: box.maxY }, { x: box.minX, y: box.maxY },
4182
+ ];
4183
+ for (const [w, h] of SIZES) for (let turn = 0; turn < 4; turn++) {
4184
+ // A 90 degree turn swaps physical width and height.
4185
+ const physicalW = (turn & 1) ? h : w, physicalH = (turn & 1) ? w : h;
4186
+ const pitchX = (box.maxX - box.minX + 1) / physicalW;
4187
+ const pitchY = (box.maxY - box.minY + 1) / physicalH;
4188
+ if (Math.min(pitchX, pitchY) < 1 || Math.abs(pitchX - pitchY) > Math.max(pitchX, pitchY) * 0.22) continue;
4189
+ const corners = base.slice(turn).concat(base.slice(0, turn));
4190
+ const profile = ink.slice(turn).concat(ink.slice(0, turn));
4191
+ // Canonical edge order: top clock, right clock, bottom solid, left solid.
4192
+ const top = edge(binaryImage, profile[0], profile[1], w);
4193
+ const right = edge(binaryImage, profile[1], profile[2], h);
4194
+ const bottom = edge(binaryImage, profile[2], profile[3], w);
4195
+ const left = edge(binaryImage, profile[3], profile[0], h);
4196
+ // Sampling only the proposed module centres aliases exact harmonics: an
4197
+ // 80-module clock border, for example, can look like a perfect 16-module
4198
+ // border. Count transitions at image-pixel resolution as an independent
4199
+ // dimension measurement before accepting the candidate.
4200
+ if (!transitionCountFits(edgeTransitions(binaryImage, profile[0], profile[1]), w) ||
4201
+ !transitionCountFits(edgeTransitions(binaryImage, profile[1], profile[2]), h)) continue;
4202
+ // The top clock starts dark at the solid left border. The right clock is
4203
+ // anchored dark at the solid bottom border instead, so its top phase
4204
+ // depends on the symbol height (all ECC 200 heights are even and
4205
+ // therefore start light).
4206
+ const score = (clockScore(top, true) + clockScore(right, (h & 1) === 1) +
4207
+ solidScore(bottom) + solidScore(left)) / 4;
4208
+ if (score < 0.88) continue;
4209
+ const key = `${box.minX},${box.minY},${box.maxX},${box.maxY}`;
4210
+ if (used.has(key)) continue;
4211
+ let matrix;
4212
+ try { matrix = sample(binaryImage, w, h, corners, false); } catch (e) { continue; }
4213
+ used.add(key);
4214
+ detections.push({ corners, dimension: w === h ? w : 0, width: w, height: h,
4215
+ moduleSize: (pitchX + pitchY) / 2, matrix, score });
4216
+ }
4217
+ }
4218
+ detections.sort((a, b) => b.score - a.score || b.moduleSize - a.moduleSize);
4219
+ return detections[0] ?? null;
4220
+ }
4221
+
4222
+ /**
4223
+ * Detect and decode Data Matrix symbols. Detection failure is normal for an
4224
+ * image without a symbol, so candidates that cannot decode are skipped.
4225
+ *
4226
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
4227
+ * @returns {(import('./decoder.js').DecodeResult & {corners: Point[]}) | null}
4228
+ */
4229
+ function detectAndDecodeDataMatrix(binaryImage) {
4230
+ let detection;
4231
+ try { detection = detectDataMatrix(binaryImage); } catch (e) { return null; }
4232
+ if (!detection) return null;
4233
+ for (const voting of [false, true]) {
4234
+ let matrix = detection.matrix;
4235
+ try { if (voting) matrix = sample(binaryImage, detection.width, detection.height, detection.corners, true); } catch (e) { continue; }
4236
+ try { return Object.assign({ corners: detection.corners }, decodeDataMatrix(matrix)); }
4237
+ catch (e) { /* A geometric candidate is not necessarily a symbol. */ }
4238
+ }
4239
+ return null;
4240
+ }
4241
+
4242
+ __exports.detectDataMatrix = detectDataMatrix;
4243
+ __exports.detectAndDecodeDataMatrix = detectAndDecodeDataMatrix;
4244
+ };
4245
+
4246
+ __modules["datamatrix/index.js"] = function (__require, __exports) {
4247
+ /** Data Matrix ECC 200 entry points. @module datamatrix */
4248
+ const __reexport0 = __require("datamatrix/encoder.js"); __exports.encodeDataMatrix = __reexport0.encodeDataMatrix; __exports.encodeDataMatrixCodewords = __reexport0.encodeDataMatrixCodewords;
4249
+ const __reexport1 = __require("datamatrix/decoder.js"); __exports.decodeDataMatrix = __reexport1.decodeDataMatrix;
4250
+ const __reexport2 = __require("datamatrix/detector.js"); __exports.detectDataMatrix = __reexport2.detectDataMatrix; __exports.detectAndDecodeDataMatrix = __reexport2.detectAndDecodeDataMatrix;
4251
+ const __reexport3 = __require("datamatrix/tables.js"); __exports.DATAMATRIX_SYMBOLS = __reexport3.DATAMATRIX_SYMBOLS; __exports.SYMBOLS = __reexport3.SYMBOLS; __exports.symbolForDataCodewords = __reexport3.symbolForDataCodewords; __exports.validateDataMatrixTables = __reexport3.validateDataMatrixTables; __exports.validateTables = __reexport3.validateTables;
4252
+
4253
+
4254
+ };
4255
+
4256
+ __modules["core/bit-buffer.js"] = function (__require, __exports) {
4257
+ /**
4258
+ * Bit-level writing and reading, MSB-first.
4259
+ *
4260
+ * Every 2D symbology serialises its payload as a bitstream that does not
4261
+ * respect byte boundaries — QR alone mixes 4-bit mode indicators, 10-bit
4262
+ * character-count fields and 11-bit alphanumeric pairs. These two classes are
4263
+ * the write and read halves of that.
4264
+ *
4265
+ * @module core/bit-buffer
4266
+ */
4267
+ const { FormatError } = __require("core/errors.js");
4268
+
4269
+ /** Growable MSB-first bit writer. */
4270
+ class BitWriter {
4271
+ constructor() {
4272
+ /** @type {number[]} Packed bytes; the last one may be partially filled. */
4273
+ this.bytes = [];
4274
+ this.bitLength = 0;
4275
+ }
4276
+
4277
+ /** @returns {number} Bits written so far. */
4278
+ get length() {
4279
+ return this.bitLength;
4280
+ }
4281
+
4282
+ /**
4283
+ * Append the low `count` bits of `value`, most significant first.
4284
+ *
4285
+ * @param {number} value
4286
+ * @param {number} count
4287
+ */
4288
+ put(value, count) {
4289
+ for (let i = count - 1; i >= 0; i--) {
4290
+ this.putBit(((value >>> i) & 1) === 1);
4291
+ }
4292
+ }
4293
+
4294
+ /** @param {boolean} bit */
4295
+ putBit(bit) {
4296
+ const idx = this.bitLength >>> 3;
4297
+ if (this.bytes.length <= idx) this.bytes.push(0);
4298
+ if (bit) this.bytes[idx] |= 0x80 >>> (this.bitLength & 7);
4299
+ this.bitLength++;
4300
+ }
4301
+
4302
+ /** @param {ArrayLike<number>} data */
4303
+ putBytes(data) {
4304
+ for (let i = 0; i < data.length; i++) this.put(data[i], 8);
4305
+ }
4306
+
4307
+ /** Pad with zero bits until the length is a multiple of 8. */
4308
+ padToByte() {
4309
+ while (this.bitLength & 7) this.putBit(false);
4310
+ }
4311
+
4312
+ /**
4313
+ * @returns {Uint8Array} Byte view; trailing bits of the final byte are zero.
4314
+ */
4315
+ toBytes() {
4316
+ return Uint8Array.from(this.bytes);
4317
+ }
4318
+
4319
+ /** @returns {string} Debug view, e.g. "0100 0011 0101". */
4320
+ toString() {
4321
+ let s = '';
4322
+ for (let i = 0; i < this.bitLength; i++) {
4323
+ if (i && i % 4 === 0) s += ' ';
4324
+ s += (this.bytes[i >>> 3] >>> (7 - (i & 7))) & 1;
4325
+ }
4326
+ return s;
4327
+ }
4328
+ }
4329
+
4330
+ /** MSB-first bit reader over a byte array. */
4331
+ class BitReader {
4332
+ /** @param {ArrayLike<number>} bytes */
4333
+ constructor(bytes) {
4334
+ this.bytes = bytes;
4335
+ this.byteOffset = 0;
4336
+ this.bitOffset = 0;
4337
+ }
4338
+
4339
+ /** @returns {number} Bits not yet consumed. */
4340
+ available() {
4341
+ return 8 * (this.bytes.length - this.byteOffset) - this.bitOffset;
4342
+ }
4343
+
4344
+ /**
4345
+ * Read `count` bits (1..32) as an unsigned integer, most significant first.
4346
+ *
4347
+ * @param {number} count
4348
+ * @returns {number}
4349
+ * @throws {FormatError} If the stream is exhausted.
4350
+ */
4351
+ read(count) {
4352
+ if (count < 1 || count > 32) {
4353
+ throw new FormatError(`BitReader: cannot read ${count} bits`);
4354
+ }
4355
+ if (count > this.available()) {
4356
+ throw new FormatError(
4357
+ `BitReader: needed ${count} bits, ${this.available()} remain`
4358
+ );
4359
+ }
4360
+
4361
+ let result = 0;
4362
+ let remaining = count;
4363
+
4364
+ // Finish the partially consumed byte first, then take whole bytes.
4365
+ if (this.bitOffset > 0) {
4366
+ const inCurrent = 8 - this.bitOffset;
4367
+ const take = Math.min(remaining, inCurrent);
4368
+ const shift = inCurrent - take;
4369
+ const mask = (0xff >> this.bitOffset) & ~((1 << shift) - 1);
4370
+ result = (this.bytes[this.byteOffset] & mask) >> shift;
4371
+ remaining -= take;
4372
+ this.bitOffset += take;
4373
+ if (this.bitOffset === 8) {
4374
+ this.bitOffset = 0;
4375
+ this.byteOffset++;
4376
+ }
4377
+ }
4378
+
4379
+ while (remaining >= 8) {
4380
+ result = (result << 8) | (this.bytes[this.byteOffset] & 0xff);
4381
+ this.byteOffset++;
4382
+ remaining -= 8;
4383
+ }
4384
+
4385
+ if (remaining > 0) {
4386
+ const shift = 8 - remaining;
4387
+ const mask = ~((1 << shift) - 1) & 0xff;
4388
+ result = (result << remaining) | ((this.bytes[this.byteOffset] & mask) >> shift);
4389
+ this.bitOffset += remaining;
4390
+ }
4391
+
4392
+ return result >>> 0;
4393
+ }
4394
+
4395
+ /** @returns {boolean} */
4396
+ readBit() {
4397
+ return this.read(1) === 1;
4398
+ }
4399
+ }
4400
+
4401
+ __exports.BitWriter = BitWriter;
4402
+ __exports.BitReader = BitReader;
4403
+ };
4404
+
4405
+ __modules["qr/tables.js"] = function (__require, __exports) {
4406
+ /**
4407
+ * QR Code structural tables.
4408
+ *
4409
+ * The design principle here is that as little as possible is *recalled* and as
4410
+ * much as possible is *derived*, because a barcode table is the one place where
4411
+ * a single mistyped digit produces a symbol that looks perfect and scans as
4412
+ * garbage — or, worse, scans correctly for the payload you tested and fails for
4413
+ * the payload your user sends.
4414
+ *
4415
+ * So:
4416
+ *
4417
+ * - Symbol size, function-pattern layout and total codeword capacity are
4418
+ * computed from geometry. Nothing is tabulated that the module grid already
4419
+ * knows.
4420
+ * - Alignment centres come from the spec's spacing rule, not a 40-row table.
4421
+ * - The group-1 / group-2 block split is arithmetic, not data.
4422
+ *
4423
+ * That leaves exactly three recalled numbers per (version, level): the error
4424
+ * correction codewords per block, the block count, and the total data codeword
4425
+ * count. Those three are deliberately redundant — they must satisfy
4426
+ *
4427
+ * blocks * eccPerBlock + totalDataCodewords === geometricTotalCodewords(v)
4428
+ *
4429
+ * for all 160 combinations, where the right-hand side is counted off the module
4430
+ * grid. Any single typo on either side breaks the identity. {@link validateTables}
4431
+ * enforces it, and the test suite asserts it returns no problems.
4432
+ *
4433
+ * @module qr/tables
4434
+ */
4435
+ const { BitMatrix } = __require("core/bit-matrix.js");
4436
+
4437
+ /** Error correction levels, weakest to strongest. */
4438
+ const ECC_LEVELS = ['L', 'M', 'Q', 'H'];
4439
+
4440
+ /**
4441
+ * Two-bit level indicator used in the format information.
4442
+ * Note this is *not* the L/M/Q/H ordering — the spec assigns them out of order.
4443
+ */
4444
+ const ECC_LEVEL_BITS = { L: 0b01, M: 0b00, Q: 0b11, H: 0b10 };
4445
+
4446
+ /** Inverse of {@link ECC_LEVEL_BITS}, indexed by the 2-bit value. */
4447
+ const ECC_LEVEL_BY_BITS = ['M', 'L', 'H', 'Q'];
4448
+ const MIN_VERSION = 1;
4449
+ const MAX_VERSION = 40;
4450
+
4451
+ /** Version at and above which an 18-bit version information block is carried. */
4452
+ const VERSION_INFO_MIN = 7;
4453
+
4454
+ /** Mode indicator nibbles. */
4455
+ const MODE = {
4456
+ TERMINATOR: 0x0,
4457
+ NUMERIC: 0x1,
4458
+ ALPHANUMERIC: 0x2,
4459
+ STRUCTURED_APPEND: 0x3,
4460
+ BYTE: 0x4,
4461
+ FNC1_FIRST: 0x5,
4462
+ ECI: 0x7,
4463
+ KANJI: 0x8,
4464
+ FNC1_SECOND: 0x9,
4465
+ };
4466
+
4467
+ /**
4468
+ * Character count indicator width, in bits, by mode and version band.
4469
+ *
4470
+ * The bands are versions 1-9, 10-26 and 27-40. They are the reason segment
4471
+ * selection and version selection are mutually dependent: widening the count
4472
+ * field can push a payload over a version boundary, which widens it again.
4473
+ */
4474
+ const COUNT_BITS = {
4475
+ [MODE.NUMERIC]: [10, 12, 14],
4476
+ [MODE.ALPHANUMERIC]: [9, 11, 13],
4477
+ [MODE.BYTE]: [8, 16, 16],
4478
+ [MODE.KANJI]: [8, 10, 12],
4479
+ };
4480
+
4481
+ /**
4482
+ * @param {number} version 1-40
4483
+ * @returns {number} Modules per side.
4484
+ */
4485
+ function versionSize(version) {
4486
+ return 17 + 4 * version;
4487
+ }
4488
+
4489
+ /**
4490
+ * Bits in the character count indicator.
4491
+ *
4492
+ * @param {number} mode One of {@link MODE}.
4493
+ * @param {number} version
4494
+ * @returns {number}
4495
+ */
4496
+ function countBits(mode, version) {
4497
+ const widths = COUNT_BITS[mode];
4498
+ if (!widths) return 0;
4499
+ if (version <= 9) return widths[0];
4500
+ if (version <= 26) return widths[1];
4501
+ return widths[2];
4502
+ }
4503
+
4504
+ /**
4505
+ * Centre coordinates of the alignment patterns for a version.
4506
+ *
4507
+ * The spec's rule: the first centre is always 6 and the last is always
4508
+ * `size - 7`; the count grows by one every seven versions; and the centres are
4509
+ * evenly spaced with the *first* gap absorbing the rounding slack. Expressing
4510
+ * that as arithmetic rather than a 40-row table means there is no table to
4511
+ * mistype, and {@link validateTables} can then assert the shape of the result.
4512
+ *
4513
+ * @param {number} version
4514
+ * @returns {number[]} Ascending centres. Empty for version 1.
4515
+ */
4516
+ function alignmentCoordinates(version) {
4517
+ if (version < 2) return [];
4518
+
4519
+ const size = versionSize(version);
4520
+ const count = Math.floor(version / 7) + 2;
4521
+ const last = size - 7;
4522
+
4523
+ // Spacing is rounded up to an even number of modules so every centre lands on
4524
+ // the same parity as the timing pattern, which is what keeps the patterns
4525
+ // aligned with the module grid rather than straddling it.
4526
+ const step = Math.ceil((size - 13) / (2 * count - 2)) * 2;
4527
+
4528
+ const coords = [6];
4529
+ // Walk backwards from the final centre so the slack lands in the first gap.
4530
+ for (let i = count - 1; i >= 1; i--) coords.push(last - (count - 1 - i) * step);
4531
+ coords.sort((a, b) => a - b);
4532
+ return coords;
4533
+ }
4534
+
4535
+ /**
4536
+ * Centres of the alignment patterns actually drawn, as [x, y] pairs.
4537
+ *
4538
+ * The three combinations that would sit on top of a finder pattern are omitted.
4539
+ *
4540
+ * @param {number} version
4541
+ * @returns {Array<[number, number]>}
4542
+ */
4543
+ function alignmentCentres(version) {
4544
+ const coords = alignmentCoordinates(version);
4545
+ if (coords.length === 0) return [];
4546
+
4547
+ const size = versionSize(version);
4548
+ const lo = 6;
4549
+ const hi = size - 7;
4550
+ const out = [];
4551
+ for (let i = 0; i < coords.length; i++) {
4552
+ for (let j = 0; j < coords.length; j++) {
4553
+ const x = coords[j];
4554
+ const y = coords[i];
4555
+ // Skip the three finder corners.
4556
+ if (x === lo && y === lo) continue;
4557
+ if (x === lo && y === hi) continue;
4558
+ if (x === hi && y === lo) continue;
4559
+ out.push([x, y]);
4560
+ }
4561
+ }
4562
+ return out;
4563
+ }
4564
+
4565
+ const reservedCache = new Map();
4566
+
4567
+ /**
4568
+ * Map of modules that carry function patterns rather than payload.
4569
+ *
4570
+ * A set bit means "reserved": finder, separator, timing, alignment, format
4571
+ * information, the dark module, and the version information blocks. This is the
4572
+ * single source of truth used by the encoder to skip modules while laying out
4573
+ * the bitstream, by the decoder to read them back in the same order, and by
4574
+ * {@link geometricTotalCodewords} to count what is left.
4575
+ *
4576
+ * Deriving capacity this way rather than by hand arithmetic is what makes the
4577
+ * awkward cases free: an alignment pattern that overlaps the timing pattern is
4578
+ * counted once because it is the same set of modules, not because anyone
4579
+ * remembered to subtract five.
4580
+ *
4581
+ * @param {number} version
4582
+ * @returns {BitMatrix} Shared, cached — treat as immutable.
4583
+ */
4584
+ function reservedModules(version) {
4585
+ const cached = reservedCache.get(version);
4586
+ if (cached) return cached;
4587
+
4588
+ const size = versionSize(version);
4589
+ const m = new BitMatrix(size, size);
4590
+
4591
+ // Finder patterns with their separators: an 8x8 reserved block at each of
4592
+ // three corners (7x7 pattern plus a one-module light border on the inner
4593
+ // sides, which the corner blocks absorb).
4594
+ m.setRegion(0, 0, 8, 8);
4595
+ m.setRegion(size - 8, 0, 8, 8);
4596
+ m.setRegion(0, size - 8, 8, 8);
4597
+
4598
+ // Timing patterns, spanning the gap between the separators.
4599
+ for (let i = 8; i < size - 8; i++) {
4600
+ m.set(i, 6);
4601
+ m.set(6, i);
4602
+ }
4603
+
4604
+ // Alignment patterns, 5x5 each.
4605
+ const centres = alignmentCentres(version);
4606
+ for (let i = 0; i < centres.length; i++) {
4607
+ m.setRegion(centres[i][0] - 2, centres[i][1] - 2, 5, 5);
4608
+ }
4609
+
4610
+ // Format information: two copies plus the dark module. The copies partly
4611
+ // fall inside the 8x8 finder blocks already reserved; setting them again is
4612
+ // harmless and keeps the intent explicit.
4613
+ const [copyA, copyB] = formatInfoPositions(size);
4614
+ for (let i = 0; i < 15; i++) {
4615
+ m.set(copyA[i][0], copyA[i][1]);
4616
+ m.set(copyB[i][0], copyB[i][1]);
4617
+ }
4618
+ m.set(8, size - 8); // dark module
4619
+
4620
+ // Version information, two 6x3 blocks.
4621
+ if (version >= VERSION_INFO_MIN) {
4622
+ m.setRegion(size - 11, 0, 3, 6);
4623
+ m.setRegion(0, size - 11, 6, 3);
4624
+ }
4625
+
4626
+ reservedCache.set(version, m);
4627
+ return m;
4628
+ }
4629
+
4630
+ /**
4631
+ * Module positions of the two format information copies.
4632
+ *
4633
+ * Index `i` in each array is bit `i` of the 15-bit format value, bit 0 being
4634
+ * the least significant.
4635
+ *
4636
+ * CAVEAT WORTH READING: the *direction* of this numbering is the one thing in
4637
+ * this file that a round-trip test cannot falsify. Encoder and decoder share
4638
+ * these tables, so a mirrored layout would pass every test in the suite and
4639
+ * fail only against a real scanner. The layout below is the standard one; both
4640
+ * sides deliberately consume this single definition so there is no second place
4641
+ * for the convention to drift.
4642
+ *
4643
+ * @param {number} size Modules per side.
4644
+ * @returns {[Array<[number, number]>, Array<[number, number]>]} [copyA, copyB]
4645
+ */
4646
+ function formatInfoPositions(size) {
4647
+ /** @type {Array<[number, number]>} */
4648
+ const a = [];
4649
+ /** @type {Array<[number, number]>} */
4650
+ const b = [];
4651
+
4652
+ for (let i = 0; i < 15; i++) {
4653
+ // Copy A wraps the top-left finder: down column 8, then left along row 8,
4654
+ // stepping over the two timing modules.
4655
+ if (i < 6) a.push([8, i]);
4656
+ else if (i === 6) a.push([8, 7]);
4657
+ else if (i === 7) a.push([8, 8]);
4658
+ else if (i === 8) a.push([7, 8]);
4659
+ else a.push([14 - i, 8]);
4660
+
4661
+ // Copy B is split: the low bits run right-to-left along row 8 beside the
4662
+ // top-right finder, the high bits run bottom-up beside the bottom-left one.
4663
+ if (i < 8) b.push([size - 1 - i, 8]);
4664
+ else b.push([8, size - 15 + i]);
4665
+ }
4666
+
4667
+ return [a, b];
4668
+ }
4669
+
4670
+ /**
4671
+ * Modules available to data and error correction, counted off the grid.
4672
+ *
4673
+ * @param {number} version
4674
+ * @returns {number}
4675
+ */
4676
+ function freeModuleCount(version) {
4677
+ const reserved = reservedModules(version);
4678
+ const size = versionSize(version);
4679
+ let free = 0;
4680
+ for (let y = 0; y < size; y++) {
4681
+ for (let x = 0; x < size; x++) {
4682
+ if (!reserved.get(x, y)) free++;
4683
+ }
4684
+ }
4685
+ return free;
4686
+ }
4687
+
4688
+ /**
4689
+ * Total codewords (data + error correction) a version holds.
4690
+ *
4691
+ * Geometric, not tabulated — this is the reference the ECC table is checked
4692
+ * against.
4693
+ *
4694
+ * @param {number} version
4695
+ * @returns {number}
4696
+ */
4697
+ function geometricTotalCodewords(version) {
4698
+ return Math.floor(freeModuleCount(version) / 8);
4699
+ }
4700
+
4701
+ /**
4702
+ * Bits left over after the last whole codeword, written as zeroes.
4703
+ *
4704
+ * @param {number} version
4705
+ * @returns {number} 0, 3, 4 or 7.
4706
+ */
4707
+ function remainderBits(version) {
4708
+ return freeModuleCount(version) % 8;
4709
+ }
4710
+
4711
+ const orderCache = new Map();
4712
+
4713
+ /**
4714
+ * Module positions in bitstream order, as interleaved x, y pairs.
4715
+ *
4716
+ * The layout walks two-module-wide columns from the bottom-right corner
4717
+ * leftward, alternating upward and downward, right module of the pair before
4718
+ * the left, skipping the vertical timing column and every reserved module.
4719
+ *
4720
+ * Encoder and decoder both consume this one function. That is not tidiness: a
4721
+ * placement order that disagrees between the two would still round-trip
4722
+ * perfectly within this library while producing symbols no scanner can read.
4723
+ * There is only one order because there is only one implementation of it.
4724
+ *
4725
+ * @param {number} version
4726
+ * @returns {Int32Array} Shared, cached — treat as immutable. Length is
4727
+ * `2 * freeModuleCount(version)`.
4728
+ */
4729
+ function dataModuleOrder(version) {
4730
+ const cached = orderCache.get(version);
4731
+ if (cached) return cached;
4732
+
4733
+ const size = versionSize(version);
4734
+ const reserved = reservedModules(version);
4735
+ const out = new Int32Array(freeModuleCount(version) * 2);
4736
+ let n = 0;
4737
+
4738
+ let upward = true;
4739
+ for (let col = size - 1; col > 0; col -= 2) {
4740
+ // Column 6 is the vertical timing pattern. Stepping over it shifts the
4741
+ // whole remaining schedule left by one, so the loop variable itself has to
4742
+ // move — adjusting only the current pair would visit column 4 twice and
4743
+ // column 0 never, which is self-consistent between encoder and decoder and
4744
+ // therefore invisible to a round-trip test.
4745
+ if (col === 6) col--;
4746
+ for (let i = 0; i < size; i++) {
4747
+ const y = upward ? size - 1 - i : i;
3721
4748
  for (let c = 0; c < 2; c++) {
3722
4749
  const x = col - c;
3723
4750
  if (reserved.get(x, y)) continue;
@@ -5565,934 +6592,1741 @@ __exports.ChecksumError = ChecksumError;
5565
6592
  __exports.decodeQR = decodeQR;
5566
6593
  };
5567
6594
 
5568
- __modules["image/perspective.js"] = function (__require, __exports) {
6595
+ __modules["qr/detector.js"] = function (__require, __exports) {
5569
6596
  /**
5570
- * Projective (perspective) transforms.
5571
- *
5572
- * A 2D symbol photographed off-axis is not a rotated square — it is a
5573
- * quadrilateral with converging edges. Correcting that needs a full projective
5574
- * map, not an affine one; an affine approximation reads the near edge of a
5575
- * tilted symbol correctly and drifts a module or more by the far edge.
6597
+ * QR Code detection — finding symbols in a binarized image.
5576
6598
  *
5577
- * The map is a 3x3 homogeneous matrix. Points are transformed as
5578
- * (x, y, 1) * M, then divided through by the resulting w.
6599
+ * The whole thing hangs off one property of the finder pattern: along any line
6600
+ * through its centre, in any direction, the dark and light runs are in the
6601
+ * ratio 1:1:3:1:1. That is true horizontally, vertically and diagonally, it is
6602
+ * scale-independent, and no ordinary printed matter reproduces it by accident.
6603
+ * So the search is: scan rows for that ratio, confirm each hit by scanning the
6604
+ * column through it, cluster what survives, and look for three clusters
6605
+ * arranged in a right isoceles triangle.
5579
6606
  *
5580
- * @module image/perspective
5581
- */
5582
- class PerspectiveTransform {
5583
- /* eslint-disable-next-line max-params */
5584
- constructor(a11, a21, a31, a12, a22, a32, a13, a23, a33) {
5585
- this.a11 = a11; this.a21 = a21; this.a31 = a31;
5586
- this.a12 = a12; this.a22 = a22; this.a32 = a32;
5587
- this.a13 = a13; this.a23 = a23; this.a33 = a33;
5588
- }
5589
-
5590
- /**
5591
- * Transform points in place.
5592
- *
5593
- * @param {Float32Array | number[]} points Interleaved [x0, y0, x1, y1, ...].
5594
- * @returns {Float32Array | number[]} The same array.
5595
- */
5596
- transform(points) {
5597
- const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
5598
- for (let i = 0; i < points.length; i += 2) {
5599
- const x = points[i];
5600
- const y = points[i + 1];
5601
- const w = a13 * x + a23 * y + a33;
5602
- points[i] = (a11 * x + a21 * y + a31) / w;
5603
- points[i + 1] = (a12 * x + a22 * y + a32) / w;
5604
- }
5605
- return points;
5606
- }
5607
-
5608
- /**
5609
- * Transform a single point.
5610
- *
5611
- * @param {number} x @param {number} y
5612
- * @returns {{x: number, y: number}}
5613
- */
5614
- transformPoint(x, y) {
5615
- const w = this.a13 * x + this.a23 * y + this.a33;
5616
- return {
5617
- x: (this.a11 * x + this.a21 * y + this.a31) / w,
5618
- y: (this.a12 * x + this.a22 * y + this.a32) / w,
5619
- };
5620
- }
5621
-
5622
- /**
5623
- * Map the unit square — (0,0), (1,0), (1,1), (0,1) — onto an arbitrary quad.
5624
- *
5625
- * Corners are given in that same order, i.e. going around the quad, not
5626
- * as opposite pairs.
5627
- *
5628
- * @returns {PerspectiveTransform}
5629
- */
5630
- /* eslint-disable-next-line max-params */
5631
- static squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3) {
5632
- const dx3 = x0 - x1 + x2 - x3;
5633
- const dy3 = y0 - y1 + y2 - y3;
5634
-
5635
- if (dx3 === 0 && dy3 === 0) {
5636
- // The quad is a parallelogram, so the map is affine and the projective
5637
- // terms vanish. Worth special-casing: it is the common case for flat
5638
- // scans, and the general solution divides by zero here.
5639
- return new PerspectiveTransform(
5640
- x1 - x0, x2 - x1, x0,
5641
- y1 - y0, y2 - y1, y0,
5642
- 0, 0, 1
5643
- );
5644
- }
5645
-
5646
- const dx1 = x1 - x2;
5647
- const dx2 = x3 - x2;
5648
- const dy1 = y1 - y2;
5649
- const dy2 = y3 - y2;
5650
- const denominator = dx1 * dy2 - dx2 * dy1;
5651
- const a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
5652
- const a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
5653
-
5654
- return new PerspectiveTransform(
5655
- x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0,
5656
- y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0,
5657
- a13, a23, 1
5658
- );
5659
- }
5660
-
5661
- /**
5662
- * Map an arbitrary quad onto the unit square — the inverse of
5663
- * {@link squareToQuad}, via the adjugate.
5664
- *
5665
- * @returns {PerspectiveTransform}
5666
- */
5667
- /* eslint-disable-next-line max-params */
5668
- static quadToSquare(x0, y0, x1, y1, x2, y2, x3, y3) {
5669
- return PerspectiveTransform.squareToQuad(x0, y0, x1, y1, x2, y2, x3, y3).inverse();
5670
- }
5671
-
5672
- /**
5673
- * Map one quad onto another, corner for corner.
5674
- *
5675
- * This is what turns four detected finder corners into a sampling grid:
5676
- * compose "detected quad -> unit square" with "unit square -> ideal grid".
5677
- *
5678
- * @returns {PerspectiveTransform}
5679
- */
5680
- /* eslint-disable-next-line max-params */
5681
- static quadToQuad(
5682
- sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3,
5683
- dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3
5684
- ) {
5685
- const toSquare = PerspectiveTransform.quadToSquare(sx0, sy0, sx1, sy1, sx2, sy2, sx3, sy3);
5686
- const toQuad = PerspectiveTransform.squareToQuad(dx0, dy0, dx1, dy1, dx2, dy2, dx3, dy3);
5687
- return toSquare.times(toQuad);
5688
- }
5689
-
5690
- /**
5691
- * Adjugate — the inverse up to a scale factor, which is irrelevant in
5692
- * homogeneous coordinates because the division by w cancels it.
5693
- *
5694
- * @returns {PerspectiveTransform}
5695
- */
5696
- inverse() {
5697
- const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
5698
- return new PerspectiveTransform(
5699
- a22 * a33 - a23 * a32,
5700
- a23 * a31 - a21 * a33,
5701
- a21 * a32 - a22 * a31,
5702
- a13 * a32 - a12 * a33,
5703
- a11 * a33 - a13 * a31,
5704
- a12 * a31 - a11 * a32,
5705
- a12 * a23 - a13 * a22,
5706
- a13 * a21 - a11 * a23,
5707
- a11 * a22 - a12 * a21
5708
- );
5709
- }
5710
-
5711
- /**
5712
- * Matrix product: apply `this` first, then `other`.
5713
- *
5714
- * @param {PerspectiveTransform} other
5715
- * @returns {PerspectiveTransform}
5716
- */
5717
- times(other) {
5718
- const { a11, a21, a31, a12, a22, a32, a13, a23, a33 } = this;
5719
- const o = other;
5720
- return new PerspectiveTransform(
5721
- o.a11 * a11 + o.a21 * a12 + o.a31 * a13,
5722
- o.a11 * a21 + o.a21 * a22 + o.a31 * a23,
5723
- o.a11 * a31 + o.a21 * a32 + o.a31 * a33,
5724
- o.a12 * a11 + o.a22 * a12 + o.a32 * a13,
5725
- o.a12 * a21 + o.a22 * a22 + o.a32 * a23,
5726
- o.a12 * a31 + o.a22 * a32 + o.a32 * a33,
5727
- o.a13 * a11 + o.a23 * a12 + o.a33 * a13,
5728
- o.a13 * a21 + o.a23 * a22 + o.a33 * a23,
5729
- o.a13 * a31 + o.a23 * a32 + o.a33 * a33
5730
- );
5731
- }
5732
- }
6607
+ * Three finders give three corners. The fourth is the problem: extrapolating
6608
+ * `topRight + bottomLeft - topLeft` assumes the symbol is a parallelogram,
6609
+ * which is only true if it was photographed square-on. For version 2 and up the
6610
+ * bottom-right alignment pattern pins that corner down properly, which is what
6611
+ * makes a tilted symbol readable. When the alignment pattern cannot be found,
6612
+ * detection degrades to the parallelogram estimate rather than failing — a
6613
+ * slightly wrong corner still decodes on a flat image, and Reed-Solomon absorbs
6614
+ * the rest.
6615
+ *
6616
+ * @module qr/detector
6617
+ */
6618
+ const { NotFoundError } = __require("core/errors.js");
6619
+ const { PerspectiveTransform } = __require("image/perspective.js");
6620
+ const { sampleQuad } = __require("image/grid-sampler.js");
6621
+ const { decodeQR } = __require("qr/decoder.js");
5733
6622
 
5734
- __exports.PerspectiveTransform = PerspectiveTransform;
5735
- };
6623
+ /** Finder pattern run ratios, centre run first in the array's own order. */
6624
+ const FINDER_RATIOS = [1, 1, 3, 1, 1];
6625
+
6626
+ /** Alignment pattern run ratios. */
6627
+ const ALIGNMENT_RATIOS = [1, 1, 1, 1, 1];
6628
+
6629
+ /** Smallest and largest legal symbol dimensions, in modules. */
6630
+ const MIN_DIMENSION = 21;
6631
+ const MAX_DIMENSION = 177;
5736
6632
 
5737
- __modules["image/grid-sampler.js"] = function (__require, __exports) {
5738
6633
  /**
5739
- * Resample a distorted symbol in the image into an upright module grid.
6634
+ * Do five alternating runs match the expected ratios?
5740
6635
  *
5741
- * Given a transform that maps grid coordinates to image coordinates, this
5742
- * samples the centre of every module. Sampling centres rather than averaging
5743
- * whole cells is deliberate: module edges are where blur and bleed live, and
5744
- * including them turns a marginal symbol into an unreadable one.
6636
+ * Tolerance is half a module, scaled by the ratio, which is the widest band
6637
+ * that still rejects ordinary text and rules while accepting the blur and
6638
+ * rounding of a real scan.
5745
6639
  *
5746
- * @module image/grid-sampler
6640
+ * @param {number[]} counts Five run lengths, dark first.
6641
+ * @param {number[]} ratios
6642
+ * @returns {number} The implied module size, or 0 if the ratios do not match.
5747
6643
  */
5748
- const { BitMatrix } = __require("core/bit-matrix.js");
5749
- const { NotFoundError } = __require("core/errors.js");
5750
- const { PerspectiveTransform } = __require("image/perspective.js");
6644
+ function matchRatios(counts, ratios) {
6645
+ let total = 0;
6646
+ let units = 0;
6647
+ for (let i = 0; i < 5; i++) {
6648
+ if (counts[i] === 0) return 0;
6649
+ total += counts[i];
6650
+ units += ratios[i];
6651
+ }
6652
+ if (total < units) return 0;
6653
+
6654
+ const moduleSize = total / units;
6655
+ const tolerance = moduleSize / 2;
6656
+ for (let i = 0; i < 5; i++) {
6657
+ if (Math.abs(counts[i] - moduleSize * ratios[i]) > tolerance * ratios[i]) return 0;
6658
+ }
6659
+ return moduleSize;
6660
+ }
5751
6661
 
5752
6662
  /**
5753
- * Sample a `dimension` x `dimension` grid (or `width` x `height`).
6663
+ * Scan one row for the finder run ratio.
5754
6664
  *
5755
- * @param {BitMatrix} image Binarized source image.
5756
- * @param {number} width Modules across.
5757
- * @param {number} height Modules down.
5758
- * @param {PerspectiveTransform} transform Grid space -> image space.
5759
- * @returns {BitMatrix}
5760
- * @throws {NotFoundError} If the grid falls outside the image.
6665
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
6666
+ * @param {number} y
6667
+ * @param {number[]} ratios
6668
+ * @param {(centreX: number, moduleSize: number) => void} onHit
5761
6669
  */
5762
- function sampleGrid(image, width, height, transform) {
5763
- const out = new BitMatrix(width, height);
5764
- const points = new Float32Array(width * 2);
6670
+ function scanRow(image, y, ratios, onHit) {
6671
+ const width = image.width;
6672
+ const counts = [0, 0, 0, 0, 0];
6673
+ let state = 0;
5765
6674
 
5766
- for (let y = 0; y < height; y++) {
5767
- // Module centres: offset by half a module in both axes.
5768
- const gridY = y + 0.5;
5769
- for (let x = 0; x < width; x++) {
5770
- points[x * 2] = x + 0.5;
5771
- points[x * 2 + 1] = gridY;
6675
+ for (let x = 0; x < width; x++) {
6676
+ const dark = image.get(x, y);
6677
+
6678
+ // Even states are dark runs, odd states light.
6679
+ if (dark === ((state & 1) === 0)) {
6680
+ counts[state]++;
6681
+ continue;
5772
6682
  }
5773
- transform.transform(points);
5774
6683
 
5775
- for (let x = 0; x < width; x++) {
5776
- const px = points[x * 2] | 0;
5777
- const py = points[x * 2 + 1] | 0;
5778
- if (px < 0 || py < 0 || px >= image.width || py >= image.height) {
5779
- throw new NotFoundError(
5780
- `Sampling grid escapes the image at module (${x}, ${y})`
5781
- );
5782
- }
5783
- if (image.get(px, py)) out.set(x, y);
6684
+ // A leading light margin is not part of any pattern.
6685
+ if (state === 0 && counts[0] === 0) continue;
6686
+
6687
+ if (state < 4) {
6688
+ state++;
6689
+ counts[state] = 1;
6690
+ continue;
6691
+ }
6692
+
6693
+ // Five runs complete, and the sixth has begun.
6694
+ const moduleSize = matchRatios(counts, ratios);
6695
+ if (moduleSize > 0) {
6696
+ onHit(x - counts[4] - counts[3] - counts[2] / 2, moduleSize);
5784
6697
  }
6698
+
6699
+ // Slide the window on by two runs: the trailing dark run of a rejected
6700
+ // candidate is often the leading dark run of the real one.
6701
+ counts[0] = counts[2];
6702
+ counts[1] = counts[3];
6703
+ counts[2] = counts[4];
6704
+ counts[3] = 1;
6705
+ counts[4] = 0;
6706
+ state = 3;
5785
6707
  }
5786
6708
 
5787
- return out;
6709
+ if (state === 4) {
6710
+ const moduleSize = matchRatios(counts, ratios);
6711
+ if (moduleSize > 0) {
6712
+ onHit(width - counts[4] - counts[3] - counts[2] / 2, moduleSize);
6713
+ }
6714
+ }
5788
6715
  }
5789
6716
 
5790
6717
  /**
5791
- * Sample with a 3x3 majority vote per module.
6718
+ * Walk a line through a candidate centre and confirm the ratio holds there too.
5792
6719
  *
5793
- * Slower, and worth it when a single-point sample lands on a speck of noise or
5794
- * a JPEG artefact. Readers fall back to this after a clean sample fails to
5795
- * decode, rather than paying for it on every attempt.
6720
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
6721
+ * @param {number} x @param {number} y
6722
+ * @param {number} dx @param {number} dy Unit step defining the line.
6723
+ * @param {number[]} ratios
6724
+ * @param {number} maxRun Guard against running the length of a dark image.
6725
+ * @returns {number} Refined centre offset along the line, or NaN.
6726
+ */
6727
+ function crossCheck(image, x, y, dx, dy, ratios, maxRun) {
6728
+ const width = image.width;
6729
+ const height = image.height;
6730
+
6731
+ /** @returns {boolean | null} Null when the sample falls outside the image. */
6732
+ const at = (i) => {
6733
+ const px = x + dx * i;
6734
+ const py = y + dy * i;
6735
+ if (px < 0 || py < 0 || px >= width || py >= height) return null;
6736
+ return image.get(px, py);
6737
+ };
6738
+
6739
+ if (at(0) !== true) return NaN;
6740
+
6741
+ const counts = [0, 0, 0, 0, 0];
6742
+ let i = 0;
6743
+
6744
+ // Forward from the centre: rest of the centre run, then light, then dark.
6745
+ while (at(i) === true && counts[2] < maxRun) { counts[2]++; i++; }
6746
+ if (at(i) === null) return NaN;
6747
+ const centreForward = counts[2];
6748
+
6749
+ while (at(i) === false && counts[3] < maxRun) { counts[3]++; i++; }
6750
+ if (at(i) === null || counts[3] === 0) return NaN;
6751
+
6752
+ while (at(i) === true && counts[4] < maxRun) { counts[4]++; i++; }
6753
+ if (counts[4] === 0) return NaN;
6754
+
6755
+ // Backward from the centre.
6756
+ i = -1;
6757
+ while (at(i) === true && counts[2] < maxRun * 2) { counts[2]++; i--; }
6758
+ if (at(i) === null) return NaN;
6759
+ const centreBackward = counts[2] - centreForward;
6760
+
6761
+ while (at(i) === false && counts[1] < maxRun) { counts[1]++; i--; }
6762
+ if (at(i) === null || counts[1] === 0) return NaN;
6763
+
6764
+ while (at(i) === true && counts[0] < maxRun) { counts[0]++; i--; }
6765
+ if (counts[0] === 0) return NaN;
6766
+
6767
+ if (matchRatios(counts, ratios) === 0) return NaN;
6768
+
6769
+ // The centre run spans offsets [-centreBackward, centreForward - 1].
6770
+ return (centreForward - 1 - centreBackward) / 2;
6771
+ }
6772
+
6773
+ /**
6774
+ * @typedef {object} Candidate
6775
+ * @property {number} x
6776
+ * @property {number} y
6777
+ * @property {number} moduleSize
6778
+ * @property {number} hits
6779
+ */
6780
+
6781
+ /**
6782
+ * Locate pattern centres of a given run ratio.
5796
6783
  *
5797
- * @param {BitMatrix} image
5798
- * @param {number} width
5799
- * @param {number} height
5800
- * @param {PerspectiveTransform} transform
5801
- * @returns {BitMatrix}
6784
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
6785
+ * @param {number[]} ratios
6786
+ * @param {number} unitsWide Modules the pattern spans (7 or 5).
6787
+ * @param {{x0: number, y0: number, x1: number, y1: number}} [region]
6788
+ * @returns {Candidate[]}
5802
6789
  */
5803
- function sampleGridVoting(image, width, height, transform) {
5804
- const out = new BitMatrix(width, height);
6790
+ function findPatterns(image, ratios, unitsWide, region) {
6791
+ /** @type {Candidate[]} */
6792
+ const found = [];
5805
6793
 
5806
- // Spacing between module centres, measured in image pixels, so the vote
5807
- // spreads across the module rather than a fixed pixel radius that would be
5808
- // meaningless at a different scale.
5809
- const p0 = transform.transformPoint(0.5, 0.5);
5810
- const p1 = transform.transformPoint(1.5, 0.5);
5811
- const p2 = transform.transformPoint(0.5, 1.5);
5812
- const stepX = Math.hypot(p1.x - p0.x, p1.y - p0.y);
5813
- const stepY = Math.hypot(p2.x - p0.x, p2.y - p0.y);
5814
- const rx = Math.max(1, Math.round(stepX / 4));
5815
- const ry = Math.max(1, Math.round(stepY / 4));
6794
+ const y0 = region ? Math.max(0, region.y0) : 0;
6795
+ const y1 = region ? Math.min(image.height, region.y1) : image.height;
5816
6796
 
5817
- for (let y = 0; y < height; y++) {
5818
- for (let x = 0; x < width; x++) {
5819
- const c = transform.transformPoint(x + 0.5, y + 0.5);
5820
- const cx = c.x | 0;
5821
- const cy = c.y | 0;
5822
- if (cx < 0 || cy < 0 || cx >= image.width || cy >= image.height) {
5823
- throw new NotFoundError(
5824
- `Sampling grid escapes the image at module (${x}, ${y})`
5825
- );
5826
- }
6797
+ for (let y = y0; y < y1; y++) {
6798
+ scanRow(image, y, ratios, (centreX, moduleSize) => {
6799
+ if (region && (centreX < region.x0 || centreX > region.x1)) return;
5827
6800
 
5828
- let dark = 0;
5829
- let total = 0;
5830
- for (let dy = -1; dy <= 1; dy++) {
5831
- for (let dx = -1; dx <= 1; dx++) {
5832
- const sx = cx + dx * rx;
5833
- const sy = cy + dy * ry;
5834
- if (sx < 0 || sy < 0 || sx >= image.width || sy >= image.height) continue;
5835
- total++;
5836
- if (image.get(sx, sy)) dark++;
6801
+ const px = Math.floor(centreX);
6802
+ const maxRun = Math.ceil(moduleSize * unitsWide);
6803
+ const offsetY = crossCheck(image, px, y, 0, 1, ratios, maxRun);
6804
+ if (Number.isNaN(offsetY)) return;
6805
+
6806
+ const cy = y + offsetY;
6807
+ // Re-check horizontally at the refined row, which both confirms the hit
6808
+ // and gives a better x than the original scan line did.
6809
+ const offsetX = crossCheck(image, px, Math.round(cy), 1, 0, ratios, maxRun);
6810
+ if (Number.isNaN(offsetX)) return;
6811
+
6812
+ const cx = px + offsetX;
6813
+
6814
+ // Merge with an existing centre when they describe the same pattern.
6815
+ for (let i = 0; i < found.length; i++) {
6816
+ const c = found[i];
6817
+ if (
6818
+ Math.abs(c.x - cx) <= c.moduleSize &&
6819
+ Math.abs(c.y - cy) <= c.moduleSize &&
6820
+ Math.abs(c.moduleSize - moduleSize) <= Math.max(1, c.moduleSize / 2)
6821
+ ) {
6822
+ const n = c.hits + 1;
6823
+ c.x = (c.x * c.hits + cx) / n;
6824
+ c.y = (c.y * c.hits + cy) / n;
6825
+ c.moduleSize = (c.moduleSize * c.hits + moduleSize) / n;
6826
+ c.hits = n;
6827
+ return;
5837
6828
  }
5838
6829
  }
5839
- if (total > 0 && dark * 2 > total) out.set(x, y);
5840
- }
6830
+
6831
+ found.push({ x: cx, y: cy, moduleSize, hits: 1 });
6832
+ });
5841
6833
  }
5842
6834
 
5843
- return out;
6835
+ return found;
5844
6836
  }
5845
6837
 
5846
6838
  /**
5847
- * Build the transform for a symbol whose four corners are known, and sample it.
6839
+ * @param {{x: number, y: number}} a @param {{x: number, y: number}} b
6840
+ * @returns {number}
6841
+ */
6842
+ function distance(a, b) {
6843
+ return Math.hypot(a.x - b.x, a.y - b.y);
6844
+ }
6845
+
6846
+ /**
6847
+ * Order three finder centres as top-left, top-right, bottom-left.
5848
6848
  *
5849
- * Corners are in reading order: top-left, top-right, bottom-right, bottom-left.
6849
+ * The corner is the centre opposite the longest side. Which of the remaining
6850
+ * two is "top right" follows from the sign of the cross product: in image
6851
+ * coordinates, with y increasing downward, a symbol the right way round has
6852
+ * (topRight - topLeft) x (bottomLeft - topLeft) positive.
5850
6853
  *
5851
- * @param {BitMatrix} image
5852
- * @param {number} dimension Modules per side.
5853
- * @param {Array<{x: number, y: number}>} corners
5854
- * @param {boolean} [voting]
5855
- * @returns {BitMatrix}
6854
+ * @param {Candidate[]} three
6855
+ * @returns {{tl: Candidate, tr: Candidate, bl: Candidate} | null}
5856
6856
  */
5857
- function sampleQuad(image, dimension, corners, voting = false) {
5858
- if (corners.length !== 4) throw new NotFoundError('sampleQuad needs exactly 4 corners');
5859
- const [tl, tr, br, bl] = corners;
5860
- const d = dimension;
6857
+ function orientFinders(three) {
6858
+ const [a, b, c] = three;
6859
+ const ab = distance(a, b);
6860
+ const bc = distance(b, c);
6861
+ const ca = distance(c, a);
5861
6862
 
5862
- const transform = PerspectiveTransform.quadToQuad(
5863
- 0, 0, d, 0, d, d, 0, d,
5864
- tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y
5865
- );
6863
+ let tl, p, q, hypotenuse, leg1, leg2;
6864
+ if (ab >= bc && ab >= ca) {
6865
+ tl = c; p = a; q = b; hypotenuse = ab; leg1 = ca; leg2 = bc;
6866
+ } else if (bc >= ab && bc >= ca) {
6867
+ tl = a; p = b; q = c; hypotenuse = bc; leg1 = ab; leg2 = ca;
6868
+ } else {
6869
+ tl = b; p = c; q = a; hypotenuse = ca; leg1 = bc; leg2 = ab;
6870
+ }
5866
6871
 
5867
- return voting
5868
- ? sampleGridVoting(image, d, d, transform)
5869
- : sampleGrid(image, d, d, transform);
6872
+ if (leg1 === 0 || leg2 === 0) return null;
6873
+
6874
+ // The two legs must be near enough equal, and Pythagoras must hold: this is
6875
+ // what rejects three unrelated finder-lookalikes that happen to co-occur.
6876
+ const ratio = leg1 / leg2;
6877
+ if (ratio < 0.7 || ratio > 1.4) return null;
6878
+ const expected = Math.hypot(leg1, leg2);
6879
+ if (Math.abs(hypotenuse - expected) > expected * 0.25) return null;
6880
+
6881
+ const cross = (p.x - tl.x) * (q.y - tl.y) - (p.y - tl.y) * (q.x - tl.x);
6882
+ return cross >= 0 ? { tl, tr: p, bl: q } : { tl, tr: q, bl: p };
5870
6883
  }
5871
6884
 
5872
- __exports.sampleGrid = sampleGrid;
5873
- __exports.sampleGridVoting = sampleGridVoting;
5874
- __exports.sampleQuad = sampleQuad;
5875
- };
6885
+ /**
6886
+ * Snap a measured dimension to a legal symbol size.
6887
+ *
6888
+ * Every QR dimension is 17 + 4v, so `dimension % 4 === 1`. A measurement one
6889
+ * off is rounding; two off means the module size estimate is wrong and the
6890
+ * candidate is not worth pursuing.
6891
+ *
6892
+ * @param {number} raw
6893
+ * @returns {number} 0 if it cannot be reconciled.
6894
+ */
6895
+ function snapDimension(raw) {
6896
+ let d = Math.round(raw);
6897
+ switch (d & 3) {
6898
+ case 0: d--; break;
6899
+ case 2: d++; break;
6900
+ case 3: return 0;
6901
+ default: break;
6902
+ }
6903
+ if (d < MIN_DIMENSION || d > MAX_DIMENSION) return 0;
6904
+ return d;
6905
+ }
6906
+
6907
+ /** The alignment pattern, as modules. 1 is dark. */
6908
+ const ALIGNMENT_MODULES = [
6909
+ [1, 1, 1, 1, 1],
6910
+ [1, 0, 0, 0, 1],
6911
+ [1, 0, 1, 0, 1],
6912
+ [1, 0, 0, 0, 1],
6913
+ [1, 1, 1, 1, 1],
6914
+ ];
5876
6915
 
5877
- __modules["qr/detector.js"] = function (__require, __exports) {
5878
6916
  /**
5879
- * QR Code detection finding symbols in a binarized image.
6917
+ * Confirm a candidate by reading the 5x5 module block it claims to be.
5880
6918
  *
5881
- * The whole thing hangs off one property of the finder pattern: along any line
5882
- * through its centre, in any direction, the dark and light runs are in the
5883
- * ratio 1:1:3:1:1. That is true horizontally, vertically and diagonally, it is
5884
- * scale-independent, and no ordinary printed matter reproduces it by accident.
5885
- * So the search is: scan rows for that ratio, confirm each hit by scanning the
5886
- * column through it, cluster what survives, and look for three clusters
5887
- * arranged in a right isoceles triangle.
6919
+ * The run-ratio scan alone is not enough here. A finder pattern's 1:1:3:1:1 is
6920
+ * rare enough to stand on its own, but an alignment pattern's 1:1:1:1:1 occurs
6921
+ * constantly in ordinary data modules, so an unverified match near the expected
6922
+ * position is more likely to be payload than pattern — and a false match drags
6923
+ * the fourth corner off by several modules, which is worse than having no
6924
+ * alignment pattern at all.
5888
6925
  *
5889
- * Three finders give three corners. The fourth is the problem: extrapolating
5890
- * `topRight + bottomLeft - topLeft` assumes the symbol is a parallelogram,
5891
- * which is only true if it was photographed square-on. For version 2 and up the
5892
- * bottom-right alignment pattern pins that corner down properly, which is what
5893
- * makes a tilted symbol readable. When the alignment pattern cannot be found,
5894
- * detection degrades to the parallelogram estimate rather than failing — a
5895
- * slightly wrong corner still decodes on a flat image, and Reed-Solomon absorbs
5896
- * the rest.
6926
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
6927
+ * @param {number} cx @param {number} cy @param {number} moduleSize
6928
+ * @returns {boolean}
6929
+ */
6930
+ function verifyAlignment(image, cx, cy, moduleSize) {
6931
+ let good = 0;
6932
+ for (let j = 0; j < 5; j++) {
6933
+ for (let i = 0; i < 5; i++) {
6934
+ const px = Math.round(cx + (i - 2) * moduleSize);
6935
+ const py = Math.round(cy + (j - 2) * moduleSize);
6936
+ if (px < 0 || py < 0 || px >= image.width || py >= image.height) return false;
6937
+ if (image.get(px, py) === (ALIGNMENT_MODULES[j][i] === 1)) good++;
6938
+ }
6939
+ }
6940
+ // Allow two modules of slop for blur and sampling, but no more.
6941
+ return good >= 23;
6942
+ }
6943
+
6944
+ /**
6945
+ * Look for the bottom-right alignment pattern near where the geometry predicts.
5897
6946
  *
5898
- * @module qr/detector
6947
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
6948
+ * @param {{x: number, y: number}} expected
6949
+ * @param {number} moduleSize
6950
+ * @returns {{x: number, y: number} | null}
5899
6951
  */
5900
- const { NotFoundError } = __require("core/errors.js");
5901
- const { PerspectiveTransform } = __require("image/perspective.js");
5902
- const { sampleQuad } = __require("image/grid-sampler.js");
5903
- const { decodeQR } = __require("qr/decoder.js");
6952
+ function findAlignment(image, expected, moduleSize) {
6953
+ // Three modules of slack. The parallelogram estimate is good to well under
6954
+ // that on any symbol flat enough to decode, and a wider net only admits more
6955
+ // data modules as candidates.
6956
+ const radius = Math.max(3, Math.ceil(moduleSize * 3));
6957
+ const region = {
6958
+ x0: expected.x - radius,
6959
+ x1: expected.x + radius,
6960
+ y0: Math.floor(expected.y - radius),
6961
+ y1: Math.ceil(expected.y + radius),
6962
+ };
5904
6963
 
5905
- /** Finder pattern run ratios, centre run first in the array's own order. */
5906
- const FINDER_RATIOS = [1, 1, 3, 1, 1];
6964
+ const found = findPatterns(image, ALIGNMENT_RATIOS, 5, region);
5907
6965
 
5908
- /** Alignment pattern run ratios. */
5909
- const ALIGNMENT_RATIOS = [1, 1, 1, 1, 1];
6966
+ let best = null;
6967
+ let bestDistance = Infinity;
6968
+ for (let i = 0; i < found.length; i++) {
6969
+ // The alignment pattern is 5 modules across, so its implied module size
6970
+ // should agree with the one the finders reported.
6971
+ if (found[i].moduleSize > moduleSize * 1.5 || found[i].moduleSize < moduleSize / 1.5) continue;
6972
+ if (!verifyAlignment(image, found[i].x, found[i].y, moduleSize)) continue;
6973
+ const d = distance(found[i], expected);
6974
+ if (d < bestDistance) {
6975
+ bestDistance = d;
6976
+ best = found[i];
6977
+ }
6978
+ }
5910
6979
 
5911
- /** Smallest and largest legal symbol dimensions, in modules. */
5912
- const MIN_DIMENSION = 21;
5913
- const MAX_DIMENSION = 177;
6980
+ // Last resort: the pattern is exactly where predicted but its runs were
6981
+ // mangled by blur. Reading the modules directly still confirms it.
6982
+ if (!best && verifyAlignment(image, expected.x, expected.y, moduleSize)) {
6983
+ best = { x: expected.x, y: expected.y };
6984
+ }
6985
+
6986
+ return best;
6987
+ }
5914
6988
 
5915
6989
  /**
5916
- * Do five alternating runs match the expected ratios?
5917
- *
5918
- * Tolerance is half a module, scaled by the ratio, which is the widest band
5919
- * that still rejects ordinary text and rules while accepting the blur and
5920
- * rounding of a real scan.
6990
+ * @typedef {object} Detection
6991
+ * @property {Array<{x: number, y: number}>} corners Outer corners of the
6992
+ * symbol, ordered top-left, top-right, bottom-right, bottom-left.
6993
+ * @property {number} dimension Modules per side.
6994
+ * @property {number} version
6995
+ * @property {number} moduleSize Estimated pixels per module.
6996
+ * @property {boolean} alignmentFound
6997
+ */
6998
+
6999
+ /**
7000
+ * Find QR Code symbols in a binarized image.
5921
7001
  *
5922
- * @param {number[]} counts Five run lengths, dark first.
5923
- * @param {number[]} ratios
5924
- * @returns {number} The implied module size, or 0 if the ratios do not match.
7002
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
7003
+ * @returns {Detection[]} Possibly empty; ordered by descending module size, so
7004
+ * the most prominent symbol comes first.
5925
7005
  */
5926
- function matchRatios(counts, ratios) {
5927
- let total = 0;
5928
- let units = 0;
5929
- for (let i = 0; i < 5; i++) {
5930
- if (counts[i] === 0) return 0;
5931
- total += counts[i];
5932
- units += ratios[i];
7006
+ function detectQR(binaryImage) {
7007
+ if (!binaryImage || !binaryImage.width) {
7008
+ throw new NotFoundError('detectQR: no image supplied');
5933
7009
  }
5934
- if (total < units) return 0;
5935
7010
 
5936
- const moduleSize = total / units;
5937
- const tolerance = moduleSize / 2;
5938
- for (let i = 0; i < 5; i++) {
5939
- if (Math.abs(counts[i] - moduleSize * ratios[i]) > tolerance * ratios[i]) return 0;
5940
- }
5941
- return moduleSize;
5942
- }
7011
+ const finders = findPatterns(binaryImage, FINDER_RATIOS, 7);
7012
+ // A single stray row hit is noise; a real finder is crossed many times.
7013
+ const solid = finders.filter((f) => f.hits >= 2);
7014
+ const pool = solid.length >= 3 ? solid : finders;
7015
+ if (pool.length < 3) return [];
7016
+
7017
+ // Prefer the largest patterns, and cap the combinatorics on noisy images.
7018
+ pool.sort((a, b) => b.moduleSize - a.moduleSize || b.hits - a.hits);
7019
+ const limit = Math.min(pool.length, 12);
5943
7020
 
5944
- /**
5945
- * Scan one row for the finder run ratio.
5946
- *
5947
- * @param {import('../core/bit-matrix.js').BitMatrix} image
5948
- * @param {number} y
5949
- * @param {number[]} ratios
5950
- * @param {(centreX: number, moduleSize: number) => void} onHit
5951
- */
5952
- function scanRow(image, y, ratios, onHit) {
5953
- const width = image.width;
5954
- const counts = [0, 0, 0, 0, 0];
5955
- let state = 0;
7021
+ /** @type {Detection[]} */
7022
+ const detections = [];
7023
+ const used = new Set();
5956
7024
 
5957
- for (let x = 0; x < width; x++) {
5958
- const dark = image.get(x, y);
7025
+ for (let i = 0; i < limit; i++) {
7026
+ for (let j = i + 1; j < limit; j++) {
7027
+ for (let k = j + 1; k < limit; k++) {
7028
+ const three = [pool[i], pool[j], pool[k]];
5959
7029
 
5960
- // Even states are dark runs, odd states light.
5961
- if (dark === ((state & 1) === 0)) {
5962
- counts[state]++;
5963
- continue;
5964
- }
7030
+ // All three finders belong to one symbol, so they share a module size.
7031
+ const sizes = three.map((f) => f.moduleSize);
7032
+ if (Math.max(...sizes) > Math.min(...sizes) * 1.6) continue;
5965
7033
 
5966
- // A leading light margin is not part of any pattern.
5967
- if (state === 0 && counts[0] === 0) continue;
7034
+ const oriented = orientFinders(three);
7035
+ if (!oriented) continue;
5968
7036
 
5969
- if (state < 4) {
5970
- state++;
5971
- counts[state] = 1;
5972
- continue;
5973
- }
7037
+ const { tl, tr, bl } = oriented;
7038
+ const moduleSize = (tl.moduleSize + tr.moduleSize + bl.moduleSize) / 3;
7039
+ if (moduleSize <= 0) continue;
5974
7040
 
5975
- // Five runs complete, and the sixth has begun.
5976
- const moduleSize = matchRatios(counts, ratios);
5977
- if (moduleSize > 0) {
5978
- onHit(x - counts[4] - counts[3] - counts[2] / 2, moduleSize);
5979
- }
7041
+ // Centre-to-centre spans dimension - 7 modules.
7042
+ const across = (distance(tl, tr) + distance(tl, bl)) / 2;
7043
+ const dimension = snapDimension(across / moduleSize + 7);
7044
+ if (dimension === 0) continue;
5980
7045
 
5981
- // Slide the window on by two runs: the trailing dark run of a rejected
5982
- // candidate is often the leading dark run of the real one.
5983
- counts[0] = counts[2];
5984
- counts[1] = counts[3];
5985
- counts[2] = counts[4];
5986
- counts[3] = 1;
5987
- counts[4] = 0;
5988
- state = 3;
5989
- }
7046
+ const version = (dimension - 17) / 4;
7047
+ if (version < 1 || version > 40) continue;
5990
7048
 
5991
- if (state === 4) {
5992
- const moduleSize = matchRatios(counts, ratios);
5993
- if (moduleSize > 0) {
5994
- onHit(width - counts[4] - counts[3] - counts[2] / 2, moduleSize);
7049
+ const key = `${Math.round(tl.x)},${Math.round(tl.y)},${dimension}`;
7050
+ if (used.has(key)) continue;
7051
+ used.add(key);
7052
+
7053
+ detections.push(
7054
+ buildDetection(binaryImage, tl, tr, bl, dimension, version, moduleSize)
7055
+ );
7056
+ }
5995
7057
  }
5996
7058
  }
7059
+
7060
+ detections.sort((a, b) => b.moduleSize - a.moduleSize);
7061
+ return detections;
5997
7062
  }
5998
7063
 
5999
7064
  /**
6000
- * Walk a line through a candidate centre and confirm the ratio holds there too.
7065
+ * Turn three finder centres into four symbol corners.
6001
7066
  *
6002
7067
  * @param {import('../core/bit-matrix.js').BitMatrix} image
6003
- * @param {number} x @param {number} y
6004
- * @param {number} dx @param {number} dy Unit step defining the line.
6005
- * @param {number[]} ratios
6006
- * @param {number} maxRun Guard against running the length of a dark image.
6007
- * @returns {number} Refined centre offset along the line, or NaN.
7068
+ * @param {Candidate} tl @param {Candidate} tr @param {Candidate} bl
7069
+ * @param {number} dimension @param {number} version @param {number} moduleSize
7070
+ * @returns {Detection}
6008
7071
  */
6009
- function crossCheck(image, x, y, dx, dy, ratios, maxRun) {
6010
- const width = image.width;
6011
- const height = image.height;
6012
-
6013
- /** @returns {boolean | null} Null when the sample falls outside the image. */
6014
- const at = (i) => {
6015
- const px = x + dx * i;
6016
- const py = y + dy * i;
6017
- if (px < 0 || py < 0 || px >= width || py >= height) return null;
6018
- return image.get(px, py);
6019
- };
6020
-
6021
- if (at(0) !== true) return NaN;
6022
-
6023
- const counts = [0, 0, 0, 0, 0];
6024
- let i = 0;
6025
-
6026
- // Forward from the centre: rest of the centre run, then light, then dark.
6027
- while (at(i) === true && counts[2] < maxRun) { counts[2]++; i++; }
6028
- if (at(i) === null) return NaN;
6029
- const centreForward = counts[2];
7072
+ function buildDetection(image, tl, tr, bl, dimension, version, moduleSize) {
7073
+ const d = dimension;
7074
+ // Finder centres sit on module (3, 3) and friends, so at grid coordinate 3.5.
7075
+ const gridTl = [3.5, 3.5];
7076
+ const gridTr = [d - 3.5, 3.5];
7077
+ const gridBl = [3.5, d - 3.5];
6030
7078
 
6031
- while (at(i) === false && counts[3] < maxRun) { counts[3]++; i++; }
6032
- if (at(i) === null || counts[3] === 0) return NaN;
7079
+ // Parallelogram estimate of the far corner, used both as the search seed for
7080
+ // the alignment pattern and as the fallback when it is not there.
7081
+ const guess = { x: tr.x + bl.x - tl.x, y: tr.y + bl.y - tl.y };
6033
7082
 
6034
- while (at(i) === true && counts[4] < maxRun) { counts[4]++; i++; }
6035
- if (counts[4] === 0) return NaN;
7083
+ /** @type {PerspectiveTransform | null} */
7084
+ let transform = null;
7085
+ let alignmentFound = false;
6036
7086
 
6037
- // Backward from the centre.
6038
- i = -1;
6039
- while (at(i) === true && counts[2] < maxRun * 2) { counts[2]++; i--; }
6040
- if (at(i) === null) return NaN;
6041
- const centreBackward = counts[2] - centreForward;
7087
+ if (version >= 2) {
7088
+ // The bottom-right alignment pattern is centred on module (d - 7, d - 7).
7089
+ const gridAlign = [d - 6.5, d - 6.5];
7090
+ // Where that module lands under the parallelogram assumption.
7091
+ const seed = {
7092
+ x: tl.x + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.x - tl.x) +
7093
+ ((gridAlign[1] - 3.5) / (d - 7)) * (bl.x - tl.x),
7094
+ y: tl.y + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.y - tl.y) +
7095
+ ((gridAlign[1] - 3.5) / (d - 7)) * (bl.y - tl.y),
7096
+ };
6042
7097
 
6043
- while (at(i) === false && counts[1] < maxRun) { counts[1]++; i--; }
6044
- if (at(i) === null || counts[1] === 0) return NaN;
7098
+ const align = findAlignment(image, seed, moduleSize);
7099
+ if (align) {
7100
+ alignmentFound = true;
7101
+ transform = PerspectiveTransform.quadToQuad(
7102
+ gridTl[0], gridTl[1], gridTr[0], gridTr[1], gridAlign[0], gridAlign[1], gridBl[0], gridBl[1],
7103
+ tl.x, tl.y, tr.x, tr.y, align.x, align.y, bl.x, bl.y
7104
+ );
7105
+ }
7106
+ }
6045
7107
 
6046
- while (at(i) === true && counts[0] < maxRun) { counts[0]++; i--; }
6047
- if (counts[0] === 0) return NaN;
7108
+ const plain = PerspectiveTransform.quadToQuad(
7109
+ gridTl[0], gridTl[1], gridTr[0], gridTr[1], d - 3.5, d - 3.5, gridBl[0], gridBl[1],
7110
+ tl.x, tl.y, tr.x, tr.y, guess.x, guess.y, bl.x, bl.y
7111
+ );
6048
7112
 
6049
- if (matchRatios(counts, ratios) === 0) return NaN;
7113
+ const corners = cornersOf(transform ?? plain, d);
7114
+ // Keep the parallelogram corners as a second opinion whenever an alignment
7115
+ // pattern steered the first set. Verification makes a false match unlikely,
7116
+ // not impossible, and one extra sampling attempt is far cheaper than losing
7117
+ // a symbol to it.
7118
+ const altCorners = transform ? cornersOf(plain, d) : null;
6050
7119
 
6051
- // The centre run spans offsets [-centreBackward, centreForward - 1].
6052
- return (centreForward - 1 - centreBackward) / 2;
7120
+ return { corners, altCorners, dimension, version, moduleSize, alignmentFound };
6053
7121
  }
6054
7122
 
6055
7123
  /**
6056
- * @typedef {object} Candidate
6057
- * @property {number} x
6058
- * @property {number} y
6059
- * @property {number} moduleSize
6060
- * @property {number} hits
7124
+ * The four outer corners of the symbol under a grid-to-image transform.
7125
+ *
7126
+ * @param {PerspectiveTransform} transform @param {number} d
7127
+ * @returns {Array<{x: number, y: number}>}
6061
7128
  */
7129
+ function cornersOf(transform, d) {
7130
+ return [
7131
+ transform.transformPoint(0, 0),
7132
+ transform.transformPoint(d, 0),
7133
+ transform.transformPoint(d, d),
7134
+ transform.transformPoint(0, d),
7135
+ ];
7136
+ }
6062
7137
 
6063
7138
  /**
6064
- * Locate pattern centres of a given run ratio.
7139
+ * Find and decode every QR Code in a binarized image.
6065
7140
  *
6066
- * @param {import('../core/bit-matrix.js').BitMatrix} image
6067
- * @param {number[]} ratios
6068
- * @param {number} unitsWide Modules the pattern spans (7 or 5).
6069
- * @param {{x0: number, y0: number, x1: number, y1: number}} [region]
6070
- * @returns {Candidate[]}
7141
+ * Each candidate gets up to four attempts: a plain centre sample, a 3x3
7142
+ * majority vote for noisy input, and both of those rotated 180 degrees. The
7143
+ * rotation retry matters because three finders in a right isoceles triangle
7144
+ * look identical to the same three rotated half a turn — the orientation is
7145
+ * only settled once the format information reads cleanly, which is to say once
7146
+ * the decode succeeds.
7147
+ *
7148
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
7149
+ * @returns {Array<import('./decoder.js').DecodeResult & {corners: Array<{x: number, y: number}>}>}
7150
+ * Empty when nothing decodes; never throws for "no symbol here".
6071
7151
  */
6072
- function findPatterns(image, ratios, unitsWide, region) {
6073
- /** @type {Candidate[]} */
6074
- const found = [];
6075
-
6076
- const y0 = region ? Math.max(0, region.y0) : 0;
6077
- const y1 = region ? Math.min(image.height, region.y1) : image.height;
7152
+ function detectAndDecodeQR(binaryImage) {
7153
+ let detections;
7154
+ try {
7155
+ detections = detectQR(binaryImage);
7156
+ } catch (e) {
7157
+ return [];
7158
+ }
6078
7159
 
6079
- for (let y = y0; y < y1; y++) {
6080
- scanRow(image, y, ratios, (centreX, moduleSize) => {
6081
- if (region && (centreX < region.x0 || centreX > region.x1)) return;
7160
+ const results = [];
7161
+ const seen = new Set();
6082
7162
 
6083
- const px = Math.floor(centreX);
6084
- const maxRun = Math.ceil(moduleSize * unitsWide);
6085
- const offsetY = crossCheck(image, px, y, 0, 1, ratios, maxRun);
6086
- if (Number.isNaN(offsetY)) return;
7163
+ for (let i = 0; i < detections.length; i++) {
7164
+ const det = detections[i];
6087
7165
 
6088
- const cy = y + offsetY;
6089
- // Re-check horizontally at the refined row, which both confirms the hit
6090
- // and gives a better x than the original scan line did.
6091
- const offsetX = crossCheck(image, px, Math.round(cy), 1, 0, ratios, maxRun);
6092
- if (Number.isNaN(offsetX)) return;
7166
+ for (let attempt = 0; attempt < 4; attempt++) {
7167
+ const voting = attempt === 1 || attempt === 3;
7168
+ const rotated = attempt >= 2;
6093
7169
 
6094
- const cx = px + offsetX;
7170
+ let matrix;
7171
+ try {
7172
+ matrix = sampleQuad(binaryImage, det.dimension, det.corners, voting);
7173
+ } catch (e) {
7174
+ continue;
7175
+ }
7176
+ if (rotated) matrix.rotate180();
6095
7177
 
6096
- // Merge with an existing centre when they describe the same pattern.
6097
- for (let i = 0; i < found.length; i++) {
6098
- const c = found[i];
6099
- if (
6100
- Math.abs(c.x - cx) <= c.moduleSize &&
6101
- Math.abs(c.y - cy) <= c.moduleSize &&
6102
- Math.abs(c.moduleSize - moduleSize) <= Math.max(1, c.moduleSize / 2)
6103
- ) {
6104
- const n = c.hits + 1;
6105
- c.x = (c.x * c.hits + cx) / n;
6106
- c.y = (c.y * c.hits + cy) / n;
6107
- c.moduleSize = (c.moduleSize * c.hits + moduleSize) / n;
6108
- c.hits = n;
6109
- return;
7178
+ try {
7179
+ const result = decodeQR(matrix);
7180
+ // The same symbol can be detected through more than one finder triple.
7181
+ const key = `${result.version}|${result.text}`;
7182
+ if (!seen.has(key)) {
7183
+ seen.add(key);
7184
+ results.push(Object.assign({ corners: det.corners }, result));
6110
7185
  }
7186
+ break;
7187
+ } catch (e) {
7188
+ /* Try the next sampling strategy. */
6111
7189
  }
6112
-
6113
- found.push({ x: cx, y: cy, moduleSize, hits: 1 });
6114
- });
7190
+ }
6115
7191
  }
6116
7192
 
6117
- return found;
7193
+ return results;
6118
7194
  }
6119
7195
 
6120
- /**
6121
- * @param {{x: number, y: number}} a @param {{x: number, y: number}} b
6122
- * @returns {number}
6123
- */
6124
- function distance(a, b) {
6125
- return Math.hypot(a.x - b.x, a.y - b.y);
6126
- }
7196
+ __exports.detectQR = detectQR;
7197
+ __exports.detectAndDecodeQR = detectAndDecodeQR;
7198
+ };
6127
7199
 
7200
+ __modules["qr/index.js"] = function (__require, __exports) {
6128
7201
  /**
6129
- * Order three finder centres as top-left, top-right, bottom-left.
7202
+ * QR Code, re-exported.
6130
7203
  *
6131
- * The corner is the centre opposite the longest side. Which of the remaining
6132
- * two is "top right" follows from the sign of the cross product: in image
6133
- * coordinates, with y increasing downward, a symbol the right way round has
6134
- * (topRight - topLeft) x (bottomLeft - topLeft) positive.
7204
+ * `QR_PLACEHOLDER` is deliberately absent: `src/index.js` probes for it to
7205
+ * decide whether this build can read and write QR, and its absence is what
7206
+ * reports the format as available.
6135
7207
  *
6136
- * @param {Candidate[]} three
6137
- * @returns {{tl: Candidate, tr: Candidate, bl: Candidate} | null}
7208
+ * @module qr
6138
7209
  */
6139
- function orientFinders(three) {
6140
- const [a, b, c] = three;
6141
- const ab = distance(a, b);
6142
- const bc = distance(b, c);
6143
- const ca = distance(c, a);
7210
+ const __reexport0 = __require("qr/encoder.js"); __exports.encodeQR = __reexport0.encodeQR;
7211
+ const __reexport1 = __require("qr/decoder.js"); __exports.decodeQR = __reexport1.decodeQR;
7212
+ const __reexport2 = __require("qr/detector.js"); __exports.detectQR = __reexport2.detectQR; __exports.detectAndDecodeQR = __reexport2.detectAndDecodeQR;
7213
+ const __reexport3 = __require("qr/tables.js"); __exports.validateTables = __reexport3.validateTables;
6144
7214
 
6145
- let tl, p, q, hypotenuse, leg1, leg2;
6146
- if (ab >= bc && ab >= ca) {
6147
- tl = c; p = a; q = b; hypotenuse = ab; leg1 = ca; leg2 = bc;
6148
- } else if (bc >= ab && bc >= ca) {
6149
- tl = a; p = b; q = c; hypotenuse = bc; leg1 = ab; leg2 = ca;
6150
- } else {
6151
- tl = b; p = c; q = a; hypotenuse = ca; leg1 = bc; leg2 = ab;
6152
- }
6153
7215
 
6154
- if (leg1 === 0 || leg2 === 0) return null;
7216
+ };
6155
7217
 
6156
- // The two legs must be near enough equal, and Pythagoras must hold: this is
6157
- // what rejects three unrelated finder-lookalikes that happen to co-occur.
6158
- const ratio = leg1 / leg2;
6159
- if (ratio < 0.7 || ratio > 1.4) return null;
6160
- const expected = Math.hypot(leg1, leg2);
6161
- if (Math.abs(hypotenuse - expected) > expected * 0.25) return null;
7218
+ __modules["aztec/high-level.js"] = function (__require, __exports) {
7219
+ /**
7220
+ * Aztec high-level stream writer.
7221
+ *
7222
+ * The output is deliberately a `BitWriter`, rather than a byte array: Aztec's
7223
+ * text controls and binary-shift lengths are not byte aligned. This module is
7224
+ * also the boundary where JavaScript strings become UTF-8. Passing a byte
7225
+ * view bypasses that conversion and preserves every octet unchanged.
7226
+ *
7227
+ * The initial state mandated by the symbology is UPPER. The greedy text pass
7228
+ * uses UPPER, LOWER, DIGIT and PUNCT tables, selecting the shortest available
7229
+ * latch at each byte. Bytes without a text-table representation are emitted
7230
+ * through the standard B/S (binary shift) escape. B/S is available from
7231
+ * UPPER and makes this a complete, lossless representation of UTF-8 payloads.
7232
+ *
7233
+ * @module aztec/high-level
7234
+ */
7235
+ const { BitWriter } = __require("core/bit-buffer.js");
7236
+ const { EncodeError } = __require("core/errors.js");
6162
7237
 
6163
- const cross = (p.x - tl.x) * (q.y - tl.y) - (p.y - tl.y) * (q.x - tl.x);
6164
- return cross >= 0 ? { tl, tr: p, bl: q } : { tl, tr: q, bl: p };
6165
- }
7238
+ /** Aztec high-level table identifiers, exposed for decoder/API symmetry. */
7239
+ const HIGH_LEVEL_MODE = Object.freeze({
7240
+ UPPER: 0,
7241
+ LOWER: 1,
7242
+ DIGIT: 2,
7243
+ MIXED: 3,
7244
+ PUNCT: 4,
7245
+ });
7246
+
7247
+ /** Maximum number of bytes represented by one B/S escape. */
7248
+ const MAX_BINARY_SHIFT = 2078;
6166
7249
 
6167
7250
  /**
6168
- * Snap a measured dimension to a legal symbol size.
6169
- *
6170
- * Every QR dimension is 17 + 4v, so `dimension % 4 === 1`. A measurement one
6171
- * off is rounding; two off means the module size estimate is wrong and the
6172
- * candidate is not worth pursuing.
7251
+ * Convert accepted public input to its encoded octets.
6173
7252
  *
6174
- * @param {number} raw
6175
- * @returns {number} 0 if it cannot be reconciled.
7253
+ * @param {string|ArrayBuffer|ArrayBufferView} value
7254
+ * @param {'utf-8'} [charset]
7255
+ * @returns {Uint8Array}
6176
7256
  */
6177
- function snapDimension(raw) {
6178
- let d = Math.round(raw);
6179
- switch (d & 3) {
6180
- case 0: d--; break;
6181
- case 2: d++; break;
6182
- case 3: return 0;
6183
- default: break;
7257
+ function aztecBytes(value, charset = 'utf-8') {
7258
+ if (charset !== 'utf-8') throw new EncodeError(`Aztec: unsupported charset "${charset}"`);
7259
+ if (typeof value === 'string') return new TextEncoder().encode(value);
7260
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
7261
+ if (ArrayBuffer.isView(value)) {
7262
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
7263
+ }
7264
+ throw new EncodeError('Aztec: value must be a string, ArrayBuffer, or byte view');
7265
+ }
7266
+
7267
+ /** @param {number} byte @returns {number} UPPER-table value, or -1. */
7268
+ function upperValue(byte) {
7269
+ if (byte === 0x20) return 1;
7270
+ if (byte >= 0x41 && byte <= 0x5a) return byte - 0x41 + 2;
7271
+ return -1;
7272
+ }
7273
+
7274
+ /** Aztec's latch table, packed as `(bitCount << 16) | bits`. */
7275
+ const LATCH = Object.freeze([
7276
+ [0, 327708, 327710, 327709, 656318],
7277
+ [590318, 0, 327710, 327709, 656318],
7278
+ [262158, 590300, 0, 590301, 932798],
7279
+ [327709, 327708, 656322, 0, 327710],
7280
+ [327711, 656380, 656382, 656381, 0],
7281
+ ]);
7282
+
7283
+ /** @param {number} byte @returns {number} */
7284
+ function lowerValue(byte) {
7285
+ if (byte === 0x20) return 1;
7286
+ if (byte >= 0x61 && byte <= 0x7a) return byte - 0x61 + 2;
7287
+ return -1;
7288
+ }
7289
+
7290
+ /** @param {number} byte @returns {number} */
7291
+ function digitValue(byte) {
7292
+ if (byte === 0x20) return 1;
7293
+ if (byte >= 0x30 && byte <= 0x39) return byte - 0x30 + 2;
7294
+ if (byte === 0x2c) return 12;
7295
+ if (byte === 0x2e) return 13;
7296
+ return -1;
7297
+ }
7298
+
7299
+ const PUNCT = new Map([
7300
+ [0x0d, 1], [0x21, 6], [0x22, 7], [0x23, 8], [0x24, 9], [0x25, 10],
7301
+ [0x26, 11], [0x27, 12], [0x28, 13], [0x29, 14], [0x2a, 15], [0x2b, 16],
7302
+ [0x2c, 17], [0x2d, 18], [0x2e, 19], [0x2f, 20], [0x3a, 21], [0x3b, 22],
7303
+ [0x3c, 23], [0x3d, 24], [0x3e, 25], [0x3f, 26], [0x5b, 27], [0x5d, 28],
7304
+ [0x7b, 29], [0x7d, 30],
7305
+ ]);
7306
+
7307
+ /** @param {number} byte @param {number} mode @returns {number} */
7308
+ function textValue(byte, mode) {
7309
+ switch (mode) {
7310
+ case HIGH_LEVEL_MODE.UPPER: return upperValue(byte);
7311
+ case HIGH_LEVEL_MODE.LOWER: return lowerValue(byte);
7312
+ case HIGH_LEVEL_MODE.DIGIT: return digitValue(byte);
7313
+ case HIGH_LEVEL_MODE.PUNCT: return PUNCT.get(byte) ?? -1;
7314
+ default: return -1;
6184
7315
  }
6185
- if (d < MIN_DIMENSION || d > MAX_DIMENSION) return 0;
6186
- return d;
6187
7316
  }
6188
7317
 
6189
- /** The alignment pattern, as modules. 1 is dark. */
6190
- const ALIGNMENT_MODULES = [
6191
- [1, 1, 1, 1, 1],
6192
- [1, 0, 0, 0, 1],
6193
- [1, 0, 1, 0, 1],
6194
- [1, 0, 0, 0, 1],
6195
- [1, 1, 1, 1, 1],
6196
- ];
7318
+ /** @param {BitWriter} writer @param {number} from @param {number} to */
7319
+ function latch(writer, from, to) {
7320
+ if (from === to) return;
7321
+ const packed = LATCH[from][to];
7322
+ writer.put(packed & 0xffff, packed >>> 16);
7323
+ }
7324
+
7325
+ /** @param {number} mode @returns {number} */
7326
+ function characterWidth(mode) {
7327
+ return mode === HIGH_LEVEL_MODE.DIGIT ? 4 : 5;
7328
+ }
6197
7329
 
6198
7330
  /**
6199
- * Confirm a candidate by reading the 5x5 module block it claims to be.
7331
+ * Write an Aztec binary-shift segment while in UPPER mode.
6200
7332
  *
6201
- * The run-ratio scan alone is not enough here. A finder pattern's 1:1:3:1:1 is
6202
- * rare enough to stand on its own, but an alignment pattern's 1:1:1:1:1 occurs
6203
- * constantly in ordinary data modules, so an unverified match near the expected
6204
- * position is more likely to be payload than pattern — and a false match drags
6205
- * the fourth corner off by several modules, which is worse than having no
6206
- * alignment pattern at all.
7333
+ * B/S is `11111`; its five-bit length directly covers 1..31 bytes. A zero
7334
+ * length selects the extended eleven-bit form, whose stored value is n - 31.
7335
+ * Splitting at 2078 keeps each control representable and makes arbitrarily
7336
+ * long byte input well-defined.
6207
7337
  *
6208
- * @param {import('../core/bit-matrix.js').BitMatrix} image
6209
- * @param {number} cx @param {number} cy @param {number} moduleSize
6210
- * @returns {boolean}
7338
+ * @param {BitWriter} writer
7339
+ * @param {Uint8Array} bytes
7340
+ * @param {number} start
7341
+ * @param {number} length
6211
7342
  */
6212
- function verifyAlignment(image, cx, cy, moduleSize) {
6213
- let good = 0;
6214
- for (let j = 0; j < 5; j++) {
6215
- for (let i = 0; i < 5; i++) {
6216
- const px = Math.round(cx + (i - 2) * moduleSize);
6217
- const py = Math.round(cy + (j - 2) * moduleSize);
6218
- if (px < 0 || py < 0 || px >= image.width || py >= image.height) return false;
6219
- if (image.get(px, py) === (ALIGNMENT_MODULES[j][i] === 1)) good++;
7343
+ function writeBinaryShift(writer, bytes, start, length) {
7344
+ let at = start;
7345
+ let left = length;
7346
+ while (left > 0) {
7347
+ const count = Math.min(left, MAX_BINARY_SHIFT);
7348
+ writer.put(31, 5); // UPPER B/S
7349
+ if (count <= 31) writer.put(count, 5);
7350
+ else {
7351
+ writer.put(0, 5);
7352
+ writer.put(count - 31, 11);
6220
7353
  }
7354
+ for (let i = 0; i < count; i++) writer.put(bytes[at + i], 8);
7355
+ at += count;
7356
+ left -= count;
6221
7357
  }
6222
- // Allow two modules of slop for blur and sampling, but no more.
6223
- return good >= 23;
6224
7358
  }
6225
7359
 
6226
7360
  /**
6227
- * Look for the bottom-right alignment pattern near where the geometry predicts.
7361
+ * Build a valid Aztec high-level bitstream.
6228
7362
  *
6229
- * @param {import('../core/bit-matrix.js').BitMatrix} image
6230
- * @param {{x: number, y: number}} expected
6231
- * @param {number} moduleSize
6232
- * @returns {{x: number, y: number} | null}
7363
+ * @param {string|ArrayBuffer|ArrayBufferView} value
7364
+ * @param {{charset?: 'utf-8'}} [options]
7365
+ * @returns {BitWriter}
6233
7366
  */
6234
- function findAlignment(image, expected, moduleSize) {
6235
- // Three modules of slack. The parallelogram estimate is good to well under
6236
- // that on any symbol flat enough to decode, and a wider net only admits more
6237
- // data modules as candidates.
6238
- const radius = Math.max(3, Math.ceil(moduleSize * 3));
6239
- const region = {
6240
- x0: expected.x - radius,
6241
- x1: expected.x + radius,
6242
- y0: Math.floor(expected.y - radius),
6243
- y1: Math.ceil(expected.y + radius),
6244
- };
6245
-
6246
- const found = findPatterns(image, ALIGNMENT_RATIOS, 5, region);
6247
-
6248
- let best = null;
6249
- let bestDistance = Infinity;
6250
- for (let i = 0; i < found.length; i++) {
6251
- // The alignment pattern is 5 modules across, so its implied module size
6252
- // should agree with the one the finders reported.
6253
- if (found[i].moduleSize > moduleSize * 1.5 || found[i].moduleSize < moduleSize / 1.5) continue;
6254
- if (!verifyAlignment(image, found[i].x, found[i].y, moduleSize)) continue;
6255
- const d = distance(found[i], expected);
6256
- if (d < bestDistance) {
6257
- bestDistance = d;
6258
- best = found[i];
7367
+ function encodeHighLevel(value, options = {}) {
7368
+ const bytes = aztecBytes(value, options.charset ?? 'utf-8');
7369
+ const writer = new BitWriter();
7370
+ let mode = HIGH_LEVEL_MODE.UPPER;
7371
+
7372
+ for (let at = 0; at < bytes.length;) {
7373
+ let bestMode = -1;
7374
+ let bestValue = -1;
7375
+ let bestCost = Number.POSITIVE_INFINITY;
7376
+ for (const candidate of [HIGH_LEVEL_MODE.UPPER, HIGH_LEVEL_MODE.LOWER, HIGH_LEVEL_MODE.DIGIT, HIGH_LEVEL_MODE.PUNCT]) {
7377
+ const value = textValue(bytes[at], candidate);
7378
+ if (value < 0) continue;
7379
+ const latchCost = candidate === mode ? 0 : LATCH[mode][candidate] >>> 16;
7380
+ const cost = latchCost + characterWidth(candidate);
7381
+ if (cost < bestCost) { bestCost = cost; bestMode = candidate; bestValue = value; }
7382
+ }
7383
+ if (bestMode >= 0) {
7384
+ latch(writer, mode, bestMode);
7385
+ writer.put(bestValue, characterWidth(bestMode));
7386
+ mode = bestMode;
7387
+ at++;
7388
+ } else {
7389
+ // B/S is defined from UPPER; the latch is retained after the shift.
7390
+ latch(writer, mode, HIGH_LEVEL_MODE.UPPER);
7391
+ mode = HIGH_LEVEL_MODE.UPPER;
7392
+ const start = at;
7393
+ while (at < bytes.length && ![HIGH_LEVEL_MODE.UPPER, HIGH_LEVEL_MODE.LOWER, HIGH_LEVEL_MODE.DIGIT, HIGH_LEVEL_MODE.PUNCT].some((m) => textValue(bytes[at], m) >= 0)) at++;
7394
+ writeBinaryShift(writer, bytes, start, at - start);
6259
7395
  }
6260
7396
  }
7397
+ return writer;
7398
+ }
6261
7399
 
6262
- // Last resort: the pattern is exactly where predicted but its runs were
6263
- // mangled by blur. Reading the modules directly still confirms it.
6264
- if (!best && verifyAlignment(image, expected.x, expected.y, moduleSize)) {
6265
- best = { x: expected.x, y: expected.y };
6266
- }
7400
+ __exports.HIGH_LEVEL_MODE = HIGH_LEVEL_MODE;
7401
+ __exports.MAX_BINARY_SHIFT = MAX_BINARY_SHIFT;
7402
+ __exports.aztecBytes = aztecBytes;
7403
+ __exports.writeBinaryShift = writeBinaryShift;
7404
+ __exports.encodeHighLevel = encodeHighLevel;
7405
+ };
6267
7406
 
6268
- return best;
7407
+ __modules["aztec/tables.js"] = function (__require, __exports) {
7408
+ /**
7409
+ * Aztec Code layer geometry and Reed-Solomon parameters.
7410
+ *
7411
+ * `totalBits` counts the payload ring before its leading pad bits are added;
7412
+ * consequently only `usableBits` can be partitioned into codewords. Compact
7413
+ * symbols have no reference grid. Full symbols insert alternating reference
7414
+ * rows and columns every 16 modules around the centre.
7415
+ *
7416
+ * The five data fields use generator base 1. GF(256)/DataMatrix is also the
7417
+ * Aztec 8-bit field: both use primitive polynomial 0x12d.
7418
+ *
7419
+ * @module aztec/tables
7420
+ */
7421
+ const { GF16, GF64, GF256_AZTEC, GF1024, GF4096 } = __require("core/galois-field.js");
7422
+
7423
+ /** Reed-Solomon generator base defined for Aztec parameter and data fields. */
7424
+ const AZTEC_RS_GENERATOR_BASE = 1;
7425
+
7426
+ /** Minimum recommended error correction: 23 percent plus three codewords. */
7427
+ const AZTEC_DEFAULT_ECC_PERCENT = 23;
7428
+ const AZTEC_MIN_ECC_WORDS = 3;
7429
+
7430
+ /** Word size selected solely by the number of layers. */
7431
+ function wordSizeForLayers(layers) {
7432
+ if (!Number.isInteger(layers) || layers < 1 || layers > 32) {
7433
+ throw new RangeError(`Aztec: layers must be an integer from 1 to 32 (got ${layers})`);
7434
+ }
7435
+ if (layers <= 2) return 6;
7436
+ if (layers <= 8) return 8;
7437
+ if (layers <= 22) return 10;
7438
+ return 12;
7439
+ }
7440
+
7441
+ /** Return the field used by Aztec codewords of `wordSize` bits. */
7442
+ function fieldForWordSize(wordSize) {
7443
+ switch (wordSize) {
7444
+ case 4: return GF16; // Mode message only.
7445
+ case 6: return GF64;
7446
+ case 8: return GF256_AZTEC;
7447
+ case 10: return GF1024;
7448
+ case 12: return GF4096;
7449
+ default: throw new RangeError(`Aztec: unsupported codeword size ${wordSize}`);
7450
+ }
7451
+ }
7452
+
7453
+ /** Return the data field selected for a symbol with `layers` layers. */
7454
+ function fieldForLayers(layers) {
7455
+ return fieldForWordSize(wordSizeForLayers(layers));
7456
+ }
7457
+
7458
+ /** Matrix side length, including Full-mode reference grid lines. */
7459
+ function aztecSymbolSize(layers, compact = false) {
7460
+ if (!Number.isInteger(layers) || layers < 1 || layers > (compact ? 4 : 32)) {
7461
+ throw new RangeError(`Aztec: ${compact ? 'Compact' : 'Full'} layers out of range: ${layers}`);
7462
+ }
7463
+ if (compact) return 11 + 4 * layers;
7464
+ const baseMatrixSize = 14 + 4 * layers;
7465
+ return baseMatrixSize + 1 + 2 * Math.floor((baseMatrixSize / 2 - 1) / 15);
7466
+ }
7467
+
7468
+ function layer(layers, compact) {
7469
+ const wordSize = wordSizeForLayers(layers);
7470
+ const totalBits = ((compact ? 88 : 112) + 16 * layers) * layers;
7471
+ const usableBits = totalBits - totalBits % wordSize;
7472
+ const totalCodewords = usableBits / wordSize;
7473
+ const baseMatrixSize = (compact ? 11 : 14) + 4 * layers;
7474
+ return Object.freeze({
7475
+ compact,
7476
+ layers,
7477
+ wordSize,
7478
+ totalBits,
7479
+ usableBits,
7480
+ totalCodewords,
7481
+ // Compact mode encodes the count in six bits and can therefore hold no
7482
+ // more than 64 data codewords even where the ring itself is larger.
7483
+ maxDataCodewords: compact ? Math.min(totalCodewords, 64) : totalCodewords,
7484
+ baseMatrixSize,
7485
+ symbolSize: aztecSymbolSize(layers, compact),
7486
+ modeMessageDataWords: compact ? 2 : 4,
7487
+ modeMessageWords: compact ? 7 : 10,
7488
+ modeMessageBits: compact ? 28 : 40,
7489
+ rsGeneratorBase: AZTEC_RS_GENERATOR_BASE,
7490
+ });
7491
+ }
7492
+
7493
+ /** Compact Aztec layers 1 through 4, in encoding preference order. */
7494
+ const AZTEC_COMPACT_LAYERS = Object.freeze(
7495
+ Array.from({ length: 4 }, (_, i) => layer(i + 1, true)),
7496
+ );
7497
+
7498
+ /** Full Aztec layers 1 through 32, in ascending layer order. */
7499
+ const AZTEC_FULL_LAYERS = Object.freeze(
7500
+ Array.from({ length: 32 }, (_, i) => layer(i + 1, false)),
7501
+ );
7502
+
7503
+ /** All allowed symbols. Compact entries precede Full entries for automatic selection. */
7504
+ const AZTEC_LAYERS = Object.freeze([
7505
+ ...AZTEC_COMPACT_LAYERS,
7506
+ ...AZTEC_FULL_LAYERS,
7507
+ ]);
7508
+
7509
+ /** Return one immutable layer record. */
7510
+ function aztecLayer(layers, compact = false) {
7511
+ if (!Number.isInteger(layers) || layers < 1 || layers > (compact ? 4 : 32)) {
7512
+ throw new RangeError(`Aztec: ${compact ? 'Compact' : 'Full'} layers out of range: ${layers}`);
7513
+ }
7514
+ return (compact ? AZTEC_COMPACT_LAYERS : AZTEC_FULL_LAYERS)[layers - 1];
6269
7515
  }
6270
7516
 
6271
7517
  /**
6272
- * @typedef {object} Detection
6273
- * @property {Array<{x: number, y: number}>} corners Outer corners of the
6274
- * symbol, ordered top-left, top-right, bottom-right, bottom-left.
6275
- * @property {number} dimension Modules per side.
6276
- * @property {number} version
6277
- * @property {number} moduleSize Estimated pixels per module.
6278
- * @property {boolean} alignmentFound
7518
+ * Calculate the minimum parity count for a data word count.
7519
+ *
7520
+ * The percentage is rounded up because a fractional codeword cannot be
7521
+ * emitted. The mandatory three words protect short payloads, where a bare
7522
+ * percentage would otherwise round to zero.
6279
7523
  */
7524
+ function eccCodewordsFor(dataCodewords, eccPercent = AZTEC_DEFAULT_ECC_PERCENT) {
7525
+ if (!Number.isInteger(dataCodewords) || dataCodewords < 0) {
7526
+ throw new RangeError(`Aztec: data codewords must be a non-negative integer (got ${dataCodewords})`);
7527
+ }
7528
+ if (!Number.isFinite(eccPercent) || eccPercent < 0 || eccPercent > 100) {
7529
+ throw new RangeError(`Aztec: ECC percent must be between 0 and 100 (got ${eccPercent})`);
7530
+ }
7531
+ return Math.ceil(dataCodewords * eccPercent / 100) + AZTEC_MIN_ECC_WORDS;
7532
+ }
6280
7533
 
6281
7534
  /**
6282
- * Find QR Code symbols in a binarized image.
7535
+ * Choose the first symbol which holds an already stuffed payload.
6283
7536
  *
6284
- * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
6285
- * @returns {Detection[]} Possibly empty; ordered by descending module size, so
6286
- * the most prominent symbol comes first.
7537
+ * `dataBits` must be a multiple of the candidate word size; callers which
7538
+ * start from high-level bits must stuff separately per candidate word size.
6287
7539
  */
6288
- function detectQR(binaryImage) {
6289
- if (!binaryImage || !binaryImage.width) {
6290
- throw new NotFoundError('detectQR: no image supplied');
7540
+ function selectAztecLayer(dataBits, {
7541
+ eccPercent = AZTEC_DEFAULT_ECC_PERCENT,
7542
+ layers = null,
7543
+ compact = null,
7544
+ } = {}) {
7545
+ if (!Number.isInteger(dataBits) || dataBits < 0) {
7546
+ throw new RangeError(`Aztec: data bits must be a non-negative integer (got ${dataBits})`);
7547
+ }
7548
+ if (compact !== null && typeof compact !== 'boolean') {
7549
+ throw new TypeError('Aztec: compact must be true, false or null');
6291
7550
  }
6292
7551
 
6293
- const finders = findPatterns(binaryImage, FINDER_RATIOS, 7);
6294
- // A single stray row hit is noise; a real finder is crossed many times.
6295
- const solid = finders.filter((f) => f.hits >= 2);
6296
- const pool = solid.length >= 3 ? solid : finders;
6297
- if (pool.length < 3) return [];
7552
+ let candidates;
7553
+ if (layers !== null) {
7554
+ if (compact === null) throw new TypeError('Aztec: compact must be specified when layers is specified');
7555
+ candidates = [aztecLayer(layers, compact)];
7556
+ } else if (compact === null) {
7557
+ candidates = AZTEC_LAYERS;
7558
+ } else {
7559
+ candidates = compact ? AZTEC_COMPACT_LAYERS : AZTEC_FULL_LAYERS;
7560
+ }
6298
7561
 
6299
- // Prefer the largest patterns, and cap the combinatorics on noisy images.
6300
- pool.sort((a, b) => b.moduleSize - a.moduleSize || b.hits - a.hits);
6301
- const limit = Math.min(pool.length, 12);
7562
+ for (const candidate of candidates) {
7563
+ if (dataBits % candidate.wordSize !== 0) continue;
7564
+ const dataCodewords = dataBits / candidate.wordSize;
7565
+ const eccCodewords = eccCodewordsFor(dataCodewords, eccPercent);
7566
+ if (dataCodewords <= candidate.maxDataCodewords &&
7567
+ dataCodewords + eccCodewords <= candidate.totalCodewords) {
7568
+ return Object.freeze({ ...candidate, dataCodewords, eccCodewords });
7569
+ }
7570
+ }
6302
7571
 
6303
- /** @type {Detection[]} */
6304
- const detections = [];
6305
- const used = new Set();
7572
+ throw new RangeError('Aztec: payload and requested error correction do not fit an available symbol');
7573
+ }
6306
7574
 
6307
- for (let i = 0; i < limit; i++) {
6308
- for (let j = i + 1; j < limit; j++) {
6309
- for (let k = j + 1; k < limit; k++) {
6310
- const three = [pool[i], pool[j], pool[k]];
7575
+ /** Check static identities so table corruption fails explicitly in tests. */
7576
+ function validateAztecTables() {
7577
+ const issues = [];
7578
+ for (const entry of AZTEC_LAYERS) {
7579
+ if (entry.usableBits % entry.wordSize !== 0) issues.push(`${entry.compact ? 'C' : 'F'}${entry.layers}: unaligned usable bits`);
7580
+ if (entry.totalCodewords !== entry.usableBits / entry.wordSize) issues.push(`${entry.compact ? 'C' : 'F'}${entry.layers}: codeword mismatch`);
7581
+ if (entry.symbolSize !== aztecSymbolSize(entry.layers, entry.compact)) issues.push(`${entry.compact ? 'C' : 'F'}${entry.layers}: matrix size mismatch`);
7582
+ if (entry.rsGeneratorBase !== AZTEC_RS_GENERATOR_BASE) issues.push(`${entry.compact ? 'C' : 'F'}${entry.layers}: generator base mismatch`);
7583
+ if (entry.compact && entry.maxDataCodewords > 64) issues.push(`C${entry.layers}: Compact data-word limit exceeded`);
7584
+ }
7585
+ return issues;
7586
+ }
6311
7587
 
6312
- // All three finders belong to one symbol, so they share a module size.
6313
- const sizes = three.map((f) => f.moduleSize);
6314
- if (Math.max(...sizes) > Math.min(...sizes) * 1.6) continue;
7588
+ __exports.AZTEC_RS_GENERATOR_BASE = AZTEC_RS_GENERATOR_BASE;
7589
+ __exports.AZTEC_DEFAULT_ECC_PERCENT = AZTEC_DEFAULT_ECC_PERCENT;
7590
+ __exports.AZTEC_MIN_ECC_WORDS = AZTEC_MIN_ECC_WORDS;
7591
+ __exports.wordSizeForLayers = wordSizeForLayers;
7592
+ __exports.fieldForWordSize = fieldForWordSize;
7593
+ __exports.fieldForLayers = fieldForLayers;
7594
+ __exports.aztecSymbolSize = aztecSymbolSize;
7595
+ __exports.AZTEC_COMPACT_LAYERS = AZTEC_COMPACT_LAYERS;
7596
+ __exports.AZTEC_FULL_LAYERS = AZTEC_FULL_LAYERS;
7597
+ __exports.AZTEC_LAYERS = AZTEC_LAYERS;
7598
+ __exports.aztecLayer = aztecLayer;
7599
+ __exports.eccCodewordsFor = eccCodewordsFor;
7600
+ __exports.selectAztecLayer = selectAztecLayer;
7601
+ __exports.validateAztecTables = validateAztecTables;
7602
+ };
6315
7603
 
6316
- const oriented = orientFinders(three);
6317
- if (!oriented) continue;
7604
+ __modules["aztec/encoder.js"] = function (__require, __exports) {
7605
+ /**
7606
+ * Aztec encoder: high-level bits, bit stuffing, Reed-Solomon and matrix layout.
7607
+ *
7608
+ * `tables.js` is intentionally the source of geometry and field selection.
7609
+ * Its `aztecLayer(layers, compact)` entries must expose `totalBits`,
7610
+ * `totalCodewords`, `baseMatrixSize` and `symbolSize`; `fieldForLayers()` must
7611
+ * return the matching binary field. All Aztec Reed-Solomon generators start
7612
+ * at alpha^1, hence the explicit base `1` in both data and mode messages.
7613
+ *
7614
+ * @module aztec/encoder
7615
+ */
7616
+ const { BitWriter } = __require("core/bit-buffer.js");
7617
+ const { BitMatrix } = __require("core/bit-matrix.js");
7618
+ const { EncodeError } = __require("core/errors.js");
7619
+ const { rsEncode } = __require("core/reed-solomon.js");
7620
+ const { encodeHighLevel } = __require("aztec/high-level.js");
7621
+ const { AZTEC_COMPACT_LAYERS, AZTEC_FULL_LAYERS, aztecLayer, eccCodewordsFor, fieldForLayers, fieldForWordSize, wordSizeForLayers } = __require("aztec/tables.js");
6318
7622
 
6319
- const { tl, tr, bl } = oriented;
6320
- const moduleSize = (tl.moduleSize + tr.moduleSize + bl.moduleSize) / 3;
6321
- if (moduleSize <= 0) continue;
7623
+ /** @param {BitWriter} bits @param {number} at @returns {boolean} */
7624
+ function bitAt(bits, at) {
7625
+ return at >= 0 && at < bits.length && ((bits.bytes[at >>> 3] >>> (7 - (at & 7))) & 1) !== 0;
7626
+ }
6322
7627
 
6323
- // Centre-to-centre spans dimension - 7 modules.
6324
- const across = (distance(tl, tr) + distance(tl, bl)) / 2;
6325
- const dimension = snapDimension(across / moduleSize + 7);
6326
- if (dimension === 0) continue;
7628
+ /** @param {BitWriter} bits @param {number} from @param {number} count @returns {number} */
7629
+ function readBits(bits, from, count) {
7630
+ let value = 0;
7631
+ for (let i = 0; i < count; i++) value = (value << 1) | (bitAt(bits, from + i) ? 1 : 0);
7632
+ return value;
7633
+ }
7634
+
7635
+ /**
7636
+ * Prevent all-zero and all-one codewords except their final bit. The final
7637
+ * bit is intentionally re-consumed after a stuffed word; it is the mechanism
7638
+ * that makes the transform injective and reversible.
7639
+ *
7640
+ * @param {BitWriter} bits @param {number} wordSize @returns {BitWriter}
7641
+ */
7642
+ function stuffBits(bits, wordSize) {
7643
+ const out = new BitWriter();
7644
+ const reserved = (1 << wordSize) - 2;
7645
+ for (let at = 0; at < bits.length; at += wordSize) {
7646
+ const word = readBits(bits, at, wordSize);
7647
+ if ((word & reserved) === reserved) {
7648
+ out.put(word & reserved, wordSize);
7649
+ at--;
7650
+ } else if ((word & reserved) === 0) {
7651
+ out.put(word | 1, wordSize);
7652
+ at--;
7653
+ } else {
7654
+ out.put(word, wordSize);
7655
+ }
7656
+ }
7657
+ return out;
7658
+ }
7659
+
7660
+ /**
7661
+ * Add systematic Aztec Reed-Solomon parity and the leading alignment bits.
7662
+ * @param {BitWriter} data @param {number} totalBits @param {number} wordSize
7663
+ * @param {import('../core/galois-field.js').GaloisField} field
7664
+ * @returns {{bits: BitWriter, dataWords: number, eccWords: number}}
7665
+ */
7666
+ function addCheckWords(data, totalBits, wordSize, field) {
7667
+ const totalWords = Math.floor(totalBits / wordSize);
7668
+ const dataWords = Math.ceil(data.length / wordSize);
7669
+ if (dataWords > totalWords) throw new EncodeError('Aztec: data codewords exceed layer capacity');
7670
+ const eccWords = totalWords - dataWords;
7671
+ const words = new Array(dataWords);
7672
+ for (let i = 0; i < dataWords; i++) words[i] = readBits(data, i * wordSize, wordSize);
7673
+ const ecc = rsEncode(words, eccWords, field, 1);
7674
+ const out = new BitWriter();
7675
+ out.put(0, totalBits % wordSize);
7676
+ for (const word of words) out.put(word, wordSize);
7677
+ for (const word of ecc) out.put(word, wordSize);
7678
+ return { bits: out, dataWords, eccWords };
7679
+ }
7680
+
7681
+ /** @param {number} layers @param {number} dataWords @param {boolean} compact @returns {BitWriter} */
7682
+ function modeMessage(layers, dataWords, compact) {
7683
+ const raw = new BitWriter();
7684
+ if (compact) {
7685
+ raw.put(layers - 1, 2);
7686
+ raw.put(dataWords - 1, 6);
7687
+ return addCheckWords(raw, 28, 4, fieldForWordSize(4)).bits;
7688
+ }
7689
+ raw.put(layers - 1, 5);
7690
+ raw.put(dataWords - 1, 11);
7691
+ return addCheckWords(raw, 40, 4, fieldForWordSize(4)).bits;
7692
+ }
7693
+
7694
+ /** @param {BitMatrix} matrix @param {number} center @param {number} size */
7695
+ function drawBullsEye(matrix, center, size) {
7696
+ for (let ring = 0; ring < size; ring += 2) {
7697
+ for (let p = center - ring; p <= center + ring; p++) {
7698
+ matrix.set(p, center - ring); matrix.set(p, center + ring);
7699
+ matrix.set(center - ring, p); matrix.set(center + ring, p);
7700
+ }
7701
+ }
7702
+ matrix.set(center - size, center - size);
7703
+ matrix.set(center - size + 1, center - size);
7704
+ matrix.set(center - size, center - size + 1);
7705
+ matrix.set(center + size, center - size);
7706
+ matrix.set(center + size, center - size + 1);
7707
+ matrix.set(center + size, center + size - 1);
7708
+ }
7709
+
7710
+ /** @param {BitMatrix} matrix @param {BitWriter} message @param {boolean} compact @param {number} center */
7711
+ function drawModeMessage(matrix, message, compact, center) {
7712
+ if (compact) {
7713
+ for (let i = 0; i < 7; i++) {
7714
+ const offset = center - 3 + i;
7715
+ if (bitAt(message, i)) matrix.set(offset, center - 5);
7716
+ if (bitAt(message, i + 7)) matrix.set(center + 5, offset);
7717
+ if (bitAt(message, 20 - i)) matrix.set(offset, center + 5);
7718
+ if (bitAt(message, 27 - i)) matrix.set(center - 5, offset);
7719
+ }
7720
+ } else {
7721
+ for (let i = 0; i < 10; i++) {
7722
+ const offset = center - 5 + i + Math.floor(i / 5);
7723
+ if (bitAt(message, i)) matrix.set(offset, center - 7);
7724
+ if (bitAt(message, i + 10)) matrix.set(center + 7, offset);
7725
+ if (bitAt(message, 29 - i)) matrix.set(offset, center + 7);
7726
+ if (bitAt(message, 39 - i)) matrix.set(center - 7, offset);
7727
+ }
7728
+ }
7729
+ }
6327
7730
 
6328
- const version = (dimension - 17) / 4;
6329
- if (version < 1 || version > 40) continue;
7731
+ /**
7732
+ * Lay low-level bits in the four-sided, inward Aztec spiral.
7733
+ * @param {BitWriter} bits @param {{layers:number,compact:boolean,baseMatrixSize:number,symbolSize:number}} symbol
7734
+ * @returns {BitMatrix}
7735
+ */
7736
+ function buildAztecMatrix(bits, symbol) {
7737
+ const { layers, compact, baseMatrixSize, symbolSize } = symbol;
7738
+ const matrix = new BitMatrix(symbolSize);
7739
+ const alignment = new Int32Array(baseMatrixSize);
7740
+ const center = Math.floor(symbolSize / 2);
6330
7741
 
6331
- const key = `${Math.round(tl.x)},${Math.round(tl.y)},${dimension}`;
6332
- if (used.has(key)) continue;
6333
- used.add(key);
7742
+ if (compact) {
7743
+ for (let i = 0; i < baseMatrixSize; i++) alignment[i] = i;
7744
+ } else {
7745
+ const originalCenter = Math.floor(baseMatrixSize / 2);
7746
+ for (let i = 0; i < originalCenter; i++) {
7747
+ const offset = i + Math.floor(i / 15);
7748
+ alignment[originalCenter - i - 1] = center - offset - 1;
7749
+ alignment[originalCenter + i] = center + offset + 1;
7750
+ }
7751
+ }
7752
+
7753
+ let bit = 0;
7754
+ for (let layer = 0; layer < layers; layer++) {
7755
+ const rowSize = (layers - layer) * 4 + (compact ? 9 : 12);
7756
+ const low = layer * 2;
7757
+ const high = baseMatrixSize - 1 - low;
7758
+ for (let j = 0; j < rowSize; j++) {
7759
+ const offset = j * 2;
7760
+ for (let k = 0; k < 2; k++) {
7761
+ if (bitAt(bits, bit + offset + k)) matrix.set(alignment[low + k], alignment[low + j]);
7762
+ if (bitAt(bits, bit + rowSize * 2 + offset + k)) matrix.set(alignment[low + j], alignment[high - k]);
7763
+ if (bitAt(bits, bit + rowSize * 4 + offset + k)) matrix.set(alignment[high - k], alignment[high - j]);
7764
+ if (bitAt(bits, bit + rowSize * 6 + offset + k)) matrix.set(alignment[high - j], alignment[low + k]);
7765
+ }
7766
+ }
7767
+ bit += rowSize * 8;
7768
+ }
7769
+ if (bit !== bits.length) throw new EncodeError(`Aztec: layout consumed ${bit} of ${bits.length} bits`);
6334
7770
 
6335
- detections.push(
6336
- buildDetection(binaryImage, tl, tr, bl, dimension, version, moduleSize)
6337
- );
7771
+ const mode = modeMessage(layers, symbol.dataWords, compact);
7772
+ drawModeMessage(matrix, mode, compact, center);
7773
+ drawBullsEye(matrix, center, compact ? 5 : 7);
7774
+
7775
+ if (!compact) {
7776
+ for (let i = 0, offset = 0; i < Math.floor(baseMatrixSize / 2) - 1; i += 15, offset += 16) {
7777
+ for (let p = center & 1; p < symbolSize; p += 2) {
7778
+ matrix.set(center - offset, p); matrix.set(center + offset, p);
7779
+ matrix.set(p, center - offset); matrix.set(p, center + offset);
6338
7780
  }
6339
7781
  }
6340
7782
  }
7783
+ return matrix;
7784
+ }
6341
7785
 
6342
- detections.sort((a, b) => b.moduleSize - a.moduleSize);
6343
- return detections;
7786
+ /** @param {number | undefined} layers @param {boolean | undefined} compact */
7787
+ function candidates(layers, compact) {
7788
+ if (layers !== undefined) {
7789
+ if (!Number.isInteger(layers) || layers < 1 || layers > 32) throw new EncodeError('Aztec: layers must be an integer 1..32');
7790
+ if (compact === true && layers > 4) throw new EncodeError('Aztec: compact symbols support layers 1..4');
7791
+ return [aztecLayer(layers, compact === true)];
7792
+ }
7793
+ if (compact === true) return AZTEC_COMPACT_LAYERS;
7794
+ if (compact === false) return AZTEC_FULL_LAYERS;
7795
+ return [...AZTEC_COMPACT_LAYERS, ...AZTEC_FULL_LAYERS];
6344
7796
  }
6345
7797
 
6346
7798
  /**
6347
- * Turn three finder centres into four symbol corners.
7799
+ * Encode a UTF-8 string or bytes into an Aztec Code matrix.
6348
7800
  *
6349
- * @param {import('../core/bit-matrix.js').BitMatrix} image
6350
- * @param {Candidate} tl @param {Candidate} tr @param {Candidate} bl
6351
- * @param {number} dimension @param {number} version @param {number} moduleSize
6352
- * @returns {Detection}
7801
+ * @param {string|ArrayBuffer|ArrayBufferView} value
7802
+ * @param {{layers?:number,compact?:boolean,eccPercent?:number,charset?:'utf-8'}} [options]
7803
+ * @returns {BitMatrix & {format?:string,layers?:number,compact?:boolean,eccPercent?:number,dataCodewords?:number}}
6353
7804
  */
6354
- function buildDetection(image, tl, tr, bl, dimension, version, moduleSize) {
6355
- const d = dimension;
6356
- // Finder centres sit on module (3, 3) and friends, so at grid coordinate 3.5.
6357
- const gridTl = [3.5, 3.5];
6358
- const gridTr = [d - 3.5, 3.5];
6359
- const gridBl = [3.5, d - 3.5];
7805
+ function encodeAztec(value, options = {}) {
7806
+ const eccPercent = options.eccPercent ?? 23;
7807
+ if (!Number.isFinite(eccPercent) || eccPercent < 5 || eccPercent > 95) {
7808
+ throw new EncodeError('Aztec: eccPercent must be between 5 and 95');
7809
+ }
7810
+ const high = encodeHighLevel(value, { charset: options.charset ?? 'utf-8' });
7811
+ for (const candidate of candidates(options.layers, options.compact)) {
7812
+ if (!candidate) continue;
7813
+ const wordSize = wordSizeForLayers(candidate.layers);
7814
+ const stuffed = stuffBits(high, wordSize);
7815
+ const dataWords = Math.ceil(stuffed.length / wordSize);
7816
+ const eccWords = eccCodewordsFor(dataWords, eccPercent);
7817
+ if (dataWords > candidate.maxDataCodewords || dataWords + eccWords > candidate.totalCodewords) continue;
7818
+ const checked = addCheckWords(stuffed, candidate.totalBits, wordSize, fieldForLayers(candidate.layers));
7819
+ // `addCheckWords` uses every remaining word as parity. This is stronger
7820
+ // than the requested percentage, never weaker, and canonical for a chosen
7821
+ // layer/data-word combination.
7822
+ const symbol = { ...candidate, dataWords: checked.dataWords };
7823
+ const matrix = buildAztecMatrix(checked.bits, symbol);
7824
+ matrix.format = 'aztec'; matrix.layers = candidate.layers; matrix.compact = candidate.compact;
7825
+ matrix.eccPercent = Math.round(checked.eccWords * wordSize * 100 / Math.max(1, stuffed.length));
7826
+ matrix.dataCodewords = checked.dataWords;
7827
+ return matrix;
7828
+ }
7829
+ throw new EncodeError('Aztec: payload does not fit the requested layers and error correction');
7830
+ }
6360
7831
 
6361
- // Parallelogram estimate of the far corner, used both as the search seed for
6362
- // the alignment pattern and as the fallback when it is not there.
6363
- const guess = { x: tr.x + bl.x - tl.x, y: tr.y + bl.y - tl.y };
7832
+ __exports.stuffBits = stuffBits;
7833
+ __exports.addCheckWords = addCheckWords;
7834
+ __exports.modeMessage = modeMessage;
7835
+ __exports.buildAztecMatrix = buildAztecMatrix;
7836
+ __exports.encodeAztec = encodeAztec;
7837
+ };
6364
7838
 
6365
- /** @type {PerspectiveTransform | null} */
6366
- let transform = null;
6367
- let alignmentFound = false;
7839
+ __modules["aztec/decoder.js"] = function (__require, __exports) {
7840
+ /**
7841
+ * Decoder for a sampled Aztec symbol.
7842
+ *
7843
+ * This module deliberately accepts only a square, module-aligned BitMatrix.
7844
+ * Locating a bull's-eye in a photograph and perspective sampling are detector
7845
+ * concerns. Keeping the two stages apart makes all bit order and ECC rules
7846
+ * testable without image-processing noise.
7847
+ *
7848
+ * Contract with tables.js:
7849
+ * - aztecSymbolForLayers(compact, layers) returns the nominal symbol data;
7850
+ * - aztecWordSizeForLayers(layers) returns 6, 8, 10 or 12;
7851
+ * - aztecFieldForLayers(layers) returns the matching binary Galois field;
7852
+ * - aztecMatrixSize(compact, layers) returns the rendered square size.
7853
+ *
7854
+ * @module aztec/decoder
7855
+ */
7856
+ const { FormatError } = __require("core/errors.js");
7857
+ const { rsDecode } = __require("core/reed-solomon.js");
7858
+ const { aztecLayer: aztecSymbolForLayers, wordSizeForLayers: aztecWordSizeForLayers, fieldForLayers: aztecFieldForLayers, fieldForWordSize, aztecSymbolSize: aztecMatrixSize } = __require("aztec/tables.js");
6368
7859
 
6369
- if (version >= 2) {
6370
- // The bottom-right alignment pattern is centred on module (d - 7, d - 7).
6371
- const gridAlign = [d - 6.5, d - 6.5];
6372
- // Where that module lands under the parallelogram assumption.
6373
- const seed = {
6374
- x: tl.x + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.x - tl.x) +
6375
- ((gridAlign[1] - 3.5) / (d - 7)) * (bl.x - tl.x),
6376
- y: tl.y + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.y - tl.y) +
6377
- ((gridAlign[1] - 3.5) / (d - 7)) * (bl.y - tl.y),
6378
- };
7860
+ const UPPER = ['CTRL_PS', ' ', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'CTRL_LL', 'CTRL_ML', 'CTRL_DL', 'CTRL_BS'];
7861
+ const LOWER = ['CTRL_PS', ' ', ...'abcdefghijklmnopqrstuvwxyz', 'CTRL_US', 'CTRL_ML', 'CTRL_DL', 'CTRL_BS'];
7862
+ const MIXED = [
7863
+ 'CTRL_PS', ' ', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\b', '\t', '\n', '\x0b', '\f', '\r', '\x1b',
7864
+ '\x1c', '\x1d', '\x1e', '\x1f', '@', '\\', '^', '_', '`', '|', '~', '\x7f', 'CTRL_LL', 'CTRL_UL', 'CTRL_PL', 'CTRL_BS',
7865
+ ];
7866
+ const PUNCT = ['FLG(n)', '\r', '\r\n', '. ', ', ', ': ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '[', ']', '{', '}', 'CTRL_UL'];
7867
+ const DIGIT = ['CTRL_PS', ' ', ...'0123456789', ',', '.', 'CTRL_UL'];
7868
+ const TABLES = { UPPER, LOWER, MIXED, PUNCT, DIGIT };
6379
7869
 
6380
- const align = findAlignment(image, seed, moduleSize);
6381
- if (align) {
6382
- alignmentFound = true;
6383
- transform = PerspectiveTransform.quadToQuad(
6384
- gridTl[0], gridTl[1], gridTr[0], gridTr[1], gridAlign[0], gridAlign[1], gridBl[0], gridBl[1],
6385
- tl.x, tl.y, tr.x, tr.y, align.x, align.y, bl.x, bl.y
6386
- );
7870
+ /** @param {boolean[]} bits @param {number} offset @param {number} count */
7871
+ function readBits(bits, offset, count) {
7872
+ if (offset + count > bits.length) throw new FormatError('Aztec: truncated high-level stream');
7873
+ let value = 0;
7874
+ for (let i = 0; i < count; i++) value = (value << 1) | (bits[offset + i] ? 1 : 0);
7875
+ return value;
7876
+ }
7877
+
7878
+ /** @param {number} value @param {number} count @param {boolean[]} out */
7879
+ function appendBits(value, count, out) {
7880
+ for (let i = count - 1; i >= 0; i--) out.push(((value >>> i) & 1) !== 0);
7881
+ }
7882
+
7883
+ /**
7884
+ * Decode an Aztec high-level bit stream to its exact byte payload.
7885
+ *
7886
+ * Text tables contribute their ISO-8859-1 byte values; Binary Shift appends
7887
+ * raw bytes. ECI markers are consumed but intentionally not emitted: callers
7888
+ * receive the transported byte payload and may select their own charset.
7889
+ *
7890
+ * @param {boolean[]} bits
7891
+ * @returns {Uint8Array}
7892
+ */
7893
+ function decodeHighLevelBits(bits) {
7894
+ const output = [];
7895
+ let latch = 'UPPER';
7896
+ let shift = 'UPPER';
7897
+ let offset = 0;
7898
+
7899
+ while (offset < bits.length) {
7900
+ if (shift === 'BINARY') {
7901
+ if (offset + 5 > bits.length) break; // legal trailing pad
7902
+ let length = readBits(bits, offset, 5);
7903
+ offset += 5;
7904
+ if (length === 0) {
7905
+ if (offset + 11 > bits.length) throw new FormatError('Aztec: truncated Binary Shift length');
7906
+ length = readBits(bits, offset, 11) + 31;
7907
+ offset += 11;
7908
+ }
7909
+ if (offset + length * 8 > bits.length) throw new FormatError('Aztec: truncated Binary Shift data');
7910
+ for (let i = 0; i < length; i++) {
7911
+ output.push(readBits(bits, offset, 8));
7912
+ offset += 8;
7913
+ }
7914
+ shift = latch;
7915
+ continue;
7916
+ }
7917
+
7918
+ const size = shift === 'DIGIT' ? 4 : 5;
7919
+ if (offset + size > bits.length) break; // trailing pad after unstuffing
7920
+ const code = readBits(bits, offset, size);
7921
+ offset += size;
7922
+ const table = TABLES[shift];
7923
+ const token = table[code];
7924
+ if (token === undefined) throw new FormatError(`Aztec: invalid ${shift} code ${code}`);
7925
+
7926
+ if (token === 'FLG(n)') {
7927
+ if (offset + 3 > bits.length) throw new FormatError('Aztec: truncated FLG(n)');
7928
+ const count = readBits(bits, offset, 3);
7929
+ offset += 3;
7930
+ if (count === 0) output.push(0x1d); // FNC1 / GS
7931
+ else if (count <= 6) {
7932
+ // ECI assignment number, encoded as count decimal digits. It changes
7933
+ // interpretation, not the wire bytes, so consume it without output.
7934
+ for (let i = 0; i < count; i++) {
7935
+ if (offset + 4 > bits.length) throw new FormatError('Aztec: truncated ECI');
7936
+ const digit = readBits(bits, offset, 4);
7937
+ offset += 4;
7938
+ if (digit < 2 || digit > 11) throw new FormatError('Aztec: invalid ECI digit');
7939
+ }
7940
+ } else {
7941
+ throw new FormatError(`Aztec: unsupported FLG(${count})`);
7942
+ }
7943
+ shift = latch;
7944
+ continue;
7945
+ }
7946
+
7947
+ if (token.startsWith('CTRL_')) {
7948
+ const targetCode = token.slice(5, -1);
7949
+ const latchMode = token.endsWith('L');
7950
+ const target = ({ P: 'PUNCT', L: 'LOWER', M: 'MIXED', D: 'DIGIT', U: 'UPPER', B: 'BINARY' })[targetCode];
7951
+ if (!target) throw new FormatError(`Aztec: invalid control ${token}`);
7952
+ shift = target;
7953
+ if (latchMode) latch = shift;
7954
+ continue;
6387
7955
  }
7956
+
7957
+ for (let i = 0; i < token.length; i++) output.push(token.charCodeAt(i));
7958
+ shift = latch;
6388
7959
  }
7960
+ return Uint8Array.from(output);
7961
+ }
6389
7962
 
6390
- const plain = PerspectiveTransform.quadToQuad(
6391
- gridTl[0], gridTl[1], gridTr[0], gridTr[1], d - 3.5, d - 3.5, gridBl[0], gridBl[1],
6392
- tl.x, tl.y, tr.x, tr.y, guess.x, guess.y, bl.x, bl.y
7963
+ /** @param {boolean} compact @param {number} layers */
7964
+ function alignmentMap(compact, layers) {
7965
+ const baseSize = (compact ? 11 : 14) + layers * 4;
7966
+ if (compact) return Array.from({ length: baseSize }, (_, i) => i);
7967
+ const size = aztecMatrixSize(layers, false);
7968
+ const map = new Array(baseSize);
7969
+ const baseCenter = baseSize >> 1;
7970
+ const center = size >> 1;
7971
+ for (let i = 0; i < baseCenter; i++) {
7972
+ const offset = i + Math.floor(i / 15);
7973
+ map[baseCenter - i - 1] = center - offset - 1;
7974
+ map[baseCenter + i] = center + offset + 1;
7975
+ }
7976
+ return map;
7977
+ }
7978
+
7979
+ /**
7980
+ * Read the four sides of the parameter message. The order mirrors the
7981
+ * clockwise write order and is independent of the data spiral.
7982
+ *
7983
+ * @param {import('../core/bit-matrix.js').BitMatrix} matrix
7984
+ * @param {boolean} compact
7985
+ * @returns {boolean[]}
7986
+ */
7987
+ function readModeBits(matrix, compact) {
7988
+ const center = matrix.width >> 1;
7989
+ const side = compact ? 7 : 10;
7990
+ const offset = compact ? 5 : 7;
7991
+ // Full symbols skip the reference grid line through the bull's-eye. This
7992
+ // exact sequence is also used by drawModeMessage() in encoder.js.
7993
+ const positions = Array.from(
7994
+ { length: side },
7995
+ (_, i) => compact ? center - 3 + i : center - 5 + i + Math.floor(i / 5),
6393
7996
  );
7997
+ const bits = [];
7998
+ for (let i = 0; i < side; i++) bits.push(matrix.get(positions[i], center - offset));
7999
+ for (let i = 0; i < side; i++) bits.push(matrix.get(center + offset, positions[i]));
8000
+ for (let i = 0; i < side; i++) bits.push(matrix.get(positions[side - 1 - i], center + offset));
8001
+ for (let i = 0; i < side; i++) bits.push(matrix.get(center - offset, positions[side - 1 - i]));
8002
+ return bits;
8003
+ }
6394
8004
 
6395
- const corners = cornersOf(transform ?? plain, d);
6396
- // Keep the parallelogram corners as a second opinion whenever an alignment
6397
- // pattern steered the first set. Verification makes a false match unlikely,
6398
- // not impossible, and one extra sampling attempt is far cheaper than losing
6399
- // a symbol to it.
6400
- const altCorners = transform ? cornersOf(plain, d) : null;
8005
+ /** @param {boolean[]} bits @param {boolean} compact */
8006
+ function decodeModeMessage(bits, compact) {
8007
+ const total = compact ? 7 : 10;
8008
+ const dataWords = compact ? 2 : 4;
8009
+ const words = new Array(total);
8010
+ for (let i = 0; i < total; i++) words[i] = readBits(bits, i * 4, 4);
8011
+ const corrections = rsDecode(words, total - dataWords, fieldForWordSize(4), 1);
8012
+ let data = 0;
8013
+ for (let i = 0; i < dataWords; i++) data = (data << 4) | words[i];
8014
+ const layers = compact ? (data >>> 6) + 1 : (data >>> 11) + 1;
8015
+ const dataCodewords = compact ? (data & 0x3f) + 1 : (data & 0x7ff) + 1;
8016
+ return { layers, dataCodewords, corrections };
8017
+ }
6401
8018
 
6402
- return { corners, altCorners, dimension, version, moduleSize, alignmentFound };
8019
+ /** @param {boolean} compact @param {number} layers */
8020
+ function totalBitsInLayers(compact, layers) {
8021
+ return ((compact ? 88 : 112) + 16 * layers) * layers;
6403
8022
  }
6404
8023
 
6405
8024
  /**
6406
- * The four outer corners of the symbol under a grid-to-image transform.
8025
+ * Extract raw, stuffed codeword bits in logical ring order.
8026
+ * @param {import('../core/bit-matrix.js').BitMatrix} matrix
8027
+ * @param {boolean} compact @param {number} layers
8028
+ * @returns {boolean[]}
8029
+ */
8030
+ function extractBits(matrix, compact, layers) {
8031
+ const baseSize = (compact ? 11 : 14) + layers * 4;
8032
+ const map = alignmentMap(compact, layers);
8033
+ const raw = new Array(totalBitsInLayers(compact, layers));
8034
+ let offset = 0;
8035
+ for (let layer = 0; layer < layers; layer++) {
8036
+ const rowSize = (layers - layer) * 4 + (compact ? 9 : 12);
8037
+ for (let j = 0; j < rowSize; j++) {
8038
+ const col = j * 2;
8039
+ for (let k = 0; k < 2; k++) {
8040
+ raw[offset + col + k] = matrix.get(map[layer * 2 + k], map[layer * 2 + j]);
8041
+ raw[offset + rowSize * 2 + col + k] = matrix.get(map[layer * 2 + j], map[baseSize - 1 - layer * 2 - k]);
8042
+ raw[offset + rowSize * 4 + col + k] = matrix.get(map[baseSize - 1 - layer * 2 - k], map[baseSize - 1 - layer * 2 - j]);
8043
+ raw[offset + rowSize * 6 + col + k] = matrix.get(map[baseSize - 1 - layer * 2 - j], map[layer * 2 + k]);
8044
+ }
8045
+ }
8046
+ offset += rowSize * 8;
8047
+ }
8048
+ return raw;
8049
+ }
8050
+
8051
+ /** @param {boolean[]} raw @param {number} layers @param {number} dataCodewords */
8052
+ function correctAndUnstuff(raw, layers, dataCodewords) {
8053
+ const wordSize = aztecWordSizeForLayers(layers);
8054
+ const totalWords = Math.floor(raw.length / wordSize);
8055
+ if (dataCodewords <= 0 || dataCodewords > totalWords) throw new FormatError('Aztec: invalid data word count');
8056
+ const start = raw.length % wordSize;
8057
+ const words = new Array(totalWords);
8058
+ for (let i = 0; i < totalWords; i++) words[i] = readBits(raw, start + i * wordSize, wordSize);
8059
+ const corrections = rsDecode(words, totalWords - dataCodewords, aztecFieldForLayers(layers), 1);
8060
+ const mask = (1 << wordSize) - 1;
8061
+ const corrected = [];
8062
+ for (let i = 0; i < dataCodewords; i++) {
8063
+ const word = words[i];
8064
+ if (word === 0 || word === mask) throw new FormatError('Aztec: invalid stuffed codeword');
8065
+ if (word === 1 || word === mask - 1) {
8066
+ for (let j = 0; j < wordSize - 1; j++) corrected.push(word === mask - 1);
8067
+ } else {
8068
+ appendBits(word, wordSize, corrected);
8069
+ }
8070
+ }
8071
+ return { bits: corrected, corrections };
8072
+ }
8073
+
8074
+ /** @param {Uint8Array} bytes */
8075
+ function bytesToText(bytes) {
8076
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
8077
+ catch { return new TextDecoder('latin1').decode(bytes); }
8078
+ }
8079
+
8080
+ /**
8081
+ * Decode a square Aztec symbol with one bit per module and no quiet zone.
8082
+ * The matrix must already be oriented with the mode message at the top.
6407
8083
  *
6408
- * @param {PerspectiveTransform} transform @param {number} d
6409
- * @returns {Array<{x: number, y: number}>}
8084
+ * @param {import('../core/bit-matrix.js').BitMatrix} matrix
8085
+ * @returns {{text: string, bytes: Uint8Array, compact: boolean, layers: number, corrections: number, eccPercent: number}}
6410
8086
  */
6411
- function cornersOf(transform, d) {
6412
- return [
6413
- transform.transformPoint(0, 0),
6414
- transform.transformPoint(d, 0),
6415
- transform.transformPoint(d, d),
6416
- transform.transformPoint(0, d),
6417
- ];
8087
+ function decodeAztec(matrix) {
8088
+ if (!matrix || matrix.width !== matrix.height) throw new FormatError('Aztec: expected a square BitMatrix');
8089
+ let compact;
8090
+ let mode;
8091
+ // Compact and full dimensions are disjoint; trying both also makes malformed
8092
+ // candidate handling deterministic for the future image detector.
8093
+ for (const candidate of [true, false]) {
8094
+ try {
8095
+ const value = decodeModeMessage(readModeBits(matrix, candidate), candidate);
8096
+ if (value.layers < 1 || value.layers > (candidate ? 4 : 32)) continue;
8097
+ if (aztecMatrixSize(value.layers, candidate) !== matrix.width) continue;
8098
+ compact = candidate;
8099
+ mode = value;
8100
+ break;
8101
+ } catch { /* Try the other family. */ }
8102
+ }
8103
+ if (compact === undefined || !mode) throw new FormatError('Aztec: invalid mode message or dimensions');
8104
+ // Ensure the declared layer data agrees with the table module, so a future
8105
+ // tables refactor cannot silently make decoder capacity calculations stale.
8106
+ aztecSymbolForLayers(mode.layers, compact);
8107
+ const raw = extractBits(matrix, compact, mode.layers);
8108
+ const payload = correctAndUnstuff(raw, mode.layers, mode.dataCodewords);
8109
+ const bytes = decodeHighLevelBits(payload.bits);
8110
+ const totalWords = Math.floor(raw.length / aztecWordSizeForLayers(mode.layers));
8111
+ return {
8112
+ text: bytesToText(bytes),
8113
+ bytes,
8114
+ compact,
8115
+ layers: mode.layers,
8116
+ corrections: mode.corrections + payload.corrections,
8117
+ eccPercent: Math.round(((totalWords - mode.dataCodewords) * 100) / totalWords),
8118
+ };
6418
8119
  }
6419
8120
 
8121
+ __exports.decodeHighLevelBits = decodeHighLevelBits;
8122
+ __exports.decodeAztec = decodeAztec;
8123
+ };
8124
+
8125
+ __modules["aztec/detector.js"] = function (__require, __exports) {
6420
8126
  /**
6421
- * Find and decode every QR Code in a binarized image.
8127
+ * Aztec image detection.
6422
8128
  *
6423
- * Each candidate gets up to four attempts: a plain centre sample, a 3x3
6424
- * majority vote for noisy input, and both of those rotated 180 degrees. The
6425
- * rotation retry matters because three finders in a right isoceles triangle
6426
- * look identical to the same three rotated half a turn — the orientation is
6427
- * only settled once the format information reads cleanly, which is to say once
6428
- * the decode succeeds.
8129
+ * Aztec has no finder pattern at its outer border. Its reliable geometric
8130
+ * anchor is instead the alternating square bull's-eye in the centre: five
8131
+ * rings in Compact symbols, seven rings in Full symbols. The detector finds
8132
+ * isolated central modules, verifies those rings at module centres, then
8133
+ * samples each legal symbol dimension. The decoder is deliberately the final
8134
+ * arbiter: its mode-message Reed--Solomon check rejects accidental concentric
8135
+ * artwork and tells us which of the compact/full dimensions is real.
6429
8136
  *
6430
- * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
6431
- * @returns {Array<import('./decoder.js').DecodeResult & {corners: Array<{x: number, y: number}>}>}
6432
- * Empty when nothing decodes; never throws for "no symbol here".
8137
+ * Sampling uses a quadrilateral, not a cropped bitmap, so the detected
8138
+ * rotation is corrected before decoding. The ring search covers arbitrary
8139
+ * in-plane rotations (four-degree coarse search; at normal camera scales its
8140
+ * positional error remains well inside a module). The optional inverse pass
8141
+ * supports light modules on a dark field.
8142
+ *
8143
+ * @module aztec/detector
6433
8144
  */
6434
- function detectAndDecodeQR(binaryImage) {
6435
- let detections;
6436
- try {
6437
- detections = detectQR(binaryImage);
6438
- } catch (e) {
6439
- return [];
8145
+ const { NotFoundError } = __require("core/errors.js");
8146
+ const { sampleQuad } = __require("image/grid-sampler.js");
8147
+ const { decodeAztec } = __require("aztec/decoder.js");
8148
+
8149
+ /** @typedef {{x:number, y:number}} Point */
8150
+ /** @typedef {{corners: Point[], dimension: number, compact: boolean, moduleSize: number, matrix: import('../core/bit-matrix.js').BitMatrix}} Detection */
8151
+
8152
+ // Compact: 11 + 4 layers. Full symbols add reference-grid rows/columns every
8153
+ // 15 modules measured from their central 14-module base, not every 15 layers.
8154
+ const DIMENSIONS = [
8155
+ ...[1, 2, 3, 4].map((layers) => ({ compact: true, dimension: 11 + 4 * layers })),
8156
+ ...Array.from({ length: 32 }, (_, index) => {
8157
+ const layers = index + 1;
8158
+ return { compact: false, dimension: 15 + 4 * layers + 2 * Math.floor((2 * layers + 6) / 15) };
8159
+ }),
8160
+ ];
8161
+
8162
+ function pixel(image, x, y) {
8163
+ const ix = Math.round(x);
8164
+ const iy = Math.round(y);
8165
+ return ix >= 0 && iy >= 0 && ix < image.width && iy < image.height && image.get(ix, iy);
8166
+ }
8167
+
8168
+ /** Connected components of either polarity, retaining only plausible modules. */
8169
+ function components(image, value) {
8170
+ const seen = new Uint8Array(image.width * image.height);
8171
+ const out = [];
8172
+ const maximumArea = Math.max(4, Math.floor(image.width * image.height * 0.08));
8173
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) {
8174
+ const start = y * image.width + x;
8175
+ if (seen[start] || image.get(x, y) !== value) continue;
8176
+ const xs = [x];
8177
+ const ys = [y];
8178
+ seen[start] = 1;
8179
+ let head = 0;
8180
+ let minX = x; let maxX = x; let minY = y; let maxY = y;
8181
+ while (head < xs.length) {
8182
+ const px = xs[head]; const py = ys[head++];
8183
+ if (px < minX) minX = px; if (px > maxX) maxX = px;
8184
+ if (py < minY) minY = py; if (py > maxY) maxY = py;
8185
+ for (const [nx, ny] of [[px - 1, py], [px + 1, py], [px, py - 1], [px, py + 1]]) {
8186
+ if (nx < 0 || ny < 0 || nx >= image.width || ny >= image.height) continue;
8187
+ const at = ny * image.width + nx;
8188
+ if (!seen[at] && image.get(nx, ny) === value) {
8189
+ seen[at] = 1; xs.push(nx); ys.push(ny);
8190
+ }
8191
+ }
8192
+ }
8193
+ const width = maxX - minX + 1;
8194
+ const height = maxY - minY + 1;
8195
+ const area = width * height;
8196
+ // The central module is solid and approximately square. This filter is
8197
+ // intentionally permissive because a rotated raster module is diamond-ish.
8198
+ if (xs.length <= maximumArea && Math.abs(width - height) <= Math.max(1, Math.ceil(Math.max(width, height) * 0.35)) &&
8199
+ xs.length >= area * 0.45) {
8200
+ out.push({ x: (minX + maxX) / 2, y: (minY + maxY) / 2, width, height, pixels: xs.length });
8201
+ }
6440
8202
  }
8203
+ return out.sort((a, b) => b.pixels - a.pixels).slice(0, 2000);
8204
+ }
6441
8205
 
6442
- const results = [];
6443
- const seen = new Set();
8206
+ function expectedDark(ring, inverted) {
8207
+ return inverted ? (ring & 1) === 1 : (ring & 1) === 0;
8208
+ }
6444
8209
 
6445
- for (let i = 0; i < detections.length; i++) {
6446
- const det = detections[i];
8210
+ /** Score one square bull's-eye at an angle and a candidate module pitch. */
8211
+ function ringScore(image, centre, pitch, angle, inverted, rings) {
8212
+ const cos = Math.cos(angle);
8213
+ const sin = Math.sin(angle);
8214
+ let correct = 0;
8215
+ let total = 0;
8216
+ for (let ring = 0; ring < rings; ring++) {
8217
+ const wanted = expectedDark(ring, inverted);
8218
+ for (let j = -ring; j <= ring; j++) for (let i = -ring; i <= ring; i++) {
8219
+ if (ring && Math.abs(i) !== ring && Math.abs(j) !== ring) continue;
8220
+ const x = centre.x + (i * cos - j * sin) * pitch;
8221
+ const y = centre.y + (i * sin + j * cos) * pitch;
8222
+ if (pixel(image, x, y) === wanted) correct++;
8223
+ total++;
8224
+ }
8225
+ }
8226
+ return correct / total;
8227
+ }
6447
8228
 
6448
- for (let attempt = 0; attempt < 4; attempt++) {
6449
- const voting = attempt === 1 || attempt === 3;
6450
- const rotated = attempt >= 2;
8229
+ function rotateCorners(corners, turn) {
8230
+ return corners.slice(turn).concat(corners.slice(0, turn));
8231
+ }
6451
8232
 
6452
- let matrix;
6453
- try {
6454
- matrix = sampleQuad(binaryImage, det.dimension, det.corners, voting);
6455
- } catch (e) {
6456
- continue;
6457
- }
6458
- if (rotated) matrix.rotate180();
8233
+ function invert(matrix) {
8234
+ const out = matrix.clone();
8235
+ for (let y = 0; y < out.height; y++) for (let x = 0; x < out.width; x++) out.flip(x, y);
8236
+ return out;
8237
+ }
6459
8238
 
6460
- try {
6461
- const result = decodeQR(matrix);
6462
- // The same symbol can be detected through more than one finder triple.
6463
- const key = `${result.version}|${result.text}`;
6464
- if (!seen.has(key)) {
6465
- seen.add(key);
6466
- results.push(Object.assign({ corners: det.corners }, result));
8239
+ function cornersFor(centre, pitch, angle, dimension) {
8240
+ const half = dimension * pitch / 2;
8241
+ const cos = Math.cos(angle);
8242
+ const sin = Math.sin(angle);
8243
+ const point = (x, y) => ({ x: centre.x + x * cos - y * sin, y: centre.y + x * sin + y * cos });
8244
+ return [point(-half, -half), point(half, -half), point(half, half), point(-half, half)];
8245
+ }
8246
+
8247
+ /**
8248
+ * Find an Aztec symbol in a binarized image.
8249
+ *
8250
+ * The returned matrix is in the orientation accepted by the Aztec decoder.
8251
+ * A valid mode message is required before a geometric candidate is returned,
8252
+ * making false positives from decorative concentric squares very unlikely.
8253
+ *
8254
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
8255
+ * @returns {Detection | null}
8256
+ */
8257
+ function detectAztec(binaryImage) {
8258
+ if (!binaryImage || !binaryImage.width || !binaryImage.height) {
8259
+ throw new NotFoundError('detectAztec: no image supplied');
8260
+ }
8261
+ const candidates = [];
8262
+ for (const inverted of [false, true]) {
8263
+ for (const core of components(binaryImage, !inverted)) {
8264
+ // A non-rotated one-module component directly gives its pitch. For
8265
+ // rotated modules its bounding box grows by |sin| + |cos|, compensated
8266
+ // below for every tested angle.
8267
+ for (let degrees = 0; degrees < 180; degrees += 4) {
8268
+ const angle = degrees * Math.PI / 180;
8269
+ const scale = Math.abs(Math.cos(angle)) + Math.abs(Math.sin(angle));
8270
+ const pitch = ((core.width + core.height) / 2) / scale;
8271
+ if (pitch < 0.8) continue;
8272
+ // Test Full first: its seven rings also exclude Compact candidates.
8273
+ const fullScore = ringScore(binaryImage, core, pitch, angle, inverted, 7);
8274
+ const rings = fullScore >= 0.88 ? 7 : 5;
8275
+ const score = rings === 7 ? fullScore : ringScore(binaryImage, core, pitch, angle, inverted, 5);
8276
+ if (score < 0.91) continue;
8277
+ const symbolKinds = rings === 7 ? DIMENSIONS.filter((item) => !item.compact) : DIMENSIONS.filter((item) => item.compact);
8278
+ for (const kind of symbolKinds) {
8279
+ const baseCorners = cornersFor(core, pitch, angle, kind.dimension);
8280
+ for (let turn = 0; turn < 4; turn++) {
8281
+ const corners = rotateCorners(baseCorners, turn);
8282
+ let matrix;
8283
+ try { matrix = sampleQuad(binaryImage, kind.dimension, corners); } catch (e) { continue; }
8284
+ if (inverted) matrix = invert(matrix);
8285
+ try {
8286
+ // The decoder verifies the mode-message ECC and exact geometry.
8287
+ // We do not expose its result here so callers can use pure
8288
+ // detection without treating payload decoding as an API contract.
8289
+ decodeAztec(matrix);
8290
+ candidates.push({ corners, dimension: kind.dimension, compact: kind.compact,
8291
+ moduleSize: pitch, matrix, score });
8292
+ } catch (e) { /* Not an Aztec mode message at this dimension. */ }
8293
+ }
6467
8294
  }
6468
- break;
6469
- } catch (e) {
6470
- /* Try the next sampling strategy. */
6471
8295
  }
6472
8296
  }
6473
8297
  }
6474
-
6475
- return results;
8298
+ candidates.sort((a, b) => b.score - a.score || b.moduleSize - a.moduleSize);
8299
+ const best = candidates[0];
8300
+ if (!best) return null;
8301
+ delete best.score;
8302
+ return best;
6476
8303
  }
6477
8304
 
6478
- __exports.detectQR = detectQR;
6479
- __exports.detectAndDecodeQR = detectAndDecodeQR;
6480
- };
6481
-
6482
- __modules["qr/index.js"] = function (__require, __exports) {
6483
8305
  /**
6484
- * QR Code, re-exported.
6485
- *
6486
- * `QR_PLACEHOLDER` is deliberately absent: `src/index.js` probes for it to
6487
- * decide whether this build can read and write QR, and its absence is what
6488
- * reports the format as available.
8306
+ * Detect then decode an Aztec symbol. Detection failure is a normal result for
8307
+ * images without an Aztec code, therefore invalid candidates return null.
6489
8308
  *
6490
- * @module qr
8309
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
8310
+ * @returns {(import('./decoder.js').DecodeResult & {corners: Point[]}) | null}
6491
8311
  */
6492
- const __reexport0 = __require("qr/encoder.js"); __exports.encodeQR = __reexport0.encodeQR;
6493
- const __reexport1 = __require("qr/decoder.js"); __exports.decodeQR = __reexport1.decodeQR;
6494
- const __reexport2 = __require("qr/detector.js"); __exports.detectQR = __reexport2.detectQR; __exports.detectAndDecodeQR = __reexport2.detectAndDecodeQR;
6495
- const __reexport3 = __require("qr/tables.js"); __exports.validateTables = __reexport3.validateTables;
8312
+ function detectAndDecodeAztec(binaryImage) {
8313
+ let detection;
8314
+ try { detection = detectAztec(binaryImage); } catch (e) { return null; }
8315
+ if (!detection) return null;
8316
+ try { return Object.assign({ corners: detection.corners }, decodeAztec(detection.matrix)); }
8317
+ catch (e) { return null; }
8318
+ }
8319
+
8320
+ __exports.detectAztec = detectAztec;
8321
+ __exports.detectAndDecodeAztec = detectAndDecodeAztec;
8322
+ };
8323
+
8324
+ __modules["aztec/index.js"] = function (__require, __exports) {
8325
+ /** Aztec Code entry points. @module aztec */
8326
+ const __reexport0 = __require("aztec/encoder.js"); __exports.encodeAztec = __reexport0.encodeAztec;
8327
+ const __reexport1 = __require("aztec/decoder.js"); __exports.decodeAztec = __reexport1.decodeAztec;
8328
+ const __reexport2 = __require("aztec/detector.js"); __exports.detectAztec = __reexport2.detectAztec; __exports.detectAndDecodeAztec = __reexport2.detectAndDecodeAztec;
8329
+ const __reexport3 = __require("aztec/tables.js"); __exports.AZTEC_COMPACT_LAYERS = __reexport3.AZTEC_COMPACT_LAYERS; __exports.AZTEC_FULL_LAYERS = __reexport3.AZTEC_FULL_LAYERS; __exports.AZTEC_LAYERS = __reexport3.AZTEC_LAYERS; __exports.AZTEC_DEFAULT_ECC_PERCENT = __reexport3.AZTEC_DEFAULT_ECC_PERCENT; __exports.AZTEC_RS_GENERATOR_BASE = __reexport3.AZTEC_RS_GENERATOR_BASE; __exports.aztecLayer = __reexport3.aztecLayer; __exports.aztecSymbolSize = __reexport3.aztecSymbolSize; __exports.validateAztecTables = __reexport3.validateAztecTables;
6496
8330
 
6497
8331
 
6498
8332
  };
@@ -7750,7 +9584,9 @@ const { LuminanceSource } = __require("image/luminance.js");
7750
9584
  const { binarize } = __require("image/binarizer.js");
7751
9585
  const { ONED_FORMATS } = __require("oned/index.js");
7752
9586
  const { decodeOneD } = __require("oned/reader.js");
9587
+ const datamatrix = __require("datamatrix/index.js");
7753
9588
  const qr = __require("qr/index.js");
9589
+ const aztec = __require("aztec/index.js");
7754
9590
  __exports.BitMatrix = BitMatrix;
7755
9591
  const __reexport0 = __require("core/errors.js"); __exports.BarcodeError = __reexport0.BarcodeError; __exports.EncodeError = __reexport0.EncodeError; __exports.NotFoundError = __reexport0.NotFoundError; __exports.FormatError = __reexport0.FormatError; __exports.ChecksumError = __reexport0.ChecksumError;
7756
9592
  const __reexport1 = __require("image/luminance.js"); __exports.LuminanceSource = __reexport1.LuminanceSource;
@@ -7762,6 +9598,8 @@ const __reexport5 = __require("render/png.js"); __exports.toPNG = __reexport5.to
7762
9598
  const __reexport6 = __require("render/index.js"); __exports.renderToCanvasAuto = __reexport6.renderToCanvasAuto; __exports.isWebGL2Available = __reexport6.isWebGL2Available;
7763
9599
  const __reexport7 = __require("render/index.js"); __exports.renderToCanvasAutoAsync = __reexport7.renderToCanvasAutoAsync; __exports.isWebGPUAvailable = __reexport7.isWebGPUAvailable;
7764
9600
  const __reexport8 = __require("qr/index.js"); __exports.encodeQR = __reexport8.encodeQR; __exports.decodeQR = __reexport8.decodeQR; __exports.detectQR = __reexport8.detectQR; __exports.detectAndDecodeQR = __reexport8.detectAndDecodeQR;
9601
+ const __reexport9 = __require("datamatrix/index.js"); __exports.encodeDataMatrix = __reexport9.encodeDataMatrix; __exports.decodeDataMatrix = __reexport9.decodeDataMatrix; __exports.detectDataMatrix = __reexport9.detectDataMatrix; __exports.detectAndDecodeDataMatrix = __reexport9.detectAndDecodeDataMatrix;
9602
+ const __reexport10 = __require("aztec/index.js"); __exports.encodeAztec = __reexport10.encodeAztec; __exports.decodeAztec = __reexport10.decodeAztec; __exports.detectAztec = __reexport10.detectAztec; __exports.detectAndDecodeAztec = __reexport10.detectAndDecodeAztec;
7765
9603
 
7766
9604
  /**
7767
9605
  * @typedef {object} FormatInfo
@@ -7785,6 +9623,10 @@ const qrCanEncode = qrPresent &&
7785
9623
  typeof qr.encodeQR === 'function' && qr.QR_CAN_ENCODE !== false;
7786
9624
  const qrCanDecode = qrPresent &&
7787
9625
  typeof qr.detectAndDecodeQR === 'function' && qr.QR_CAN_DECODE !== false;
9626
+ const dataMatrixCanEncode = typeof datamatrix.encodeDataMatrix === 'function';
9627
+ const dataMatrixCanDecode = typeof datamatrix.detectAndDecodeDataMatrix === 'function';
9628
+ const aztecCanEncode = typeof aztec.encodeAztec === 'function';
9629
+ const aztecCanDecode = typeof aztec.detectAndDecodeAztec === 'function';
7788
9630
 
7789
9631
  /**
7790
9632
  * Every format this build supports.
@@ -7812,6 +9654,20 @@ function listFormats() {
7812
9654
  canRead: qrCanDecode,
7813
9655
  kind: /** @type {'2D'} */ ('2D'),
7814
9656
  });
9657
+ formats.push({
9658
+ id: 'datamatrix',
9659
+ label: 'Data Matrix ECC 200',
9660
+ canWrite: dataMatrixCanEncode,
9661
+ canRead: dataMatrixCanDecode,
9662
+ kind: /** @type {'2D'} */ ('2D'),
9663
+ });
9664
+ formats.push({
9665
+ id: 'aztec',
9666
+ label: 'Aztec Code',
9667
+ canWrite: aztecCanEncode,
9668
+ canRead: aztecCanDecode,
9669
+ kind: /** @type {'2D'} */ ('2D'),
9670
+ });
7815
9671
 
7816
9672
  return formats;
7817
9673
  }
@@ -7832,6 +9688,9 @@ function listFormats() {
7832
9688
  * @param {boolean} [options.checkDigit] Append a check digit, where optional.
7833
9689
  * @param {boolean} [options.fullAscii] Code 39 extended encoding.
7834
9690
  * @param {boolean} [options.gs1] Emit a leading FNC1.
9691
+ * @param {number} [options.layers] Aztec layer count; automatic if omitted.
9692
+ * @param {boolean} [options.compact] Force an Aztec Compact or Full symbol.
9693
+ * @param {number} [options.eccPercent] Requested Aztec error-correction percentage.
7835
9694
  * @returns {BitMatrix}
7836
9695
  */
7837
9696
  function encode(text, options = {}) {
@@ -7841,10 +9700,16 @@ function encode(text, options = {}) {
7841
9700
  if (format === 'qr' || format === 'qrcode') {
7842
9701
  return qr.encodeQR(value, options);
7843
9702
  }
9703
+ if (format === 'datamatrix' || format === 'data-matrix') {
9704
+ return datamatrix.encodeDataMatrix(value, options);
9705
+ }
9706
+ if (format === 'aztec' || format === 'aztec-code') {
9707
+ return aztec.encodeAztec(value, options);
9708
+ }
7844
9709
 
7845
9710
  const entry = ONED_FORMATS[format];
7846
9711
  if (!entry) {
7847
- const known = [...Object.keys(ONED_FORMATS), 'qr'].join(', ');
9712
+ const known = [...Object.keys(ONED_FORMATS), 'qr', 'datamatrix', 'aztec'].join(', ');
7848
9713
  throw new EncodeError(`Unknown format "${format}". Known formats: ${known}`);
7849
9714
  }
7850
9715
  return entry.encode(value, options);
@@ -7857,6 +9722,9 @@ function encode(text, options = {}) {
7857
9722
  * @property {Uint8Array} [bytes] Raw payload, before text decoding.
7858
9723
  * @property {number} [version] QR version.
7859
9724
  * @property {string} [ecc] QR error-correction level.
9725
+ * @property {number} [layers] Aztec layer count.
9726
+ * @property {boolean} [compact] Whether an Aztec symbol is Compact.
9727
+ * @property {number} [corrections] Reed–Solomon corrections applied by an Aztec decode.
7860
9728
  */
7861
9729
 
7862
9730
  /**
@@ -7877,6 +9745,8 @@ function decode(image, options = {}) {
7877
9745
  const { formats = null, tryHarder = true, binarizer = 'auto' } = options;
7878
9746
  const want = formats ? new Set(formats.map((f) => f.toLowerCase())) : null;
7879
9747
  const wantQR = !want || want.has('qr') || want.has('qrcode');
9748
+ const wantDataMatrix = !want || want.has('datamatrix') || want.has('data-matrix');
9749
+ const wantAztec = !want || want.has('aztec') || want.has('aztec-code');
7880
9750
  const wantOneD = !want || [...want].some((f) => f in ONED_FORMATS);
7881
9751
 
7882
9752
  const source = LuminanceSource.fromImageData(image);
@@ -7899,6 +9769,36 @@ function decode(image, options = {}) {
7899
9769
  }
7900
9770
  }
7901
9771
 
9772
+ if (wantDataMatrix && dataMatrixCanDecode) {
9773
+ // Hybrid thresholding can erase the interior of very large, perfectly
9774
+ // uniform modules. In auto mode keep the local-threshold attempt, then
9775
+ // retry Data Matrix once with the global threshold before giving up.
9776
+ const dataMatrixBits = binarizer === 'auto' ? [bits, binarize(pass, 'global')] : [bits];
9777
+ for (const candidateBits of dataMatrixBits) {
9778
+ try {
9779
+ const found = datamatrix.detectAndDecodeDataMatrix(candidateBits);
9780
+ if (found) { results.push({ ...found, format: 'datamatrix' }); break; }
9781
+ } catch {
9782
+ /* no Data Matrix with this threshold */
9783
+ }
9784
+ }
9785
+ }
9786
+
9787
+ if (wantAztec && aztecCanDecode) {
9788
+ // The central bull's-eye is a small, high-contrast target. Hybrid
9789
+ // thresholding can flatten it on clean rendered symbols, so mirror the
9790
+ // Data Matrix global fallback in auto mode.
9791
+ const aztecBits = binarizer === 'auto' ? [bits, binarize(pass, 'global')] : [bits];
9792
+ for (const candidateBits of aztecBits) {
9793
+ try {
9794
+ const found = aztec.detectAndDecodeAztec(candidateBits);
9795
+ if (found) { results.push({ ...found, format: 'aztec' }); break; }
9796
+ } catch {
9797
+ /* no Aztec code with this threshold */
9798
+ }
9799
+ }
9800
+ }
9801
+
7902
9802
  if (wantOneD) {
7903
9803
  const oneDFormats = want ? [...want].filter((f) => f in ONED_FORMATS) : null;
7904
9804
  for (const found of decodeOneD(bits, { formats: oneDFormats, tryHarder })) {
@@ -7933,7 +9833,7 @@ function decodeStrict(image, options) {
7933
9833
  }
7934
9834
 
7935
9835
  /** Library version, matching package.json. */
7936
- const VERSION = '0.1.0';
9836
+ const VERSION = '1.1.0';
7937
9837
 
7938
9838
  __exports.listFormats = listFormats;
7939
9839
  __exports.encode = encode;