123456789101112131415161718192021222324import {25 AztecCodeReader,26 AztecEncoder,27 BinaryBitmap,28 GlobalHistogramBinarizer,29 RGBLuminanceSource,30} from "@zxing/library";31import type { BitMatrix } from "@zxing/library";32import {33 AZTEC_ECC_PERCENT,34 CodexError,35 type ChunkEnvelope,36} from "../../shared/codex-types";3738export interface AztecImage {39 pngBytes: Uint8Array;40 width: number;41 height: number;42}4344454647484950export function buildAztecMatrix(payload: string): BitMatrix {51 const dataBytes = new Uint8Array(payload.length);52 for (let i = 0; i < payload.length; i++) dataBytes[i] = payload.charCodeAt(i);53 const aztecCode = AztecEncoder.encode(dataBytes, AZTEC_ECC_PERCENT, 0 );54 const matrix = aztecCode.getMatrix();55 if (!matrix) throw new Error("AztecEncoder returned null matrix");56 return matrix;57}585960616263646566676869export function verifyMatrixRoundTrip(matrix: BitMatrix, payload: string): boolean {70 const modules = matrix.getWidth(); 71 const scale = 4;72 const pad = 2 * scale; 73 const imgW = modules * scale + pad * 2;74 const imgH = imgW;7576 77 const WHITE = -1; 78 const BLACK = (0xff << 24) | 0; 7980 const argb = new Int32Array(imgW * imgH);81 argb.fill(WHITE);8283 for (let y = 0; y < modules; y++) {84 for (let x = 0; x < modules; x++) {85 if (matrix.get(x, y)) {86 const baseRow = (pad + y * scale) * imgW + pad + x * scale;87 for (let dy = 0; dy < scale; dy++) {88 for (let dx = 0; dx < scale; dx++) {89 argb[baseRow + dy * imgW + dx] = BLACK;90 }91 }92 }93 }94 }9596 try {97 const source = new RGBLuminanceSource(argb, imgW, imgH);98 const binarizer = new GlobalHistogramBinarizer(source);99 const bitmap = new BinaryBitmap(binarizer);100 const reader = new AztecCodeReader();101 return reader.decode(bitmap).getText() === payload;102 } catch {103 return false;104 }105}106107108109async function matrixToPngBrowser(matrix: BitMatrix): Promise<AztecImage> {110 const modules = matrix.getWidth();111 const scale = 4;112 const pad = 2 * scale;113 const size = modules * scale + pad * 2;114115 const canvas = new OffscreenCanvas(size, size);116 const ctx = canvas.getContext("2d")!;117 ctx.fillStyle = "#ffffff";118 ctx.fillRect(0, 0, size, size);119 ctx.fillStyle = "#000000";120 for (let y = 0; y < modules; y++) {121 for (let x = 0; x < modules; x++) {122 if (matrix.get(x, y)) {123 ctx.fillRect(pad + x * scale, pad + y * scale, scale, scale);124 }125 }126 }127128 const blob = await canvas.convertToBlob({ type: "image/png" });129 const pngBytes = new Uint8Array(await blob.arrayBuffer());130 return { pngBytes, width: size, height: size };131}132133134135interface BwipNodeModule {136 toBuffer: (opts: Record<string, unknown>) => Promise<Buffer>;137}138139let cachedBwipNode: BwipNodeModule | null = null;140141async function loadBwipNode(): Promise<BwipNodeModule> {142 if (cachedBwipNode) return cachedBwipNode;143 144 const ns = (await import( "bwip-js/node")) as Record<string, unknown>;145 const def = ns["default"] as Record<string, unknown> | undefined;146 const merged = { ...(def ?? {}), ...ns } as unknown as BwipNodeModule;147 cachedBwipNode = merged;148 return merged;149}150151async function matrixToPngNode(payload: string): Promise<AztecImage> {152 const mod = await loadBwipNode();153 const opts = {154 bcid: "azteccode",155 text: payload,156 eclevel: AZTEC_ECC_PERCENT.toString(),157 scale: 4,158 padding: 2,159 monochrome: true,160 } as const;161162 const buf = await mod.toBuffer(opts);163 const pngBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength).slice();164 const { width, height } = readPngDimensions(pngBytes);165 return { pngBytes, width, height };166}167168169function readPngDimensions(png: Uint8Array): { width: number; height: number } {170 171 if (png.length < 24) {172 throw new CodexError("AZTEC_GENERATION_FAILED", "PNG buffer too small to contain IHDR");173 }174 const view = new DataView(png.buffer, png.byteOffset, png.byteLength);175 const width = view.getUint32(16, false);176 const height = view.getUint32(20, false);177 return { width, height };178}179180181182183184185186187188189190191192193194export async function generateVerifiedAztec(envelope: ChunkEnvelope): Promise<AztecImage> {195 const envelopeJson = JSON.stringify(envelope);196197 198 let matrix: BitMatrix;199 try {200 matrix = buildAztecMatrix(envelopeJson);201 } catch (err) {202 throw new CodexError(203 "AZTEC_GENERATION_FAILED",204 `AztecEncoder failed for chunk ${envelope.i}: ${err instanceof Error ? err.message : String(err)}`,205 );206 }207208 209 if (!verifyMatrixRoundTrip(matrix, envelopeJson)) {210 211 let diagSuffix = "";212 try {213 const modules = matrix.getWidth();214 const scale = 4;215 const pad = 2 * scale;216 const imgW = modules * scale + pad * 2;217 const WHITE = -1;218 const BLACK = (0xff << 24) | 0;219 const argb = new Int32Array(imgW * imgW);220 argb.fill(WHITE);221 for (let y = 0; y < modules; y++)222 for (let x = 0; x < modules; x++)223 if (matrix.get(x, y)) {224 const base = (pad + y * scale) * imgW + pad + x * scale;225 for (let dy = 0; dy < scale; dy++)226 for (let dx = 0; dx < scale; dx++)227 argb[base + dy * imgW + dx] = BLACK;228 }229 const source = new RGBLuminanceSource(argb, imgW, imgW);230 const binarizer = new GlobalHistogramBinarizer(source);231 const bitmap = new BinaryBitmap(binarizer);232 const decoded = new AztecCodeReader().decode(bitmap).getText();233 const el = envelopeJson.length;234 const dl = decoded.length;235 let diffAt = -1;236 for (let k = 0; k < Math.min(el, dl); k++) {237 if (envelopeJson[k] !== decoded[k]) { diffAt = k; break; }238 }239 const ctx = (s: string) => JSON.stringify(s.slice(Math.max(0, diffAt - 8), diffAt + 20));240 diagSuffix = ` decoded len=${dl} expected len=${el} first diff at ${diffAt}: expected ${ctx(envelopeJson)} got ${ctx(decoded)}`;241 } catch (diagErr) {242 diagSuffix = ` (decode threw: ${diagErr instanceof Error ? diagErr.message : String(diagErr)})`;243 }244 throw new CodexError(245 "AZTEC_ROUNDTRIP_FAILED",246 `Chunk ${envelope.i}: in-memory round-trip failed.${diagSuffix}`,247 );248 }249250 251 try {252 if (typeof OffscreenCanvas !== "undefined") {253 return await matrixToPngBrowser(matrix);254 }255 return await matrixToPngNode(envelopeJson);256 } catch (err) {257 if (err instanceof CodexError) throw err;258 throw new CodexError(259 "AZTEC_GENERATION_FAILED",260 `PNG render failed for chunk ${envelope.i}: ${err instanceof Error ? err.message : String(err)}`,261 );262 }263}