All files / qr-code/src/encode mask.ts

100% Statements 94/94
100% Branches 63/63
100% Functions 7/7
100% Lines 75/75

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177                          15x 15x 15x 15x       3417327x   411979x   466220x   467220x   413805x   459453x   395828x   398445x   404376x   1x                 3724x 3724x 102252x 4412364x 4412364x 3368926x           15x 15x                       1768x     1768x 48118x 48118x 48118x 48118x   48118x 2071384x 2071384x   2071384x 2023266x 2023266x 94005x   2023266x 2023266x 167098x     2071384x 2071384x 2071384x 1590293x 1590293x           1768x 46350x 1976916x 1976916x         227088x           1768x 2071384x 1768x 1768x   1768x       266x 266x               223880x 223878x     3x 1x                         272x   272x 216x 216x 1728x 1728x 1728x 1728x 475x 475x   1728x   56x 6x     266x 266x 266x    
import type { QRMatrix } from "../types";
import { type RawMatrix, drawFormatInfo } from "./matrix";
 
/**
 * Data masking (ISO/IEC 18004 §7.8).
 *
 * Raw codewords often produce large blank areas or accidental copies of the
 * finder pattern, both of which confuse scanners. So the encoder XORs the data
 * region with each of eight fixed patterns, scores the result against four
 * penalty rules, and keeps the least-bad one.
 */
 
/** Penalty weights from ISO Table 11. */
export const PENALTY_N1 = 3;
export const PENALTY_N2 = 3;
export const PENALTY_N3 = 40;
export const PENALTY_N4 = 10;
 
/** The eight mask conditions (ISO Table 10). True means "invert this module". */
export function maskAt(mask: number, x: number, y: number): boolean {
  switch (mask) {
    case 0:
      return (x + y) % 2 === 0;
    case 1:
      return y % 2 === 0;
    case 2:
      return x % 3 === 0;
    case 3:
      return (x + y) % 3 === 0;
    case 4:
      return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
    case 5:
      return ((x * y) % 2) + ((x * y) % 3) === 0;
    case 6:
      return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
    case 7:
      return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
    default:
      throw new RangeError(`Mask pattern must be 0–7, got ${mask}`);
  }
}
 
/**
 * XOR a mask over every non-function module. XOR is its own inverse, so
 * calling this twice with the same mask restores the original.
 */
export function applyMask(raw: RawMatrix, mask: number): void {
  const { size, modules, reserved } = raw;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const i = y * size + x;
      if (reserved[i] === 1) continue;
      if (maskAt(mask, x, y)) modules[i] ^= 1;
    }
  }
}
 
/** The 11-module finder-lookalike patterns rule 3 penalizes. */
const FINDER_LIKE_A = 0b10111010000;
const FINDER_LIKE_B = 0b00001011101;
 
/**
 * Total penalty for a masked symbol — lower is better.
 *
 * Rule 4 follows the ZXing/ISO reading (`floor(|dark% − 50| / 5)` steps), which
 * differs at exact 5% boundaries from the `ceil`-based approximation some
 * libraries use. Implementations legitimately disagree here and every choice
 * produces a valid symbol, so this package pins the spec reading and proves
 * the result by decoding every generated code in tests.
 */
export function penaltyScore(modules: Readonly<Uint8Array>, size: number): number {
  let score = 0;
 
  // Rules 1 and 3 — run lengths and finder lookalikes, scanned both ways.
  for (let a = 0; a < size; a++) {
    let rowRun = 1;
    let colRun = 1;
    let rowBits = 0;
    let colBits = 0;
 
    for (let b = 0; b < size; b++) {
      const rowValue = modules[a * size + b]!;
      const colValue = modules[b * size + a]!;
 
      if (b > 0) {
        rowRun = rowValue === modules[a * size + b - 1]! ? rowRun + 1 : 1;
        if (rowRun === 5) score += PENALTY_N1;
        else if (rowRun > 5) score += 1;
 
        colRun = colValue === modules[(b - 1) * size + a]! ? colRun + 1 : 1;
        if (colRun === 5) score += PENALTY_N1;
        else if (colRun > 5) score += 1;
      }
 
      rowBits = ((rowBits << 1) & 0x7ff) | rowValue;
      colBits = ((colBits << 1) & 0x7ff) | colValue;
      if (b >= 10) {
        if (rowBits === FINDER_LIKE_A || rowBits === FINDER_LIKE_B) score += PENALTY_N3;
        if (colBits === FINDER_LIKE_A || colBits === FINDER_LIKE_B) score += PENALTY_N3;
      }
    }
  }
 
  // Rule 2 — solid 2×2 blocks.
  for (let y = 0; y < size - 1; y++) {
    for (let x = 0; x < size - 1; x++) {
      const value = modules[y * size + x]!;
      if (
        value === modules[y * size + x + 1]! &&
        value === modules[(y + 1) * size + x]! &&
        value === modules[(y + 1) * size + x + 1]!
      ) {
        score += PENALTY_N2;
      }
    }
  }
 
  // Rule 4 — deviation from an even dark/light balance, in 5% steps.
  let dark = 0;
  for (let i = 0; i < modules.length; i++) dark += modules[i]!;
  const total = size * size;
  score += Math.floor((Math.abs(dark * 2 - total) * 10) / total) * PENALTY_N4;
 
  return score;
}
 
function freeze(raw: RawMatrix, mask: number): QRMatrix {
  const { size, modules, reserved } = raw;
  return {
    version: raw.version,
    size,
    level: raw.level,
    mask,
    modules,
    reserved,
    get(x: number, y: number): boolean {
      if (x < 0 || y < 0 || x >= size || y >= size) return false;
      return modules[y * size + x] === 1;
    },
    isReserved(x: number, y: number): boolean {
      if (x < 0 || y < 0 || x >= size || y >= size) return false;
      return reserved[y * size + x] === 1;
    },
  };
}
 
/**
 * Try every mask (or just the forced one), keep the lowest-scoring, and write
 * the matching format information.
 *
 * @param raw - An unmasked symbol from `buildMatrix`. Mutated in place.
 * @param forced - Skip scoring and use this mask, 0–7.
 */
export function applyBestMask(raw: RawMatrix, forced?: number): QRMatrix {
  let chosen = forced;
 
  if (chosen === undefined) {
    let bestScore = Infinity;
    for (let mask = 0; mask < 8; mask++) {
      applyMask(raw, mask);
      drawFormatInfo(raw, raw.level, mask);
      const score = penaltyScore(raw.modules, raw.size);
      if (score < bestScore) {
        bestScore = score;
        chosen = mask;
      }
      applyMask(raw, mask); // XOR is its own inverse — undo before the next try
    }
  } else if (!Number.isInteger(chosen) || chosen < 0 || chosen > 7) {
    throw new RangeError(`Mask pattern must be an integer 0–7, got ${chosen}`);
  }
 
  applyMask(raw, chosen!);
  drawFormatInfo(raw, raw.level, chosen!);
  return freeze(raw, chosen!);
}