// --------------------------------------------------------------------------- // File format constants // --------------------------------------------------------------------------- import { packedSize, packPixels, pad32, unpackPixels, BinaryReader } from "./helper"; import { type MonoFormatFile } from "./types"; import { ElementType, SectionType, type MonoFormatAnimation, type MonoFormatClippedText, type MonoFormatCurrentTime, 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 export const MONOFORMAT_MAGIC_HEADER = 0x632B7EAF; // --------------------------------------------------------------------------- // MonoDisplayParser // --------------------------------------------------------------------------- export class MonoDisplayParser { parse(buffer: ArrayBuffer | ArrayBufferView): MonoFormatFile { const r = new BinaryReader(buffer); // File header (12 bytes) const magic = r.readUint32(); if (magic !== MONOFORMAT_MAGIC_HEADER) throw new Error(`Bad magic 0x${magic.toString(16)} - expected 0x632B7EAF`); const version = r.readUint32(); if (version === 0 || version > 1) throw new Error(`Unsupported version ${version}`); const numSections = r.readUint16(); r.readUint16(); // reserved const sections: MonoFormatSection[] = []; for (let i = 0; i < numSections; i++) { const sectionType = r.readByte(); const sectionSize = r.readUint24(); // INCLUDES 4-byte header const sectionStart = r.offset; // start of section DATA const dataSize = sectionSize - 4; const sectionEnd = sectionStart + dataSize; switch (sectionType) { case SectionType.ElementsAlways: sections.push(this.#parseElementsSection(r, SectionType.ElementsAlways, false, sectionEnd)); break; case SectionType.ElementsTimespan: sections.push(this.#parseElementsSection(r, SectionType.ElementsTimespan, true, sectionEnd)); break; case SectionType.CustomFont: sections.push(this.#parseCustomFont(r, dataSize)); break; default: // Unknown - skip break; } // Seek past full section data (handles padding, unknown fields, future extensions) r.seek(sectionStart + dataSize); } return { sections }; } #parseFlags(raw: number): MonoFormatSectionFlags { return { drawFront: !!(raw & 0x01), drawBack: !!(raw & 0x02), clearBuffer: !!(raw & 0x04), }; } #parseElementsSection( r: BinaryReader, type: SectionType.ElementsAlways | SectionType.ElementsTimespan, hasTimestamp: boolean, sectionEnd: number, ): MonoFormatElementsAlways | MonoFormatElementsTimespan { const flags = this.#parseFlags(r.readUint16()); const numElements = r.readUint16(); let startTimestamp = 0n, endTimestamp = 0n; if (hasTimestamp) { startTimestamp = r.readUint64(); endTimestamp = r.readUint64(); } const elements: MonoFormatElement[] = []; for (let i = 0; i < numElements; i++) { // Stop if fewer than 2 bytes remain in section (can't read element type) if (r.offset + 2 > sectionEnd) break; const el = this.#parseElement(r); if (el === null) break; // unknown type - can't determine size, stop elements.push(el); } if (hasTimestamp) { return { sectionType: SectionType.ElementsTimespan, flags, startTimestamp, endTimestamp, elements }; } return { sectionType: SectionType.ElementsAlways, flags, elements }; } #parseCustomFont(r: BinaryReader, dataSize: number): MonoFormatCustomFont { const actualSize = r.readUint24(); r.readByte(); // reserved const fontData = new Uint8Array(r.readBytes(Math.min(actualSize, dataSize - 4))); return { sectionType: SectionType.CustomFont, fontData }; } #parseElement(r: BinaryReader): MonoFormatElement | null { const elementStart = r.offset; const type = r.readUint16(); switch (type) { case ElementType.Image2D: return this.#parseImage2D(r, elementStart); case ElementType.Animation: return this.#parseAnimation(r, elementStart); case ElementType.HorizontalScroll: return this.#parseHScroll(r, elementStart); case ElementType.VerticalScroll: return this.#parseVScroll(r, elementStart); case ElementType.Line: return this.#parseLine(r); case ElementType.ClippedText: return this.#parseClippedText(r, elementStart); case ElementType.HScrollText: return this.#parseHScrollText(r, elementStart); case ElementType.CurrentTime: return this.#parseCurrentTime(r); default: // Unknown element type - cannot safely skip without knowing size; stop parsing section elements return null; } } #alignTo4(start: number, current: number): void { // elements are 32-bit aligned; seek past padding if any const end = start + (((current - start) + 3) & ~3); // no-op if already aligned (handled by caller via section seek) } #parseImage2D(r: BinaryReader, start: number): MonoFormatImage2D { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); r.readUint16(); // reserved // Fixed header: 2(type)+2+2+2+2+2 = 12 bytes const packed = r.readBytes(packedSize(width, height)); const pixels = unpackPixels(new Uint8Array(packed), width, height); // Skip padding to 32-bit boundary for the element const dataRead = 12 + packed.byteLength; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.Image2D, xOffset, yOffset, image: { pixels, width, height } }; } #parseAnimation(r: BinaryReader, start: number): MonoFormatAnimation { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const numFrames = r.readUint16(); const updateInterval = r.readUint16(); r.readUint16(); // reserved // Fixed header: 2+2+2+2+2+2+2+2 = 16 bytes const frameBytes = packedSize(width, height); const frames: MonoFormatPixelImage[] = []; for (let f = 0; f < numFrames; f++) { const packed = r.readBytes(frameBytes); frames.push({ pixels: unpackPixels(new Uint8Array(packed), width, height), width, height }); } const dataRead = 16 + numFrames * frameBytes; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.Animation, xOffset, yOffset, width, height, updateInterval, frames }; } #parseScrollFlags(raw: number) { return { endless: !!(raw & 0x01), invertDirection: !!(raw & 0x02), padStart: !!(raw & 0x04), padEnd: !!(raw & 0x08), }; } #parseHScroll(r: BinaryReader, start: number): MonoFormatHScroll { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const contentWidth = r.readUint16(); const flagsByte = r.readByte(); const scrollSpeed = r.readByte(); r.readUint16(); // reserved // Fixed header: 2+2+2+2+2+2+1+1+2 = 16 bytes const packed = r.readBytes(packedSize(contentWidth, height)); const pixels = unpackPixels(new Uint8Array(packed), contentWidth, height); const dataRead = 16 + packed.byteLength; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.HorizontalScroll, xOffset, yOffset, width, height, contentWidth, scrollSpeed, flags: this.#parseScrollFlags(flagsByte), content: { pixels, width: contentWidth, height }, }; } #parseVScroll(r: BinaryReader, start: number): MonoFormatVScroll { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const contentHeight = r.readUint16(); const flagsByte = r.readByte(); const scrollSpeed = r.readByte(); r.readUint16(); // reserved const packed = r.readBytes(packedSize(width, contentHeight)); const pixels = unpackPixels(new Uint8Array(packed), width, contentHeight); const dataRead = 16 + packed.byteLength; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.VerticalScroll, xOffset, yOffset, width, height, contentHeight, scrollSpeed, flags: this.#parseScrollFlags(flagsByte), content: { pixels, width, height: contentHeight }, }; } #parseLine(r: BinaryReader): MonoFormatLine { const xOrigin = r.readUint16(); const yOrigin = r.readUint16(); const xTarget = r.readUint16(); const yTarget = r.readUint16(); const lineStyle = r.readByte(); const flagsByte = r.readByte(); // 2+2+2+2+2+1+1 = 12 bytes, already 32-bit aligned return { type: ElementType.Line, xOrigin, yOrigin, xTarget, yTarget, lineStyle, invertPixels: !!(flagsByte & 1) }; } #parseClippedText(r: BinaryReader, start: number): MonoFormatClippedText { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const fontIndex = r.readUint16(); const textLen = r.readUint16(); // Fixed header: 2+2+2+2+2+2+2 = 14 bytes const text = r.readUtf8(textLen); const dataRead = 14 + textLen; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.ClippedText, xOffset, yOffset, width, height, fontIndex, text }; } #parseCurrentTime(r: BinaryReader): MonoFormatCurrentTime { // Layout after type field (already consumed): 14 bytes, total element = 16 bytes (aligned) const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const fontIndex = r.readUint16(); const utcOffsetMinutes = r.readInt16(); const flagsByte = r.readByte(); r.readByte(); // reserved return { type: ElementType.CurrentTime, xOffset, yOffset, width, height, fontIndex, utcOffsetMinutes, flags: { clock12h: !!(flagsByte & 0x01), showHours: !!(flagsByte & 0x02), showMinutes: !!(flagsByte & 0x04), showSeconds: !!(flagsByte & 0x08), }, }; } #parseHScrollText(r: BinaryReader, start: number): MonoFormatHScrollText { const xOffset = r.readUint16(); const yOffset = r.readUint16(); const width = r.readUint16(); const height = r.readUint16(); const flagsByte = r.readByte(); const scrollSpeed = r.readByte(); const fontIndex = r.readUint16(); const textLen = r.readUint16(); // PLAN: confirm with spec (assumed, not explicit in diagram) // Fixed header: 2+2+2+2+2+1+1+2+2 = 16 bytes const text = r.readUtf8(textLen); const dataRead = 16 + textLen; r.seek(start + dataRead + pad32(dataRead)); return { type: ElementType.HScrollText, xOffset, yOffset, width, height, scrollSpeed, fontIndex, flags: this.#parseScrollFlags(flagsByte), text, }; } }