// --------------------------------------------------------------------------- // MonoDisplayFile - JSON descriptor → binary .bin builder // --------------------------------------------------------------------------- import { packedSize, packPixels, pad32 } from "./helper"; import { MONOFORMAT_MAGIC_HEADER } from "./parser"; import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCurrentTime, type MonoFormatCustomFont, type MonoFormatElement, type MonoFormatElementsAlways, type MonoFormatElementsTimespan, type MonoFormatFile, type MonoFormatHScroll, type MonoFormatHScrollText, type MonoFormatImage2D, type MonoFormatLine, type MonoFormatScrollElementFlags, type MonoFormatSection, type MonoFormatVScroll, } from "./types"; /** * Encodes a MonoDisplayFileDescriptor to a valid .bin ArrayBuffer. * * elements_always -> Section type 1 (ElementsAlways). * Pass a DrawElement[] or { flags, elements }. * Default flags: drawFront=true, drawBack=false, clearBuffer=false. * * PLAN: encode ElementsTimespan sections, CustomFont sections. */ export class MonoDisplayFile implements MonoFormatFile { constructor(public sections: MonoFormatSection[], public version: number = 1) { } toBuffer(): Uint8Array { const sections: Uint8Array[] = []; for (const section of this.sections) { switch (section.sectionType) { case SectionType.ElementsAlways: { sections.push(this.#encodeElementsAlwaysSection(section)); break; } case SectionType.ElementsTimespan: { sections.push(this.#encodeElementsTimespanSection(section)); break; } case SectionType.CustomFont: { sections.push(this.#encodeCustomFont(section)); break; } } } // File header (12 bytes) const hdrBuf = new ArrayBuffer(12); const hdrView = new DataView(hdrBuf); hdrView.setUint32(0, MONOFORMAT_MAGIC_HEADER, true); hdrView.setUint32(4, this.version, true); // version hdrView.setUint16(8, sections.length, true); hdrView.setUint16(10, 0, true); // reserved return this.#concat(new Uint8Array(hdrBuf), ...sections); } // --- section encoders --- #encodeElementsAlwaysSection( section: MonoFormatElementsAlways, ): Uint8Array { const flagBits = (section.flags.drawFront ?? true ? 0x01 : 0) | (section.flags.drawBack ?? false ? 0x02 : 0) | (section.flags.clearBuffer ?? false ? 0x04 : 0); const encodedEls = section.elements.map(el => this.#encodeElement(el)); const sectionDataSize = 4 + encodedEls.reduce((s, e) => s + e.byteLength, 0); // sectionSize (in header) = 4 (header) + sectionDataSize const sectionSize = 4 + sectionDataSize; const hdr = new Uint8Array(4 + 4); // section header (4) + section sub-header (4) const v = new DataView(hdr.buffer); v.setUint8(0, section.sectionType); v.setUint8(1, sectionSize & 0xFF); v.setUint8(2, (sectionSize >> 8) & 0xFF); v.setUint8(3, (sectionSize >> 16) & 0xFF); v.setUint16(4, flagBits, true); // flags v.setUint16(6, section.elements.length, true); // numElements return this.#concat(hdr, ...encodedEls); } #encodeElementsTimespanSection( section: MonoFormatElementsTimespan, ): Uint8Array { const flagBits = (section.flags.drawFront ?? true ? 0x01 : 0) | (section.flags.drawBack ?? false ? 0x02 : 0) | (section.flags.clearBuffer ?? false ? 0x04 : 0); const encodedEls = section.elements.map(el => this.#encodeElement(el)); // sub-header: flags(2) + numElements(2) + startTimestamp(8) + endTimestamp(8) = 20 bytes const sectionDataSize = 20 + encodedEls.reduce((s, e) => s + e.byteLength, 0); // sectionSize (in header) = 4 (header) + sectionDataSize const sectionSize = 4 + sectionDataSize; const hdr = new Uint8Array(4 + 20); // section header (4) + sub-header (20) const v = new DataView(hdr.buffer); v.setUint8(0, section.sectionType); v.setUint8(1, sectionSize & 0xFF); v.setUint8(2, (sectionSize >> 8) & 0xFF); v.setUint8(3, (sectionSize >> 16) & 0xFF); v.setUint16(4, flagBits, true); // flags v.setUint16(6, section.elements.length, true); // numElements const start = section.startTimestamp ?? 0n; const end = section.endTimestamp ?? 0n; v.setUint32( 8, Number(start & 0xFFFFFFFFn), true); v.setUint32(12, Number((start >> 32n) & 0xFFFFFFFFn), true); v.setUint32(16, Number(end & 0xFFFFFFFFn), true); v.setUint32(20, Number((end >> 32n) & 0xFFFFFFFFn), true); return this.#concat(hdr, ...encodedEls); } #encodeCustomFont( section: MonoFormatCustomFont, ): Uint8Array { const sectionSize = 4 + section.fontData.length; const hdr = new Uint8Array(4 + 4); // section header (4) + section sub-header (4) const v = new DataView(hdr.buffer); v.setUint8(0, section.sectionType); v.setUint8(1, sectionSize & 0xFF); v.setUint8(2, (sectionSize >> 8) & 0xFF); v.setUint8(3, (sectionSize >> 16) & 0xFF); v.setUint8(4, section.fontData.length & 0xFF); v.setUint8(5, (section.fontData.length >> 8) & 0xFF); v.setUint8(6, (section.fontData.length >> 16) & 0xFF); v.setUint8(7, 0); return this.#concat(hdr, section.fontData); } // --- element encoders --- #encodeElement(el: MonoFormatElement): Uint8Array { switch (el.type) { case ElementType.Image2D: return this.#encodeImage2D(el); case ElementType.Animation: return this.#encodeAnimation(el); case ElementType.HorizontalScroll: return this.#encodeHScroll(el); case ElementType.VerticalScroll: return this.#encodeVScroll(el); case ElementType.Line: return this.#encodeLine(el); case ElementType.ClippedText: return this.#encodeClippedText(el); case ElementType.HScrollText: return this.#encodeHScrollText(el); case ElementType.CurrentTime: return this.#encodeCurrentTime(el); default: return new Uint8Array(); } } #encodeImage2D(el: MonoFormatImage2D): Uint8Array { const packed = packPixels(el.image.pixels, el.image.width, el.image.height); const fixed = 12; // bytes before pixel data const rawSize = fixed + packed.byteLength; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.Image2D, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.image.width, true); v.setUint16(8, el.image.height, true); v.setUint16(10, 0, true); // reserved out.set(packed, 12); return out; } #encodeAnimation(el: MonoFormatAnimation): Uint8Array { const frameBytes = packedSize(el.width, el.height); const fixed = 16; const rawSize = fixed + el.frames.length * frameBytes; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.Animation, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.width, true); v.setUint16(8, el.height, true); v.setUint16(10, el.frames.length, true); v.setUint16(12, el.updateInterval ?? 0, true); v.setUint16(14, 0, true); // reserved let off = 16; for (const f of el.frames) { out.set(packPixels(f.pixels, el.width, el.height), off); off += frameBytes; } return out; } #encodeScrollFlags(f: MonoFormatScrollElementFlags | undefined): number { return ( (f?.endless ? 0x01 : 0) | (f?.invertDirection ? 0x02 : 0) | (f?.padStart ? 0x04 : 0) | (f?.padEnd ? 0x08 : 0) ); } #encodeHScroll(el: MonoFormatHScroll): Uint8Array { const packed = packPixels(el.content.pixels, el.contentWidth, el.height); const fixed = 16; const rawSize = fixed + packed.byteLength; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.HorizontalScroll, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.width, true); v.setUint16(8, el.height, true); v.setUint16(10, el.contentWidth, true); v.setUint8( 12, this.#encodeScrollFlags(el.flags)); v.setUint8( 13, el.scrollSpeed ?? 0); v.setUint16(14, 0, true); // reserved out.set(packed, 16); return out; } #encodeVScroll(el: MonoFormatVScroll): Uint8Array { const packed = packPixels(el.content.pixels, el.width, el.contentHeight); const fixed = 16; const rawSize = fixed + packed.byteLength; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.VerticalScroll, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.width, true); v.setUint16(8, el.height, true); v.setUint16(10, el.contentHeight, true); v.setUint8( 12, this.#encodeScrollFlags(el.flags)); v.setUint8( 13, el.scrollSpeed ?? 0); v.setUint16(14, 0, true); out.set(packed, 16); return out; } #encodeLine(el: MonoFormatLine): Uint8Array { // 12 bytes, already 32-bit aligned const out = new Uint8Array(12); const v = new DataView(out.buffer); v.setUint16(0, ElementType.Line, true); v.setUint16(2, el.xOrigin, true); v.setUint16(4, el.yOrigin, true); v.setUint16(6, el.xTarget, true); v.setUint16(8, el.yTarget, true); v.setUint8(10, el.lineStyle ?? 0); v.setUint8(11, el.invertPixels ? 1 : 0); return out; } #encodeClippedText(el: MonoFormatClippedText): Uint8Array { const enc = new TextEncoder().encode(el.text); const fixed = 14; // 2+2+2+2+2+2+2 const rawSize = fixed + enc.byteLength; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.ClippedText, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.width, true); v.setUint16(8, el.height, true); v.setUint16(10, el.fontIndex ?? 0, true); v.setUint16(12, enc.byteLength, true); out.set(enc, 14); return out; } #encodeHScrollText(el: MonoFormatHScrollText): Uint8Array { const enc = new TextEncoder().encode(el.text); const fixed = 16; // 2+2+2+2+2+1+1+2+2 const rawSize = fixed + enc.byteLength; const total = rawSize + pad32(rawSize); const out = new Uint8Array(total); const v = new DataView(out.buffer); v.setUint16(0, ElementType.HScrollText, true); v.setUint16(2, el.xOffset ?? 0, true); v.setUint16(4, el.yOffset ?? 0, true); v.setUint16(6, el.width, true); v.setUint16(8, el.height, true); v.setUint8( 10, this.#encodeScrollFlags(el.flags)); v.setUint8( 11, el.scrollSpeed ?? 0); v.setUint16(12, el.fontIndex ?? 0, true); v.setUint16(14, enc.byteLength, true); // PLAN: confirm with spec out.set(enc, 16); return out; } #encodeCurrentTime(el: MonoFormatCurrentTime): Uint8Array { // Fixed 16 bytes, already 32-bit aligned const out = new Uint8Array(16); const v = new DataView(out.buffer); const f = el.flags; const flagsByte = (f?.clock12h ? 0x01 : 0) | (f?.showHours ?? true ? 0x02 : 0) | (f?.showMinutes ?? true ? 0x04 : 0) | (f?.showSeconds ? 0x08 : 0); v.setUint16( 0, ElementType.CurrentTime, true); v.setUint16( 2, el.xOffset ?? 0, true); v.setUint16( 4, el.yOffset ?? 0, true); v.setUint16( 6, el.width, true); v.setUint16( 8, el.height, true); v.setUint16(10, el.fontIndex ?? 0, true); v.setInt16( 12, el.utcOffsetMinutes ?? 0, true); v.setUint8( 14, flagsByte); v.setUint8( 15, 0); // reserved return out; } // --- utilities --- #concat(...parts: Uint8Array[]): Uint8Array { const total = parts.reduce((s, p) => s + p.byteLength, 0); const out = new Uint8Array(total); let off = 0; for (const p of parts) { out.set(p, off); off += p.byteLength; } return out; } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- export async function loadBinFile(url: string): Promise { const res = await fetch(url); if (!res.ok) throw new Error(`loadBinFile: HTTP ${res.status} for ${url}`); return res.arrayBuffer(); } /** * Build a single-element-always .bin buffer containing one Image2D element. * Convenience for tests; for production use MonoDisplayFile directly. */ export function buildBinBuffer(el: MonoFormatImage2D): Uint8Array { return new MonoDisplayFile([{ sectionType: SectionType.ElementsAlways, elements: [el], flags: { drawFront: true, drawBack: true, clearBuffer: true, }, }]).toBuffer(); }