/** * End-to-end: build a .td5 from nothing but text, then read it back and check * the sign would accept it (CRC), and that the pixels survive intact. */ import { writeFileSync } from 'node:fs'; import { FONTS } from '../src/fonts.js'; import { fontFromJSON, renderText, trimX } from '../src/codec/bitfont.mjs'; import { parseTd5, buildTd5, pixelsToFrame, frameToPixels, crc16arc, EFFECT } from '../src/codec/td5.mjs'; const SCREEN = { width: 112, height: 16 }; const font = fontFromJSON(FONTS.ASC1609); const LIST = [ 'SCHOOL BUS', 'CHARTER', 'RAIL BUS', 'EXPRESS COACH LINES', 'CARMEL COLLEGE', 'OC1', 'NOT IN SERVICE', 'GO LIONS!', ]; function fit(bmp, height) { if (bmp.height === height) return bmp; const out = new Uint8Array(bmp.width * height); const y0 = Math.floor((height - bmp.height) / 2); for (let y = 0; y < bmp.height; y++) { const ty = y + y0; if (ty >= 0 && ty < height) out.set(bmp.pixels.subarray(y * bmp.width, (y + 1) * bmp.width), ty * bmp.width); } return { width: bmp.width, height, pixels: out }; } const destinations = LIST.map((text) => { const bmp = fit(trimX(renderText(font, text)), SCREEN.height); const frame = pixelsToFrame(bmp.pixels, bmp.width, SCREEN.height); const effect = bmp.width > SCREEN.width ? EFFECT.SCROLL : EFFECT.HOLD; return { name: text.slice(0, 16), lineName: text.slice(0, 16), frames: [frame], effects: [effect, effect] }; }); // one two-page destination, to exercise the multi-frame path const pages = ['MORETON BAY', 'BOYS COLLEGE'].map((t) => { const b = fit(trimX(renderText(font, t)), SCREEN.height); return pixelsToFrame(b.pixels, b.width, SCREEN.height); }); destinations.push({ name: 'MORETON BAY', lineName: 'MORETON BAY', frames: pages, effects: [EFFECT.HOLD, EFFECT.HOLD, EFFECT.HOLD, EFFECT.HOLD] }); const bytes = buildTd5({ company: 'Test Coaches', screen: SCREEN, destinations }); writeFileSync(new URL('./authored.td5', import.meta.url), bytes); // ---- read it back as if we were the sign const back = parseTd5(bytes); let fails = 0; const check = (cond, msg) => { if (!cond) { fails++; console.log(' āŒ ' + msg); } }; check(back.company === 'Test Coaches', 'company survived'); check(back.destinations.length === destinations.length, 'destination count'); check(back.screen.width === 112 && back.screen.height === 16, 'screen size'); check(bytes.length % 0x200 === 0, 'file padded to a 512-byte boundary'); for (const d of back.destinations) { check(d.crcOk, `${d.name}: CRC verifies`); const src = destinations.find((x) => x.name === d.name); check(src && d.frames.length === src.frames.length, `${d.name}: frame count`); d.frames.forEach((f, i) => { const a = Buffer.from(f.data), b = Buffer.from(src.frames[i].data); check(a.equals(b), `${d.name}: frame ${i} pixels identical`); }); } // independent CRC verification, byte-for-byte against the stored value const dv = new DataView(bytes.buffer); for (let i = 0; i < back.destinations.length; i++) { const ptr = dv.getUint32(0x200 + i * 4, true); const off = dv.getUint32(ptr + 0x30, true); const len = dv.getUint32(ptr + 0x34, true); const stored = bytes[off + 1] | (bytes[off + 2] << 8); check(stored === crc16arc(bytes.subarray(off + 3, off + len)), `block ${i}: CRC recomputes`); } console.log(`authored ${destinations.length} destinations -> ${bytes.length} bytes (test/authored.td5)`); for (const d of back.destinations) { const w = d.frames[0].width; console.log(` ${d.name.padEnd(17)} ${String(w).padStart(3)}px ${w > 112 ? 'scrolls' : 'fits '} ` + `${d.frames.length > 1 ? d.frames.length + ' pages' : ''}`); } console.log(fails === 0 ? '\nāœ… every check passed — a sign-ready file built from scratch' : `\nāŒ ${fails} checks failed`); process.exit(fails ? 1 : 0);