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/file.ts

368 lines
14 KiB

// ---------------------------------------------------------------------------
// 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<ArrayBufferLike> {
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<ArrayBuffer> {
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();
}