diff --git a/ts/src/file.ts b/ts/src/file.ts index 1e07ecd..1d240e0 100644 --- a/ts/src/file.ts +++ b/ts/src/file.ts @@ -255,6 +255,6 @@ export async function loadBinFile(url: string): Promise { * Build a single-element-always .bin buffer containing one Image2D element. * Convenience for tests; for production use MonoDisplayFile directly. */ -export function buildBinBuffer(el: Image2DElement): ArrayBuffer { +export function buildBinBuffer(el: Image2DElement): Uint8Array { return new MonoDisplayFile({ elements_always: [el] }).toBuffer(); } diff --git a/ts/src/helper.ts b/ts/src/helper.ts index 22d3e67..1b2f64d 100644 --- a/ts/src/helper.ts +++ b/ts/src/helper.ts @@ -11,7 +11,7 @@ export function packPixels(pixels: Uint8Array, width: number, height: number): U const total = width * height; const packed = new Uint8Array((total + 7) >> 3); for (let i = 0; i < total; i++) { - if (pixels[i]) packed[i >> 3] |= 1 << (i & 7); + if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7)); } return packed; } @@ -23,7 +23,7 @@ export function unpackPixels(packed: Uint8Array, width: number, height: number): const total = width * height; const pixels = new Uint8Array(total); for (let i = 0; i < total; i++) { - pixels[i] = (packed[i >> 3] >> (i & 7)) & 1; + pixels[i] = ((packed[i >> 3] ?? 0) >> (i & 7)) & 1; } return pixels; } diff --git a/ts/src/index.ts b/ts/src/index.ts index 37ae207..48ef98f 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -6,11 +6,7 @@ // // import { MonoDisplayDriver, loadBinFile } from "./index.ts"; // -// PLAN: if the project splits into sub-packages (parser / renderer / utils), -// this file will aggregate them here so callers never need to update imports. // ============================================================================= - - // ============================================================================= // MonoDisplay Library - binary format parser + canvas renderer // @@ -54,49 +50,9 @@ // ============================================================================= -export { - // Driver (primary browser-facing API) - MonoDisplayDriver, - - // File builder — JSON descriptor → binary .bin buffer - MonoDisplayFile, - ElementType, - - // Parser — useful when you want to parse without rendering - MonoDisplayParser, - - // Low-level binary reader — exposed so power users can parse custom sections - BinaryReader, - - // Renderer — exposed for embedding into custom canvas setups - MonoDisplayRenderer, - - // Convenience helpers - loadBinFile, - buildBinBuffer, -} from "./library"; - -export type { - // File builder types - MonoDisplayFileDescriptor, - DisplayElement, - Image2DElement, - AnimationFrame, - AnimationElement, - TextElement, - ScrollTextElement, - - // Union type for all display content variants - DisplayContent, - DisplayContentType, - - // Concrete content types - StaticFrame, - AnimFrame, - Animation, - TextDisplay, - ScrollText, - - // Driver options bag - MonoDisplayDriverOptions, -} from "./library"; +export * from "./types"; +export { BinaryReader, packPixels, unpackPixels, packedSize, pad32 } from "./helper"; +export { MonoDisplayParser, MONOFORMAT_MAGIC_HEADER } from "./parser"; +export { MonoDisplayRenderer } from "./renderer"; +export { type MonoDisplayDriverOptions, MonoDisplayDriver } from "./driver"; +export { MonoDisplayFile, loadBinFile, buildBinBuffer } from "./file"; diff --git a/ts/src/parser.ts b/ts/src/parser.ts index e019930..ac62c39 100644 --- a/ts/src/parser.ts +++ b/ts/src/parser.ts @@ -4,7 +4,7 @@ // --------------------------------------------------------------------------- import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper"; -import { type MonoFormatFile } from "./library"; +import { type MonoFormatFile } from "./types"; import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatPixelImage, type MonoFormatSection, type MonoFormatSectionFlags, type MonoFormatVScroll } from "./types"; // Magic 0xAF 0x7E 0x2B 0x63 read as uint32 LE diff --git a/ts/src/renderer.ts b/ts/src/renderer.ts index 9a5f1a8..5e6d7b5 100644 --- a/ts/src/renderer.ts +++ b/ts/src/renderer.ts @@ -14,6 +14,7 @@ import { type MonoFormatPixelImage, type MonoFormatVScroll } from "./types.js"; +import { type MonoDisplayDriverOptions } from "./driver"; /** * Renders a MonoFormatFile onto an HTMLCanvasElement. * Uses setInterval at 1000/fps ms per tick. All element state (animation @@ -151,7 +152,7 @@ export class MonoDisplayRenderer { let state = this.animState.get(id); if (!state) { state = { frame: 0, counter: 0 }; this.animState.set(id, state); } - this.#blitImage(el.frames[state.frame], el.xOffset, el.yOffset); + this.#blitImage(el.frames[state.frame]!, el.xOffset, el.yOffset); state.counter++; if (state.counter > el.updateInterval) { diff --git a/ts/test/library.test.ts b/ts/test/library.test.ts index 3041ee4..dc16af9 100644 --- a/ts/test/library.test.ts +++ b/ts/test/library.test.ts @@ -2,9 +2,6 @@ // library.test.ts — bun test suite for MonoDisplay library // // run: bun test -// -// Tests cover BinaryReader primitives and MonoDisplayParser with the real -// binary format: magic 0xAF7E2B63, section-based layout. // ============================================================================= import { test, expect, describe } from "bun:test"; @@ -13,12 +10,12 @@ import { MonoDisplayParser, MonoDisplayFile, ElementType, + SectionType, buildBinBuffer, - type StaticFrame, - type Animation, - type TextDisplay, - type ScrollText, -} from "../src/library"; + type Image2DElement, + type MonoFormatElementsAlways, + type MonoFormatImage2D, +} from "../src/index"; // --------------------------------------------------------------------------- // Low-level buffer helpers @@ -28,7 +25,9 @@ import { const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]); function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer { - const arrays = parts.map(p => p instanceof ArrayBuffer ? new Uint8Array(p) : p); + const arrays = parts.map((p: Uint8Array | ArrayBuffer) => + p instanceof ArrayBuffer ? new Uint8Array(p) : p + ); const total = arrays.reduce((s, a) => s + a.byteLength, 0); const out = new Uint8Array(total); let off = 0; @@ -36,30 +35,11 @@ function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer { return out.buffer; } -function u8(n: number) { return new Uint8Array([n]); } +function u8(n: number) { return new Uint8Array([n]); } function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; } function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); } function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; } -/** - * Build a well-formed file buffer with a single section. - * sectionType: 0x01=static 0x02=animation 0x03=text 0x04=scrolltext - * sectionData: raw section payload (after section header) - */ -function makeFile(sectionType: number, sectionData: Uint8Array): ArrayBuffer { - const fileHeader = concat( - MAGIC_BYTES, // magic - u32le(1), // version = 1 - u16le(1), // numSections = 1 - u16le(0), // reserved = 0 - ); - const sectionHeader = concat( - u8(sectionType), - u24le(sectionData.byteLength), - ); - return concat(fileHeader, sectionHeader, sectionData); -} - // --------------------------------------------------------------------------- // BinaryReader // --------------------------------------------------------------------------- @@ -159,273 +139,227 @@ describe("MonoDisplayParser — header", () => { const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0)); expect(() => parser.parse(buf)).toThrow(/version/i); }); - - test("rejects 0 sections", () => { - const buf = concat(MAGIC_BYTES, u32le(1), u16le(0), u16le(0)); // 0 sections - expect(() => parser.parse(buf)).toThrow(/0 section/i); - }); - - test("skips unknown section type, throws when no recognised section follows", () => { - // Section type 0xFF is unknown - const unknownData = new Uint8Array(4); - const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(1), u16le(0)); - const secHeader = concat(u8(0xFF), u24le(unknownData.byteLength)); - const buf = concat(fileHeader, secHeader, unknownData); - expect(() => parser.parse(buf)).toThrow(/no recognised/i); - }); -}); - -// --------------------------------------------------------------------------- -// MonoDisplayParser — static section -// --------------------------------------------------------------------------- - -describe("MonoDisplayParser — static", () => { - const parser = new MonoDisplayParser(); - - test("parses 2×2 static frame", () => { - const data = concat(u16le(2), u16le(2), new Uint8Array([1, 0, 0, 1])); - const buf = makeFile(0x01, new Uint8Array(data)); - - const result = parser.parse(buf) as StaticFrame; - expect(result.type).toBe("static"); - expect(result.width).toBe(2); - expect(result.height).toBe(2); - expect(result.pixels[0]).toBe(1); - expect(result.pixels[3]).toBe(1); - }); -}); - -// --------------------------------------------------------------------------- -// MonoDisplayParser — animation section -// --------------------------------------------------------------------------- - -describe("MonoDisplayParser — animation", () => { - const parser = new MonoDisplayParser(); - - test("parses 2-frame animation", () => { - const pixels = new Uint8Array(4); // 2×2 - const data = concat( - u16le(2), u16le(2), // width, height - u16le(2), // frameCount - u32le(100), pixels, // frame 1: 100ms, all-off - u32le(200), new Uint8Array([1,1,1,1]), // frame 2: 200ms, all-on - ); - const buf = makeFile(0x02, new Uint8Array(data)); - - const result = parser.parse(buf) as Animation; - expect(result.type).toBe("animation"); - expect(result.frames.length).toBe(2); - expect(result.frames[0].durationMs).toBe(100); - expect(result.frames[1].durationMs).toBe(200); - }); - - test("empty animation (0 frames) is valid", () => { - const data = concat(u16le(4), u16le(4), u16le(0)); - const buf = makeFile(0x02, new Uint8Array(data)); - const result = parser.parse(buf) as Animation; - expect(result.frames.length).toBe(0); - }); -}); - -// --------------------------------------------------------------------------- -// MonoDisplayParser — text section -// --------------------------------------------------------------------------- - -describe("MonoDisplayParser — text", () => { - const parser = new MonoDisplayParser(); - - test("parses text with align=center", () => { - const enc = new TextEncoder().encode("HELLO"); - const data = concat(u16le(enc.byteLength), enc, u8(0), u8(1)); // fontId=0 align=1 - const buf = makeFile(0x03, new Uint8Array(data)); - - const result = parser.parse(buf) as TextDisplay; - expect(result.type).toBe("text"); - expect(result.text).toBe("HELLO"); - expect(result.align).toBe(1); - expect(result.fontId).toBe(0); - }); - - test("handles multi-byte UTF-8 text", () => { - const str = "日本語"; - const enc = new TextEncoder().encode(str); - const data = concat(u16le(enc.byteLength), enc, u8(0), u8(0)); - const buf = makeFile(0x03, new Uint8Array(data)); - - const result = parser.parse(buf) as TextDisplay; - expect(result.text).toBe(str); - }); }); // --------------------------------------------------------------------------- -// MonoDisplayParser — scrolltext section +// MonoDisplayParser — Image2D round-trip // --------------------------------------------------------------------------- -describe("MonoDisplayParser — scrolltext", () => { +describe("MonoDisplayParser — Image2D round-trip", () => { const parser = new MonoDisplayParser(); - test("parses scrolltext left-direction", () => { - const enc = new TextEncoder().encode("SCROLL ME"); - const data = concat(u16le(enc.byteLength), enc, u16le(60), u8(0)); - const buf = makeFile(0x04, new Uint8Array(data)); - - const result = parser.parse(buf) as ScrollText; - expect(result.type).toBe("scrolltext"); - expect(result.text).toBe("SCROLL ME"); - expect(result.speedPps).toBe(60); - expect(result.direction).toBe(0); - }); - - test("handles multi-codepoint text incl. emoji", () => { - const str = "TICKER 🚀 日本語"; - const enc = new TextEncoder().encode(str); - const data = concat(u16le(enc.byteLength), enc, u16le(80), u8(1)); - const buf = makeFile(0x04, new Uint8Array(data)); - - const result = parser.parse(buf) as ScrollText; - expect(result.text).toBe(str); - expect(result.direction).toBe(1); + /** + * Build a minimal valid file buffer containing one ElementsAlways section + * with one Image2D element. + * + * File header (12 bytes): + * magic u32le + version u32le + numSections u16le + reserved u16le + * + * Section header (4 bytes): + * type u8 + size u24le (INCLUDES 4-byte header) + * + * ElementsAlways section data: + * flags u16le + numElements u16le + * + * Image2D element (12 fixed bytes + packed pixel data): + * type u16le + xOffset u16le + yOffset u16le + width u16le + height u16le + reserved u16le + * + packed 1bpp pixel data + padding to 4-byte boundary + */ + function makeImage2DFile( + width: number, + height: number, + pixels: Uint8Array, + xOffset: number = 0, + yOffset: number = 0, + ): ArrayBuffer { + // Pack pixels to 1bpp + const total = width * height; + const packedLen = (total + 7) >> 3; + const packed = new Uint8Array(packedLen); + for (let i = 0; i < total; i++) { + if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7)); + } + // Padding to 4-byte boundary for element (12 fixed bytes + packed data) + const elementDataLen = 12 + packedLen; + const padLen = (4 - (elementDataLen & 3)) & 3; + const padding = new Uint8Array(padLen); + + // Image2D element bytes + const element = new Uint8Array(concat( + u16le(1), // type = Image2D + u16le(xOffset), + u16le(yOffset), + u16le(width), + u16le(height), + u16le(0), // reserved + packed, + padding, + )); + + // Section data: flags + numElements + element + const sectionData = new Uint8Array(concat( + u16le(0), // flags = 0 + u16le(1), // numElements = 1 + element, + )); + + // Section header: type u8 + size u24le (4 header bytes + data) + const sectionSize = 4 + sectionData.byteLength; + const sectionHeader = new Uint8Array(concat( + u8(1), // SectionType.ElementsAlways = 1 + u24le(sectionSize), + )); + + // File header + const fileHeader = new Uint8Array(concat( + MAGIC_BYTES, + u32le(1), // version = 1 + u16le(1), // numSections = 1 + u16le(0), // reserved + )); + + return concat(fileHeader, sectionHeader, sectionData); + } + + test("sections[0].sectionType === SectionType.ElementsAlways", () => { + const pixels = new Uint8Array([1, 0, 0, 1]); + const buf = makeImage2DFile(2, 2, pixels); + const result = parser.parse(buf); + expect(result.sections.length).toBe(1); + expect(result.sections[0]!.sectionType).toBe(SectionType.ElementsAlways); + }); + + test("elements[0].type === ElementType.Image2D", () => { + const pixels = new Uint8Array([1, 0, 0, 1]); + const buf = makeImage2DFile(2, 2, pixels); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + expect(section.elements.length).toBe(1); + expect(section.elements[0]!.type).toBe(ElementType.Image2D); + }); + + test("correct dimensions after parse", () => { + const pixels = new Uint8Array([0, 1, 1, 0]); + const buf = makeImage2DFile(2, 2, pixels); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + const el = section.elements[0] as MonoFormatImage2D; + expect(el.image.width).toBe(2); + expect(el.image.height).toBe(2); + }); + + test("correct pixel data after round-trip", () => { + // pixels: [1, 0, 0, 1] → top-left and bottom-right are on + const pixels = new Uint8Array([1, 0, 0, 1]); + const buf = makeImage2DFile(2, 2, pixels); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + const el = section.elements[0] as MonoFormatImage2D; + expect(Array.from(el.image.pixels)).toEqual([1, 0, 0, 1]); + }); + + test("xOffset and yOffset are preserved", () => { + const pixels = new Uint8Array([1, 1, 1, 1]); + const buf = makeImage2DFile(2, 2, pixels, 5, 3); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + const el = section.elements[0] as MonoFormatImage2D; + expect(el.xOffset).toBe(5); + expect(el.yOffset).toBe(3); }); }); // --------------------------------------------------------------------------- -// MonoDisplayParser — unknown section skip +// MonoDisplayFile // --------------------------------------------------------------------------- -describe("MonoDisplayParser — section skip", () => { +describe("MonoDisplayFile", () => { const parser = new MonoDisplayParser(); - test("skips unknown section, parses second known section", () => { - // File with 2 sections: 0xFF (unknown) then 0x01 (static 2×2) - const staticData = new Uint8Array(concat(u16le(2), u16le(2), new Uint8Array([0,1,1,0]))); - const unknownData = new Uint8Array(4).fill(0xAA); - - const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(2), u16le(0)); - const sec1 = concat(u8(0xFF), u24le(unknownData.byteLength), unknownData); - const sec2 = concat(u8(0x01), u24le(staticData.byteLength), staticData); - const buf = concat(fileHeader, sec1, sec2); - - const result = parser.parse(buf) as StaticFrame; - expect(result.type).toBe("static"); - expect(result.pixels[1]).toBe(1); + test("toBuffer() produces parseable output for Image2D element", () => { + const pixels = new Uint8Array([1, 0, 1, 0]); + const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 }; + const file = new MonoDisplayFile({ elements_always: [el] }); + const buf = file.toBuffer(); + const result = parser.parse(buf); + expect(result.sections.length).toBeGreaterThan(0); + const section = result.sections[0] as MonoFormatElementsAlways; + expect(section.sectionType).toBe(SectionType.ElementsAlways); + expect(section.elements.length).toBe(1); + const parsed = section.elements[0] as MonoFormatImage2D; + expect(parsed.type).toBe(ElementType.Image2D); + expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 1, 0]); + }); + + test("xOffset/yOffset are round-tripped", () => { + const pixels = new Uint8Array([1, 1, 1, 1]); + const el: Image2DElement = { + type: ElementType.Image2D, + pixels, + width: 2, + height: 2, + xOffset: 10, + yOffset: 7, + }; + const file = new MonoDisplayFile({ elements_always: [el] }); + const buf = file.toBuffer(); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + const parsed = section.elements[0] as MonoFormatImage2D; + expect(parsed.xOffset).toBe(10); + expect(parsed.yOffset).toBe(7); + }); + + test("flags.drawFront default is applied", () => { + const pixels = new Uint8Array([0]); + const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 1, height: 1 }; + const file = new MonoDisplayFile({ elements_always: [el] }); + const buf = file.toBuffer(); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + // Default flags should have drawFront = true (flags byte bit 0 = 1) + expect(section.flags.drawFront).toBe(true); + }); + + test("empty elements_always array is valid and produces parseable output", () => { + const file = new MonoDisplayFile({ elements_always: [] }); + const buf = file.toBuffer(); + const result = parser.parse(buf); + expect(result.sections.length).toBeGreaterThan(0); + const section = result.sections[0] as MonoFormatElementsAlways; + expect(section.sectionType).toBe(SectionType.ElementsAlways); + expect(section.elements.length).toBe(0); }); }); // --------------------------------------------------------------------------- -// buildBinBuffer round-trip +// buildBinBuffer // --------------------------------------------------------------------------- describe("buildBinBuffer", () => { const parser = new MonoDisplayParser(); - test("static round-trip", () => { + test("builds a parseable Uint8Array from Image2DElement", () => { const pixels = new Uint8Array([0, 1, 1, 0]); - const original: StaticFrame = { type: "static", width: 2, height: 2, pixels }; - const buf = buildBinBuffer(original); - const parsed = parser.parse(buf) as StaticFrame; - expect(parsed.type).toBe("static"); - expect(parsed.width).toBe(2); - expect(parsed.height).toBe(2); - expect(Array.from(parsed.pixels)).toEqual([0, 1, 1, 0]); - }); - - test("non-static throws (not implemented yet)", () => { - const anim: Animation = { type: "animation", width: 1, height: 1, frames: [] }; - expect(() => buildBinBuffer(anim)).toThrow(/not yet implemented/); - }); -}); - -// --------------------------------------------------------------------------- -// MonoDisplayFile — JSON descriptor builder -// --------------------------------------------------------------------------- - -describe("MonoDisplayFile", () => { - const parser = new MonoDisplayParser(); - - test("single Image2D fills full display", () => { - const pixels = new Uint8Array(4 * 2).fill(1); // 4×2 all-on - const file = new MonoDisplayFile({ - width: 4, height: 2, - elements_always: [{ type: ElementType.Image2D, pixels, width: 4, height: 2 }], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - expect(result.type).toBe("static"); - expect(result.width).toBe(4); - expect(result.height).toBe(2); - expect(result.pixels.every(p => p === 1)).toBe(true); - }); - - test("xoffset / yoffset positions element", () => { - // 6×2 display, 2×2 all-on block placed at xoffset=2 - const block = new Uint8Array(4).fill(1); - const file = new MonoDisplayFile({ - width: 6, height: 2, - elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 2 }], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - // cols 0,1 off — cols 2,3 on — cols 4,5 off - expect(result.pixels[0]).toBe(0); // (0,0) off - expect(result.pixels[2]).toBe(1); // (2,0) on - expect(result.pixels[3]).toBe(1); // (3,0) on - expect(result.pixels[4]).toBe(0); // (4,0) off - }); - - test("two overlapping elements OR-blend", () => { - // 4×1 display: element A sets pixels [0,1,0,0], element B sets [0,0,0,1] - const a = new Uint8Array([0, 1, 0, 0]); - const b = new Uint8Array([0, 0, 0, 1]); - const file = new MonoDisplayFile({ - width: 4, height: 1, - elements_always: [ - { type: ElementType.Image2D, pixels: a, width: 4, height: 1 }, - { type: ElementType.Image2D, pixels: b, width: 4, height: 1 }, - ], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - expect(Array.from(result.pixels)).toEqual([0, 1, 0, 1]); - }); - - test("element clipped at right/bottom edge", () => { - // 4×2 display, 2×2 block placed at xoffset=3 — only 1 col fits - const block = new Uint8Array(4).fill(1); - const file = new MonoDisplayFile({ - width: 4, height: 2, - elements_always: [{ type: ElementType.Image2D, pixels: block, width: 2, height: 2, xoffset: 3 }], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - // col 3 on, col 4+ clipped - expect(result.pixels[3]).toBe(1); // (3,0) on - expect(result.width).toBe(4); // canvas didn't grow - }); - - test("size inferred from element bounding box when omitted", () => { - const pixels = new Uint8Array(3 * 2).fill(1); - const file = new MonoDisplayFile({ - elements_always: [{ type: ElementType.Image2D, pixels, width: 3, height: 2, xoffset: 1, yoffset: 1 }], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - expect(result.width).toBe(4); // 1 + 3 - expect(result.height).toBe(3); // 1 + 2 - }); - - test("empty descriptor produces 160×80 blank static frame", () => { - const file = new MonoDisplayFile({}); - const result = parser.parse(file.toBuffer()) as StaticFrame; - expect(result.width).toBe(160); - expect(result.height).toBe(80); - expect(result.pixels.every(p => p === 0)).toBe(true); - }); - - test("toBuffer round-trips through parser", () => { - const pixels = new Uint8Array(120 * 80); - for (let i = 0; i < pixels.length; i++) pixels[i] = i % 2; - const file = new MonoDisplayFile({ - width: 160, height: 80, - elements_always: [{ type: ElementType.Image2D, pixels, width: 120, height: 80, xoffset: 20 }], - }); - const result = parser.parse(file.toBuffer()) as StaticFrame; - expect(result.width).toBe(160); - expect(result.pixels[20]).toBe(0); // first pixel of element (i=0 → 0) - expect(result.pixels[21]).toBe(1); // second pixel (i=1 → 1) + const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 }; + const buf = buildBinBuffer(el); + expect(buf).toBeInstanceOf(Uint8Array); + const result = parser.parse(buf); + expect(result.sections.length).toBeGreaterThan(0); + const section = result.sections[0] as MonoFormatElementsAlways; + const parsed = section.elements[0] as MonoFormatImage2D; + expect(Array.from(parsed.image.pixels)).toEqual([0, 1, 1, 0]); + }); + + test("pixel data round-trips correctly", () => { + const pixels = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 1]); // 8 pixels, corners on + const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 8, height: 1 }; + const buf = buildBinBuffer(el); + const result = parser.parse(buf); + const section = result.sections[0] as MonoFormatElementsAlways; + const parsed = section.elements[0] as MonoFormatImage2D; + expect(parsed.image.width).toBe(8); + expect(parsed.image.height).toBe(1); + expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 0, 0, 0, 0, 0, 1]); }); }); diff --git a/ts/tsconfig.json b/ts/tsconfig.json index bfa0fea..be3d138 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",