Abfahrtsanzeiger Display Basic Library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
libmonoformat/ts/src/parser.ts

282 lines
11 KiB

// ---------------------------------------------------------------------------
// 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,
};
}
}