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