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/font/u8g2.ts

294 lines
10 KiB

// ---------------------------------------------------------------------------
// U8G2 font binary format decoder
// Derived from: https://github.com/olikraus/u8g2/blob/master/csrc/u8g2_font.c
// https://github.com/olikraus/u8g2/blob/master/tools/font/bdfconv/bdf_rle.c
// ---------------------------------------------------------------------------
const HDR_SIZE = 23;
// ---------------------------------------------------------------------------
// BitReader - LSB-first, matching u8g2_font_decode_get_unsigned_bits exactly:
// val = *ptr >> bit_pos
// if (bit_pos + cnt >= 8): val |= *(ptr+1) << (8 - bit_pos); ptr++
// val &= (1 << cnt) - 1
// ---------------------------------------------------------------------------
class BitReader {
private ptr: number;
private bitPos: number;
constructor(private data: Uint8Array, startByte: number) {
this.ptr = startByte;
this.bitPos = 0;
}
read(cnt: number): number {
let val = (this.data[this.ptr]! >> this.bitPos) & 0xFF;
const newPos = this.bitPos + cnt;
if (newPos >= 8) {
this.ptr++;
val |= (this.data[this.ptr]! << (8 - this.bitPos));
this.bitPos = newPos - 8;
} else {
this.bitPos = newPos;
}
return val & ((1 << cnt) - 1);
}
// Offset-binary signed: stored as unsigned + (1 << (cnt-1)), so decode = val - (1 << (cnt-1))
// Matches: v = get_unsigned(cnt); d = 1 << (cnt-1); v -= d;
readSigned(cnt: number): number {
return this.read(cnt) - (1 << (cnt - 1));
}
}
function readU16BE(data: Uint8Array, off: number): number {
return ((data[off]! << 8) | data[off + 1]!) >>> 0;
}
// ---------------------------------------------------------------------------
// Font header (23 bytes)
// Offsets 17/18, 19/20, 21/22 are relative to end of header (byte 23)
// ---------------------------------------------------------------------------
function parseHeader(font: Uint8Array) {
return {
m0: font[2]!,
m1: font[3]!,
bitsPerW: font[4]!,
bitsPerH: font[5]!,
bitsPerX: font[6]!,
bitsPerY: font[7]!,
bitsPerDx: font[8]!,
ascentA: font[13]! as number,
// offsets stored as BE uint16, relative to end of 23-byte header
startUpper: HDR_SIZE + readU16BE(font, 17),
startLower: HDR_SIZE + readU16BE(font, 19),
startUnicode: HDR_SIZE + readU16BE(font, 21),
};
}
// ---------------------------------------------------------------------------
// RLE decoder - matches fd_decode / u8g2_font_decode_glyph in the C source.
//
// The bitstream is a sequence of (zeros-run, ones-run) pairs, repeated.
// Each pair:
// [m0 bits] count of zero pixels
// [m1 bits] count of one pixels
// [unary] repetition: read 1-bits until a 0-stop; reps = (number of 1s) + 1
//
// This matches bg_rle_compress which encodes pairs and then a unary repeat.
// ---------------------------------------------------------------------------
function decodeRLE(
br: BitReader,
w: number,
h: number,
m0: number,
m1: number,
): Uint8Array {
const total = w * h;
const bits = new Uint8Array(total);
let filled = 0;
while (filled < total) {
const zeros = br.read(m0);
const ones = br.read(m1);
let reps = 1;
while (br.read(1) === 1) reps++;
for (let r = 0; r < reps && filled < total; r++) {
for (let p = 0; p < zeros && filled < total; p++) bits[filled++] = 0;
for (let p = 0; p < ones && filled < total; p++) bits[filled++] = 1;
}
}
return bits;
}
// ---------------------------------------------------------------------------
// Glyph result
// ---------------------------------------------------------------------------
export interface U8G2GlyphData {
cols: Uint8Array | Uint32Array; // column words (bit 0 = top row), length = w
w: number;
h: number;
dx: number; // horizontal advance
yOff: number; // downward offset from top of cell to top of glyph
}
// ---------------------------------------------------------------------------
// Decode the glyph bitstream at byteOffset (after the record header bytes).
// Field order from C source (u8g2_font.c lines 590-625):
// glyph_width, glyph_height, x (signed), y (signed), delta_x (signed)
// ---------------------------------------------------------------------------
function decodeGlyphAt(
font: Uint8Array,
byteOffset: number,
hdr: ReturnType<typeof parseHeader>,
): U8G2GlyphData {
const br = new BitReader(font, byteOffset);
const w = br.read(hdr.bitsPerW);
const h = br.read(hdr.bitsPerH);
/* x */ br.readSigned(hdr.bitsPerX); // x bearing - consumed but not needed
const yB = br.readSigned(hdr.bitsPerY);
const dx = br.readSigned(hdr.bitsPerDx);
if (w === 0 || h === 0) {
// Blank glyph (e.g. space) - advance is stored in dx
return { cols: new Uint8Array(1), w: 1, h: 0, dx: Math.max(dx, 1), yOff: 0 };
}
// Decode row-major horizontal bitmap
const bits = decodeRLE(br, w, h, hdr.m0, hdr.m1);
// Convert row-major → column words (bit 0 = top row) for rasterizeText
// Must be Uint32Array - glyphs can be up to 32px tall; Uint8Array truncates rows 8+
const cols = new Uint32Array(w);
for (let row = 0; row < h; row++) {
for (let col = 0; col < w; col++) {
if (bits[row * w + col]) cols[col] |= (1 << row);
}
}
// yB: signed distance from baseline upward to bottom of glyph bounding box.
// ascentA: pixels above baseline for cap-height reference.
// yOff: distance downward from top of the notional box to top of this glyph.
const yOff = hdr.ascentA - yB - h;
return { cols, w, h, dx, yOff };
}
// ---------------------------------------------------------------------------
// Glyph search
//
// Record layout (cp < 0x0100):
// [1 byte] encoding
// [1 byte] jump = byte distance from START of this record to next record
// [bitstream ...]
//
// Record layout (cp >= 0x0100):
// [2 bytes BE] encoding
// [1 byte] jump (same semantics)
// [bitstream ...]
//
// Unicode section is preceded by a lookup table (added in v2.23):
// pairs of [uint16 BE delta | uint16 BE last-encoding], terminated by last==0xFFFF
// delta is cumulative byte offset from TABLE START to the glyph sub-block.
// ---------------------------------------------------------------------------
export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null {
if (font.length < HDR_SIZE) return null;
const hdr = parseHeader(font);
try {
if (cp >= 0x0100) {
// -- Unicode block ----------------------------------------------------
const tableBase = hdr.startUnicode;
if (tableBase + 4 > font.length) return null;
// Walk the jump table (BE uint16 pairs) to find the right sub-block.
// delta is offset from tableBase to start of sub-block.
let glyphStart = tableBase;
let pos = tableBase;
while (pos + 4 <= font.length) {
const delta = readU16BE(font, pos);
const lastEncoding = readU16BE(font, pos + 2);
pos += 4;
if (lastEncoding === 0xFFFF) {
glyphStart = tableBase + delta;
break;
}
if (lastEncoding >= cp) {
glyphStart = tableBase + delta;
break;
}
}
// Scan glyph records in the sub-block
let recPos = glyphStart;
while (recPos + 3 <= font.length) {
const recStart = recPos;
const encoding = readU16BE(font, recPos);
const jump = font[recPos + 2]!;
if (jump === 0) break;
recPos += 3; // bitstream starts here
if (encoding === cp) return decodeGlyphAt(font, recPos, hdr);
recPos = recStart + jump;
}
return null;
} else {
// -- ASCII block (cp < 0x0100) ----------------------------------------
// Jump-start from the nearest block offset to avoid scanning from byte 0.
let recPos: number;
if (cp >= 0x61) recPos = hdr.startLower;
else if (cp >= 0x41) recPos = hdr.startUpper;
else recPos = HDR_SIZE;
while (recPos + 2 <= font.length) {
const recStart = recPos;
const encoding = font[recPos]!;
const jump = font[recPos + 1]!;
if (jump === 0) break;
recPos += 2; // bitstream starts here
if (encoding === cp) return decodeGlyphAt(font, recPos, hdr);
recPos = recStart + jump;
}
return null;
}
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Text → pixel-image rasteriser
// ---------------------------------------------------------------------------
export function rasterizeText(
text: string,
cellHeight: number,
font: { FONT_DATA: Uint8Array },
): { pixels: Uint8Array; width: number; height: number } {
const hdr = parseHeader(font.FONT_DATA);
const glyphs: Array<U8G2GlyphData> = [];
for (const char of text) {
const cp = char.codePointAt(0) ?? 0x3F;
const g = u8g2Glyph(cp, font.FONT_DATA);
if (g) glyphs.push(g);
}
if (glyphs.length === 0) {
return { pixels: new Uint8Array(cellHeight), width: 1, height: cellHeight };
}
// Total width: sum all advances, but last glyph uses its actual pixel width
const totalW = glyphs.reduce((sum, g, i) =>
sum + (i < glyphs.length - 1 ? g.dx : g.w), 0);
const pixels = new Uint8Array(totalW * cellHeight);
// Top-align the full font (ascent + |descent|) within the cell.
// header byte 14 is descent of 'g' - stored as a negative signed value.
const descent = font.FONT_DATA[14]! > 127 ? font.FONT_DATA[14]! - 256 : font.FONT_DATA[14]!;
const fontH = hdr.ascentA - descent; // total font height in pixels
const topPad = 0;
const baseline = topPad + hdr.ascentA;
let x = 0;
for (const g of glyphs) {
// yOff = ascentA - yBearing - h (may be negative if glyph taller than 'A')
const top = baseline - hdr.ascentA + g.yOff;
for (let col = 0; col < g.w; col++) {
const colByte = g.cols[col] ?? 0;
for (let row = 0; row < g.h; row++) {
if ((colByte >> row) & 1) {
const py = top + row;
if (py >= 0 && py < cellHeight) {
pixels[py * totalW + x + col] = 1;
}
}
}
}
x += g.dx;
}
return { pixels, width: totalW, height: cellHeight };
}