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 | 52x 52x 52x 52x 52x 1908x 121268x 61079x 61079x 61079x 305395x 1526975x 1526975x 1526975x 1526975x 52x 50x 50x 50x 2x 2x 2x 1x 1089x 1089x 1x 678x 1887x | import jsQR from "jsqr";
import type { QRMatrix } from "../types";
/**
* Test-only helpers that rasterize a matrix and read it back with a real QR
* decoder (`jsQR`).
*
* This is the strongest check available to the encoder: a round trip through
* an independent decoder validates the bit stream, the Reed–Solomon parity,
* the interleaving, the module placement, the chosen mask *and* the format
* information all at once. Comparing matrices against another encoder can only
* prove we agree with that encoder; decoding proves the symbol is actually a
* QR code.
*/
export interface RasterOptions {
/** Device pixels per module. */
scale?: number;
/** Quiet zone in modules. */
margin?: number;
}
export interface Raster {
data: Uint8ClampedArray;
width: number;
height: number;
}
/** Render a matrix to an RGBA buffer: black modules on white, with a quiet zone. */
export function rasterize(matrix: QRMatrix, options: RasterOptions = {}): Raster {
const scale = options.scale ?? 5;
const margin = options.margin ?? 4;
const side = (matrix.size + margin * 2) * scale;
const data = new Uint8ClampedArray(side * side * 4).fill(255);
for (let y = 0; y < matrix.size; y++) {
for (let x = 0; x < matrix.size; x++) {
if (!matrix.get(x, y)) continue;
const px = (x + margin) * scale;
const py = (y + margin) * scale;
for (let dy = 0; dy < scale; dy++) {
for (let dx = 0; dx < scale; dx++) {
const offset = ((py + dy) * side + px + dx) * 4;
data[offset] = 0;
data[offset + 1] = 0;
data[offset + 2] = 0;
}
}
}
}
return { data, width: side, height: side };
}
/** Decode a matrix back to text, or `null` when no scanner could read it. */
export function decodeMatrix(matrix: QRMatrix, options?: RasterOptions): string | null {
const raster = rasterize(matrix, options);
const result = jsQR(raster.data, raster.width, raster.height);
return result ? result.data : null;
}
/** Decode a matrix back to raw bytes, or `null` when unreadable. */
export function decodeMatrixBytes(matrix: QRMatrix, options?: RasterOptions): number[] | null {
const raster = rasterize(matrix, options);
const result = jsQR(raster.data, raster.width, raster.height);
return result ? result.binaryData : null;
}
/**
* Decode what the renderer would actually paint for a **square-module** style.
*
* A `ModuleField` already has the logo's excavation and the eye/data split
* applied, so re-adding the finder patterns reconstructs exactly the pixels a
* default-styled render produces. That makes "does a logo at this size still
* scan?" answerable in a unit test instead of only in browser QA.
*/
export function decodeField(
matrix: QRMatrix,
field: { size: number; isDark(x: number, y: number): boolean },
options: RasterOptions = {},
): string | null {
const painted: QRMatrix = {
...matrix,
get(x: number, y: number): boolean {
Iif (x < 0 || y < 0 || x >= field.size || y >= field.size) return false;
// Finder patterns are drawn as eyes, never excavated.
return field.isDark(x, y) || (isFinderModule(x, y, field.size) && matrix.get(x, y));
},
};
return decodeMatrix(painted, options);
}
function isFinderModule(x: number, y: number, size: number): boolean {
const corners = [
[0, 0],
[size - 7, 0],
[0, size - 7],
] as const;
return corners.some(([ox, oy]) => x >= ox && x < ox + 7 && y >= oy && y < oy + 7);
}
|