12345678910111213import { rsDecodeBlock } from "./rs";14import {15 MAYAN_MAGIC,16 MAYAN_V2,17 RS_DATA,18 RS_PARITY,19 RS_BLOCK,20 HEADER_BYTES,21 QUIET_ZONE,22 ANCHOR_FOOTPRINT,23 MIN_SYMBOL_SIZE,24 MAX_SYMBOL_SIZE,25 isReserved,26} from "./mayan";27282930313233343536export function decodeMayan(imageData: ImageData): Uint8Array | null {37 const binary = adaptiveBinarize(imageData);38 const W = imageData.width;39 const H = imageData.height;4041 const anchors = findAnchors(binary, W, H);42 if (anchors.length < 3) return null;4344 const orient = classifyAnchors(anchors);45 if (!orient) return null;46 const { TL, TR, BL } = orient;47 const modulePx = orient.modulePx;48 if (modulePx < 1) return null;4950 51 52 53 const distH = Math.hypot(TR.x - TL.x, TR.y - TL.y);54 const distV = Math.hypot(BL.x - TL.x, BL.y - TL.y);55 const span = (distH + distV) / 2 / modulePx;56 let estSize = Math.round(span) + 2 * QUIET_ZONE + ANCHOR_FOOTPRINT;57 estSize = clamp(estSize, MIN_SYMBOL_SIZE, MAX_SYMBOL_SIZE);5859 const matrix = sampleAndReadExact(binary, TL, TR, BL, estSize);60 if (!matrix) return null;6162 return decodeMatrix(matrix.matrix, matrix.size);63}64656667export function decodeMatrix(68 matrix: boolean[][],69 symbolSize: number,70): Uint8Array | null {71 72 73 74 75 const allBits = readDataBits(matrix, symbolSize, Infinity);76 const interleaved = new Uint8Array(bitsToBytes(allBits));7778 if (interleaved.length === 0) return null;79 const maxK = Math.floor(interleaved.length / RS_BLOCK);80 81 82 83 84 85 for (let k = 1; interleaved[0] === MAYAN_MAGIC && k <= maxK; k++) {86 if (5 * k >= interleaved.length) break;87 const version = interleaved[k];88 if (version !== MAYAN_V2) continue;89 const length = interleaved[2 * k]90 | (interleaved[3 * k] << 8)91 | (interleaved[4 * k] << 16)92 | (interleaved[5 * k] << 24);93 if (length < 0 || length > 100_000_000) continue;94 if (Math.ceil((length + HEADER_BYTES) / RS_DATA) !== k) continue;95 const decoded = rsDecodeAtBlocks(interleaved, k);96 if (!decoded) continue;97 98 99 if (decoded[0] !== MAYAN_MAGIC || decoded[1] !== MAYAN_V2) continue;100 const len2 = decoded[2] | (decoded[3] << 8) | (decoded[4] << 16) | (decoded[5] << 24);101 if (len2 < 0 || len2 + HEADER_BYTES > decoded.length) continue;102 return decoded.slice(HEADER_BYTES, HEADER_BYTES + len2);103 }104105 106 107 for (let k = 1; k <= maxK; k++) {108 const decoded = rsDecodeAtBlocks(interleaved, k);109 if (!decoded) continue;110 if (decoded[0] !== MAYAN_MAGIC) continue;111 if (decoded[1] !== MAYAN_V2) continue;112 const len = decoded[2] | (decoded[3] << 8) | (decoded[4] << 16) | (decoded[5] << 24);113 if (len < 0 || len + HEADER_BYTES > decoded.length) continue;114 if (Math.ceil((len + HEADER_BYTES) / RS_DATA) !== k) continue;115 return decoded.slice(HEADER_BYTES, HEADER_BYTES + len);116 }117118 return null;119}120121122123124function rsDecodeAtBlocks(125 interleaved: Uint8Array,126 numBlocks: number,127): Uint8Array | null {128 if (interleaved.length < numBlocks * RS_BLOCK) return null;129 const out = new Uint8Array(numBlocks * RS_DATA);130 const block = new Uint8Array(RS_BLOCK);131 for (let i = 0; i < numBlocks; i++) {132 for (let j = 0; j < RS_BLOCK; j++) {133 block[j] = interleaved[i + j * numBlocks];134 }135 const corrected = rsDecodeBlock(block, RS_PARITY);136 if (!corrected) return null;137 out.set(corrected, i * RS_DATA);138 }139 return out;140}141142143144function adaptiveBinarize(imageData: ImageData): boolean[][] {145 const { data, width, height } = imageData;146 const out: boolean[][] = Array.from(147 { length: height },148 () => new Array<boolean>(width).fill(false),149 );150 const R = 9;151 const integral = new Float64Array((width + 1) * (height + 1));152 for (let y = 0; y < height; y++) {153 let rowSum = 0;154 for (let x = 0; x < width; x++) {155 const i = (y * width + x) * 4;156 const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];157 rowSum += gray;158 integral[(y + 1) * (width + 1) + (x + 1)] =159 integral[y * (width + 1) + (x + 1)] + rowSum;160 }161 }162 for (let y = 0; y < height; y++) {163 for (let x = 0; x < width; x++) {164 const i = (y * width + x) * 4;165 const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];166 const x0 = Math.max(0, x - R);167 const y0 = Math.max(0, y - R);168 const x1 = Math.min(width - 1, x + R);169 const y1 = Math.min(height - 1, y + R);170 const sum =171 integral[(y1 + 1) * (width + 1) + (x1 + 1)] -172 integral[(y0) * (width + 1) + (x1 + 1)] -173 integral[(y1 + 1) * (width + 1) + (x0)] +174 integral[(y0) * (width + 1) + (x0)];175 const count = (x1 - x0 + 1) * (y1 - y0 + 1);176 out[y][x] = gray < sum / count - 8;177 }178 }179 return out;180}181182183184interface AnchorMatch {185 cx: number;186 cy: number;187 modulePx: number;188}189190interface RawCandidate {191 cx: number;192 cy: number;193 modulePx: number;194}195196197198199200201function findAnchors(binary: boolean[][], W: number, H: number): AnchorMatch[] {202 const candidates: RawCandidate[] = [];203204 for (let y = 0; y < H; y += 2) {205 scanLineForAnchor(206 (i) => binary[y]?.[i] ?? false,207 W,208 (cxRel, modulePx) => candidates.push({ cx: cxRel, cy: y, modulePx }),209 );210 }211212 const confirmed: AnchorMatch[] = [];213 for (const c of candidates) {214 if (c.cx < 0 || c.cx >= W) continue;215 const hits: { cy: number; modulePx: number }[] = [];216 scanLineForAnchor(217 (i) => binary[i]?.[c.cx] ?? false,218 H,219 (cyRel, modulePx) => hits.push({ cy: cyRel, modulePx }),220 );221 let best: { cy: number; modulePx: number } | null = null;222 for (const h of hits) {223 if (Math.abs(h.cy - c.cy) > c.modulePx * 5) continue;224 if (!best || Math.abs(h.cy - c.cy) < Math.abs(best.cy - c.cy)) best = h;225 }226 if (!best) continue;227 const cy = Math.round((c.cy + best.cy) / 2);228 const modulePx = (c.modulePx + best.modulePx) / 2;229 if (!verifyAnchorCenter(binary, c.cx, cy, modulePx, W, H)) continue;230 confirmed.push({ cx: c.cx, cy, modulePx });231 }232233 return clusterAnchors(confirmed);234}235236237238239240241function verifyAnchorCenter(242 binary: boolean[][],243 cx: number, cy: number,244 modulePx: number,245 W: number, H: number,246): boolean {247 248 const step = modulePx;249 let dark = 0, total = 0;250 for (let dr = -1; dr <= 1; dr++) {251 for (let dc = -1; dc <= 1; dc++) {252 const x = Math.round(cx + dc * step);253 const y = Math.round(cy + dr * step);254 if (x < 0 || x >= W || y < 0 || y >= H) return false;255 total++;256 if (binary[y][x]) dark++;257 }258 }259 if (dark < total * 0.8) return false;260261 262 263 let light = 0, ringTotal = 0;264 for (let dr = -2; dr <= 2; dr++) {265 for (let dc = -2; dc <= 2; dc++) {266 if (Math.abs(dr) < 2 && Math.abs(dc) < 2) continue; 267 const x = Math.round(cx + dc * step);268 const y = Math.round(cy + dr * step);269 if (x < 0 || x >= W || y < 0 || y >= H) continue;270 ringTotal++;271 if (!binary[y][x]) light++;272 }273 }274 return ringTotal > 0 && light >= ringTotal * 0.5;275}276277278279function scanLineForAnchor(280 pixel: (i: number) => boolean,281 N: number,282 onMatch: (center: number, modulePx: number) => void,283): void {284 285 286 287 const runs: { len: number; dark: boolean; start: number }[] = [];288 let i = 0;289 let cur = pixel(0);290 let runStart = 0;291 while (i <= N) {292 const px = i < N ? pixel(i) : !cur;293 if (px !== cur || i === N) {294 runs.push({ len: i - runStart, dark: cur, start: runStart });295 runStart = i;296 cur = px;297 298 if (runs.length > 5) runs.shift();299 if (runs.length === 5) {300 const [r0, r1, r2, r3, r4] = runs;301 302 if (!r0.dark && r1.dark && !r2.dark && r3.dark && !r4.dark) {303 const total = r0.len + r1.len + r2.len + r3.len + r4.len;304 const unit = total / 7;305 if (unit >= 1 &&306 within(r0.len, unit, 0.5) &&307 within(r1.len, unit, 0.5) &&308 within(r2.len, unit * 3, 0.8) &&309 within(r3.len, unit, 0.5) &&310 within(r4.len, unit, 0.5)) {311 const center = r2.start + r2.len / 2;312 onMatch(Math.round(center), unit);313 }314 }315 316 317 318 if (r0.dark && !r1.dark && r2.dark && !r3.dark && r4.dark) {319 const total = r0.len + r1.len + r2.len + r3.len + r4.len;320 const unit = total / 7;321 if (unit >= 1 &&322 within(r0.len, unit, 0.5) &&323 within(r1.len, unit, 0.5) &&324 within(r2.len, unit * 3, 0.8) &&325 within(r3.len, unit, 0.5) &&326 within(r4.len, unit, 0.5)) {327 const center = r2.start + r2.len / 2;328 onMatch(Math.round(center), unit);329 }330 }331 }332 }333 i++;334 }335}336337function within(value: number, target: number, tolerance: number): boolean {338 return Math.abs(value - target) <= target * tolerance;339}340341342343function clusterAnchors(matches: AnchorMatch[]): AnchorMatch[] {344 const clusters: AnchorMatch[][] = [];345 for (const m of matches) {346 let placed = false;347 for (const cluster of clusters) {348 const ref = cluster[0];349 const distSq = (m.cx - ref.cx) ** 2 + (m.cy - ref.cy) ** 2;350 if (distSq <= (ref.modulePx * 3) ** 2) {351 cluster.push(m);352 placed = true;353 break;354 }355 }356 if (!placed) clusters.push([m]);357 }358 return clusters.map((cluster) => {359 let sx = 0, sy = 0, sm = 0;360 for (const c of cluster) { sx += c.cx; sy += c.cy; sm += c.modulePx; }361 return {362 cx: sx / cluster.length,363 cy: sy / cluster.length,364 modulePx: sm / cluster.length,365 };366 });367}368369370371interface Orientation {372 TL: Point;373 TR: Point;374 BL: Point;375 modulePx: number;376}377378interface Point { x: number; y: number }379380381382383384385386387388389function classifyAnchors(anchors: AnchorMatch[]): Orientation | null {390 if (anchors.length < 3) return null;391392 let best: {393 TL: AnchorMatch; TR: AnchorMatch; BL: AnchorMatch; score: number;394 } | null = null;395396 const N = anchors.length;397 for (let i = 0; i < N; i++) {398 for (let j = 0; j < N; j++) {399 if (j === i) continue;400 for (let k = 0; k < N; k++) {401 if (k === i || k === j) continue;402 const tl = anchors[i];403 const a = anchors[j];404 const b = anchors[k];405 const va = { x: a.cx - tl.cx, y: a.cy - tl.cy };406 const vb = { x: b.cx - tl.cx, y: b.cy - tl.cy };407 const lenA = Math.hypot(va.x, va.y);408 const lenB = Math.hypot(vb.x, vb.y);409 if (lenA < 1 || lenB < 1) continue;410 411 412 const cross = va.x * vb.y - va.y * vb.x;413 if (cross <= 0) continue;414 415 const cos = (va.x * vb.x + va.y * vb.y) / (lenA * lenB);416 const angularScore = -Math.abs(cos);417 418 const legRatio = Math.min(lenA, lenB) / Math.max(lenA, lenB);419 const lengthScore = -Math.abs(1 - legRatio);420 421 const meanM = (tl.modulePx + a.modulePx + b.modulePx) / 3;422 const varM = (423 (tl.modulePx - meanM) ** 2 +424 (a.modulePx - meanM) ** 2 +425 (b.modulePx - meanM) ** 2426 ) / 3;427 const stdM = Math.sqrt(varM);428 const moduleScore = -stdM / Math.max(meanM, 1);429 430 const sizeScore = Math.log(lenA + lenB) * 0.1;431 const score =432 angularScore * 5 +433 lengthScore * 5 +434 moduleScore * 8 +435 sizeScore;436 if (!best || score > best.score) {437 best = { TL: tl, TR: a, BL: b, score };438 }439 }440 }441 }442 if (!best) return null;443 const modulePx = (best.TL.modulePx + best.TR.modulePx + best.BL.modulePx) / 3;444 return {445 TL: { x: best.TL.cx, y: best.TL.cy },446 TR: { x: best.TR.cx, y: best.TR.cy },447 BL: { x: best.BL.cx, y: best.BL.cy },448 modulePx,449 };450}451452453454455456457458function computeHomography(src: Point[], dst: Point[]): number[] | null {459 if (src.length !== 4 || dst.length !== 4) return null;460 461 462 463 const A: number[][] = [];464 const b: number[] = [];465 for (let i = 0; i < 4; i++) {466 const sx = src[i].x, sy = src[i].y;467 const dx = dst[i].x, dy = dst[i].y;468 A.push([sx, sy, 1, 0, 0, 0, -sx * dx, -sy * dx]);469 b.push(dx);470 A.push([0, 0, 0, sx, sy, 1, -sx * dy, -sy * dy]);471 b.push(dy);472 }473 const x = solve8(A, b);474 if (!x) return null;475 return [x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], 1];476}477478479function solve8(A: number[][], b: number[]): number[] | null {480 const n = 8;481 const M: number[][] = A.map((row, i) => [...row, b[i]]);482 for (let col = 0; col < n; col++) {483 let pivot = col;484 for (let r = col + 1; r < n; r++) {485 if (Math.abs(M[r][col]) > Math.abs(M[pivot][col])) pivot = r;486 }487 if (Math.abs(M[pivot][col]) < 1e-12) return null;488 [M[col], M[pivot]] = [M[pivot], M[col]];489 for (let r = 0; r < n; r++) {490 if (r === col) continue;491 const factor = M[r][col] / M[col][col];492 if (factor === 0) continue;493 for (let c = col; c <= n; c++) M[r][c] -= factor * M[col][c];494 }495 }496 const x: number[] = new Array(n);497 for (let i = 0; i < n; i++) x[i] = M[i][n] / M[i][i];498 return x;499}500501function applyHomography(H: number[], px: number, py: number): Point {502 const w = H[6] * px + H[7] * py + H[8];503 return {504 x: (H[0] * px + H[1] * py + H[2]) / w,505 y: (H[3] * px + H[4] * py + H[5]) / w,506 };507}508509function invertHomography(H: number[]): number[] | null {510 const a = H[0], b = H[1], c = H[2];511 const d = H[3], e = H[4], f = H[5];512 const g = H[6], h = H[7], i = H[8];513 const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);514 if (Math.abs(det) < 1e-12) return null;515 const inv = 1 / det;516 return [517 (e * i - f * h) * inv,518 (c * h - b * i) * inv,519 (b * f - c * e) * inv,520 (f * g - d * i) * inv,521 (a * i - c * g) * inv,522 (c * d - a * f) * inv,523 (d * h - e * g) * inv,524 (b * g - a * h) * inv,525 (a * e - b * d) * inv,526 ];527}528529530531532533534function anchorModuleCenter(size: number, corner: "TL" | "TR" | "BL" | "BR"): Point {535 const q = QUIET_ZONE;536 537 538 const tlx = q + 4 + 0.5;539 const tly = q + 4 + 0.5;540 const farx = size - q - 5 + 0.5; 541 const fary = size - q - 5 + 0.5;542 switch (corner) {543 case "TL": return { x: tlx, y: tly };544 case "TR": return { x: farx, y: tly };545 case "BL": return { x: tlx, y: fary };546 case "BR": return { x: farx, y: fary };547 }548}549550551552553function sampleGrid(554 binary: boolean[][],555 TL: Point, TR: Point, BL: Point,556 size: number,557): boolean[][] | null {558 559 const BR: Point = {560 x: TL.x + (TR.x - TL.x) + (BL.x - TL.x),561 y: TL.y + (TR.y - TL.y) + (BL.y - TL.y),562 };563 564 const src = [TL, TR, BL, BR];565 const dst = [566 anchorModuleCenter(size, "TL"),567 anchorModuleCenter(size, "TR"),568 anchorModuleCenter(size, "BL"),569 anchorModuleCenter(size, "BR"),570 ];571 572 const Hinv = computeHomography(dst, src);573 if (!Hinv) return null;574575 const matrix: boolean[][] = Array.from(576 { length: size },577 () => new Array<boolean>(size).fill(false),578 );579 const H = binary.length;580 const W = binary[0]?.length ?? 0;581 for (let r = 0; r < size; r++) {582 for (let c = 0; c < size; c++) {583 const p = applyHomography(Hinv, c + 0.5, r + 0.5);584 const ix = Math.round(p.x);585 const iy = Math.round(p.y);586 if (iy >= 0 && iy < H && ix >= 0 && ix < W) {587 matrix[r][c] = binary[iy][ix];588 }589 }590 }591 return matrix;592}593594595596function sampleAndReadExact(597 binary: boolean[][],598 TL: Point, TR: Point, BL: Point,599 estSize: number,600): { matrix: boolean[][]; size: number } | null {601 const rough = sampleGrid(binary, TL, TR, BL, estSize);602 if (!rough) return null;603 const exact = readSizeBits(rough);604 if (exact >= MIN_SYMBOL_SIZE && exact <= MAX_SYMBOL_SIZE && exact !== estSize) {605 const final = sampleGrid(binary, TL, TR, BL, exact);606 if (!final) return null;607 return { matrix: final, size: exact };608 }609 if (exact < MIN_SYMBOL_SIZE || exact > MAX_SYMBOL_SIZE) {610 611 return { matrix: rough, size: estSize };612 }613 return { matrix: rough, size: exact };614}615616function readSizeBits(matrix: boolean[][]): number {617 const row = QUIET_ZONE;618 const colStart = QUIET_ZONE + ANCHOR_FOOTPRINT + 1;619 let val = 0;620 for (let i = 0; i < 16; i++) {621 if (matrix[row]?.[colStart + i]) val |= 1 << (15 - i);622 }623 return val;624}625626627628function readDataBits(629 matrix: boolean[][],630 size: number,631 count: number,632): boolean[] {633 const bits: boolean[] = [];634 for (let r = QUIET_ZONE; r < size - QUIET_ZONE && bits.length < count; r++) {635 const row = matrix[r];636 if (!row) continue;637 for (let c = QUIET_ZONE; c < size - QUIET_ZONE && bits.length < count; c++) {638 if (isReserved(r, c, size)) continue;639 bits.push(row[c] ?? false);640 }641 }642 return bits;643}644645function bitsToBytes(bits: boolean[]): number[] {646 const bytes: number[] = [];647 for (let i = 0; i < bits.length; i += 8) {648 let byte = 0;649 for (let j = 0; j < 8 && i + j < bits.length; j++) {650 if (bits[i + j]) byte |= 1 << (7 - j);651 }652 bytes.push(byte);653 }654 return bytes;655}656657function clamp(v: number, lo: number, hi: number): number {658 return Math.max(lo, Math.min(hi, v));659}