12345678910111213141516171819202122232425262728293031import { describe, expect, it } from "vitest";3233import type { BitMatrix } from "@zxing/library";34import { buildAztecMatrix } from "./aztec";35import { buildChunkEnvelope } from "./helpers";36import {37 cellRectFor,38 decodeAztecFromArgb,39 parseChunkEnvelope,40} from "./pdf-import";41import {42 CODE_SIZE,43 COL_GAP,44 HEADER_GAP,45 HEADER_HEIGHT,46 MARGIN,47 PAGE_HEIGHT,48 PAGE_WIDTH,49 ROW_STEP,50} from "./pdf-layout";51525354describe("parseChunkEnvelope", () => {55 const v3 = {56 v: 3 as const,57 c: "PX-TEST-1234",58 s: 0,59 S: 1,60 i: 0,61 t: 15,62 p: false,63 d: "AAAA",64 h: "ABCDEF1234567890ABCDEF1234567890",65 };6667 it("parses a v3 envelope (current generator format)", () => {68 expect(parseChunkEnvelope(JSON.stringify(v3))).toEqual(v3);69 });7071 it("parses a v2 envelope (legacy single-stripe)", () => {72 const v2 = {73 v: 2,74 c: "PX-TEST-1234",75 i: 0,76 t: 15,77 p: false,78 d: "AAAA",79 h: "ABCDEF1234567890ABCDEF1234567890",80 };81 expect(parseChunkEnvelope(JSON.stringify(v2))).toEqual(v2);82 });8384 it("rejects a v3 envelope missing stripe metadata", () => {85 const broken = { ...v3, s: undefined, S: undefined };86 expect(parseChunkEnvelope(JSON.stringify(broken))).toBeNull();87 });8889 it("returns null for non-JSON input", () => {90 expect(parseChunkEnvelope("not json at all")).toBeNull();91 });9293 it("returns null for non-Peach JSON", () => {94 expect(parseChunkEnvelope(JSON.stringify({ hello: "world" }))).toBeNull();95 });9697 it("returns null when version is missing", () => {98 expect(99 parseChunkEnvelope(100 JSON.stringify({ c: "PX", i: 0, t: 5, p: false, d: "A", h: "A" }),101 ),102 ).toBeNull();103 });104105 it("returns null when fields have the wrong types", () => {106 expect(107 parseChunkEnvelope(108 JSON.stringify({109 v: 3,110 c: "PX",111 s: 0,112 S: 1,113 i: "0", 114 t: 5,115 p: false,116 d: "A",117 h: "A",118 }),119 ),120 ).toBeNull();121 });122123 it("returns null for v=1 envelopes", () => {124 expect(125 parseChunkEnvelope(126 JSON.stringify({127 v: 1,128 c: "PX",129 i: 0,130 t: 5,131 p: false,132 d: "A",133 h: "A",134 }),135 ),136 ).toBeNull();137 });138});139140141142describe("cellRectFor", () => {143 144 145 const SCALE = 2.0;146 const canvasW = Math.ceil(PAGE_WIDTH * SCALE);147 const canvasH = Math.ceil(PAGE_HEIGHT * SCALE);148149 it("covers every cell of the printed 6×6 grid", () => {150 for (let row = 0; row < 6; row++) {151 for (let col = 0; col < 6; col++) {152 const rect = cellRectFor(canvasW, canvasH, row, col);153 154 const codeXMin = (MARGIN + col * (CODE_SIZE + COL_GAP)) * SCALE;155 const codeXMax = codeXMin + CODE_SIZE * SCALE;156 const codeYMin =157 (MARGIN + HEADER_HEIGHT + HEADER_GAP + row * ROW_STEP) * SCALE;158 const codeYMax = codeYMin + CODE_SIZE * SCALE;159 expect(rect.sx).toBeLessThanOrEqual(codeXMin);160 expect(rect.sy).toBeLessThanOrEqual(codeYMin);161 expect(rect.sx + rect.sw).toBeGreaterThanOrEqual(codeXMax);162 expect(rect.sy + rect.sh).toBeGreaterThanOrEqual(codeYMax);163 }164 }165 });166167 it("places adjacent columns far enough apart that crops do not overlap into the next code", () => {168 169 170 for (let row = 0; row < 6; row++) {171 for (let col = 0; col < 5; col++) {172 const left = cellRectFor(canvasW, canvasH, row, col);173 const rightCodeXMin =174 (MARGIN + (col + 1) * (CODE_SIZE + COL_GAP)) * SCALE;175 expect(left.sx + left.sw).toBeLessThanOrEqual(rightCodeXMin);176 }177 }178 });179180 it("clamps to canvas bounds", () => {181 const rect = cellRectFor(canvasW, canvasH, 5, 5);182 expect(rect.sx + rect.sw).toBeLessThanOrEqual(canvasW);183 expect(rect.sy + rect.sh).toBeLessThanOrEqual(canvasH);184 });185});186187188189const WHITE = -1;190const BLACK = (0xff << 24) | 0;191192193194195196197198199200function renderMatrixToArgb(201 matrix: BitMatrix,202 moduleSize: number,203 padModules: number,204): { argb: Int32Array; width: number; height: number } {205 const modules = matrix.getWidth();206 const pad = padModules * moduleSize;207 const size = modules * moduleSize + pad * 2;208 const argb = new Int32Array(size * size).fill(WHITE);209 for (let y = 0; y < modules; y++) {210 for (let x = 0; x < modules; x++) {211 if (matrix.get(x, y)) {212 const baseRow = (pad + y * moduleSize) * size + pad + x * moduleSize;213 for (let dy = 0; dy < moduleSize; dy++) {214 for (let dx = 0; dx < moduleSize; dx++) {215 argb[baseRow + dy * size + dx] = BLACK;216 }217 }218 }219 }220 }221 return { argb, width: size, height: size };222}223224225226227228function blitArgb(229 src: Int32Array,230 srcW: number,231 srcH: number,232 dst: Int32Array,233 dstW: number,234 dx: number,235 dy: number,236): void {237 for (let y = 0; y < srcH; y++) {238 const dstRow = (dy + y) * dstW + dx;239 const srcRow = y * srcW;240 for (let x = 0; x < srcW; x++) {241 dst[dstRow + x] = src[srcRow + x]!;242 }243 }244}245246describe("decodeAztecFromArgb", () => {247 it("round-trips a real envelope JSON through the decode path", async () => {248 249 250 const envelope = await buildChunkEnvelope(251 new TextEncoder().encode("hello world".repeat(20)),252 0,253 36,254 false,255 "PX-TEST-1234",256 0,257 1,258 );259 const payload = JSON.stringify(envelope);260261 const matrix = buildAztecMatrix(payload);262 const { argb, width, height } = renderMatrixToArgb(matrix, 4, 2);263264 expect(decodeAztecFromArgb(argb, width, height)).toBe(payload);265 });266267 it("returns null for an all-white buffer", () => {268 const w = 100;269 const h = 100;270 const argb = new Int32Array(w * h).fill(WHITE);271 expect(decodeAztecFromArgb(argb, w, h)).toBeNull();272 });273274 it("returns null for an all-black buffer", () => {275 const w = 100;276 const h = 100;277 const argb = new Int32Array(w * h).fill(BLACK);278 expect(decodeAztecFromArgb(argb, w, h)).toBeNull();279 });280281 it("returns null for random noise", () => {282 const w = 200;283 const h = 200;284 const argb = new Int32Array(w * h);285 286 let s = 0x12345678;287 for (let i = 0; i < argb.length; i++) {288 s = (s * 1664525 + 1013904223) | 0;289 argb[i] = s & 1 ? BLACK : WHITE;290 }291 expect(decodeAztecFromArgb(argb, w, h)).toBeNull();292 });293});294295296297describe("page-grid integration", () => {298 it("decodes all 36 envelopes when laid out at the generator's grid positions", async () => {299 const SCALE = 2.0;300 const canvasW = Math.ceil(PAGE_WIDTH * SCALE);301 const canvasH = Math.ceil(PAGE_HEIGHT * SCALE);302 const page = new Int32Array(canvasW * canvasH).fill(WHITE);303304 305 306 307 const envelopes = await Promise.all(308 Array.from({ length: 36 }, (_, i) => {309 const data = new TextEncoder().encode(`chunk-${i}-`.padEnd(200, "x"));310 return buildChunkEnvelope(data, i, 36, false, "PX-TEST-1234", 0, 1);311 }),312 );313314 315 316 317 const codeBoxPx = Math.floor(CODE_SIZE * SCALE);318 for (let i = 0; i < envelopes.length; i++) {319 const env = envelopes[i]!;320 const matrix = buildAztecMatrix(JSON.stringify(env));321 const modules = matrix.getWidth();322 323 324 const moduleSize = Math.max(1, Math.floor(codeBoxPx / (modules + 4)));325 const { argb, width, height } = renderMatrixToArgb(matrix, moduleSize, 2);326 const row = Math.floor(i / 6);327 const col = i % 6;328 const dx = Math.round((MARGIN + col * (CODE_SIZE + COL_GAP)) * SCALE);329 const dy = Math.round(330 (MARGIN + HEADER_HEIGHT + HEADER_GAP + row * ROW_STEP) * SCALE,331 );332 blitArgb(argb, width, height, page, canvasW, dx, dy);333 }334335 336 337 const recovered: string[] = [];338 for (let row = 0; row < 6; row++) {339 for (let col = 0; col < 6; col++) {340 const { sx, sy, sw, sh } = cellRectFor(canvasW, canvasH, row, col);341 const cell = new Int32Array(sw * sh);342 for (let y = 0; y < sh; y++) {343 const srcRow = (sy + y) * canvasW + sx;344 const dstRow = y * sw;345 for (let x = 0; x < sw; x++) cell[dstRow + x] = page[srcRow + x]!;346 }347 const text = decodeAztecFromArgb(cell, sw, sh);348 expect(text, `cell (row=${row}, col=${col}) failed to decode`).not.toBeNull();349 recovered.push(text!);350 }351 }352353 expect(recovered).toHaveLength(36);354 for (let i = 0; i < 36; i++) {355 const parsed = parseChunkEnvelope(recovered[i]!);356 expect(parsed, `recovered envelope ${i} failed parse`).not.toBeNull();357 expect(parsed!.i).toBe(envelopes[i]!.i);358 expect(parsed!.h).toBe(envelopes[i]!.h);359 expect(parsed!.d).toBe(envelopes[i]!.d);360 }361 }, 30_000);362});