parent
5af50fcd29
commit
b9fa8ba3ac
@ -1,216 +0,0 @@ |
|||||||
// =============================================================================
|
|
||||||
// font.ts — built-in 5×7 bitmap font + text rasteriser
|
|
||||||
//
|
|
||||||
// Font encoding: column-major, 5 bytes per glyph (one byte per column).
|
|
||||||
// Each byte: bit 0 = topmost pixel, bit 6 = bottom pixel (7 rows used).
|
|
||||||
// Covers ASCII 0x20 (space) … 0x7E (~). Unknown codepoints → replacement glyph.
|
|
||||||
//
|
|
||||||
// Custom fonts (U8G2 binary format) accepted but glyph lookup not yet
|
|
||||||
// implemented — falls back to built-in automatically.
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export const GLYPH_W = 5; // pixel columns per glyph
|
|
||||||
export const GLYPH_H = 7; // pixel rows per glyph
|
|
||||||
export const GLYPH_ADVANCE = 6; // pixels advanced per character (glyph + 1px gap)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Built-in 5×7 font data — classic Adafruit GFX / GNU unifont subset
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const FONT_5x7 = new Uint8Array([ |
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, // 0x20 ' '
|
|
||||||
0x00, 0x00, 0x5F, 0x00, 0x00, // 0x21 '!'
|
|
||||||
0x00, 0x07, 0x00, 0x07, 0x00, // 0x22 '"'
|
|
||||||
0x14, 0x7F, 0x14, 0x7F, 0x14, // 0x23 '#'
|
|
||||||
0x24, 0x2A, 0x7F, 0x2A, 0x12, // 0x24 '$'
|
|
||||||
0x23, 0x13, 0x08, 0x64, 0x62, // 0x25 '%'
|
|
||||||
0x36, 0x49, 0x55, 0x22, 0x50, // 0x26 '&'
|
|
||||||
0x00, 0x05, 0x03, 0x00, 0x00, // 0x27 "'"
|
|
||||||
0x00, 0x1C, 0x22, 0x41, 0x00, // 0x28 '('
|
|
||||||
0x00, 0x41, 0x22, 0x1C, 0x00, // 0x29 ')'
|
|
||||||
0x14, 0x08, 0x3E, 0x08, 0x14, // 0x2A '*'
|
|
||||||
0x08, 0x08, 0x3E, 0x08, 0x08, // 0x2B '+'
|
|
||||||
0x00, 0x50, 0x30, 0x00, 0x00, // 0x2C ','
|
|
||||||
0x08, 0x08, 0x08, 0x08, 0x08, // 0x2D '-'
|
|
||||||
0x00, 0x60, 0x60, 0x00, 0x00, // 0x2E '.'
|
|
||||||
0x20, 0x10, 0x08, 0x04, 0x02, // 0x2F '/'
|
|
||||||
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0x30 '0'
|
|
||||||
0x00, 0x42, 0x7F, 0x40, 0x00, // 0x31 '1'
|
|
||||||
0x42, 0x61, 0x51, 0x49, 0x46, // 0x32 '2'
|
|
||||||
0x21, 0x41, 0x45, 0x4B, 0x31, // 0x33 '3'
|
|
||||||
0x18, 0x14, 0x12, 0x7F, 0x10, // 0x34 '4'
|
|
||||||
0x27, 0x45, 0x45, 0x45, 0x39, // 0x35 '5'
|
|
||||||
0x3C, 0x4A, 0x49, 0x49, 0x30, // 0x36 '6'
|
|
||||||
0x01, 0x71, 0x09, 0x05, 0x03, // 0x37 '7'
|
|
||||||
0x36, 0x49, 0x49, 0x49, 0x36, // 0x38 '8'
|
|
||||||
0x06, 0x49, 0x49, 0x29, 0x1E, // 0x39 '9'
|
|
||||||
0x00, 0x36, 0x36, 0x00, 0x00, // 0x3A ':'
|
|
||||||
0x00, 0x56, 0x36, 0x00, 0x00, // 0x3B ';'
|
|
||||||
0x08, 0x14, 0x22, 0x41, 0x00, // 0x3C '<'
|
|
||||||
0x14, 0x14, 0x14, 0x14, 0x14, // 0x3D '='
|
|
||||||
0x00, 0x41, 0x22, 0x14, 0x08, // 0x3E '>'
|
|
||||||
0x02, 0x01, 0x51, 0x09, 0x06, // 0x3F '?'
|
|
||||||
0x32, 0x49, 0x79, 0x41, 0x3E, // 0x40 '@'
|
|
||||||
0x7E, 0x11, 0x11, 0x11, 0x7E, // 0x41 'A'
|
|
||||||
0x7F, 0x49, 0x49, 0x49, 0x36, // 0x42 'B'
|
|
||||||
0x3E, 0x41, 0x41, 0x41, 0x22, // 0x43 'C'
|
|
||||||
0x7F, 0x41, 0x41, 0x22, 0x1C, // 0x44 'D'
|
|
||||||
0x7F, 0x49, 0x49, 0x49, 0x41, // 0x45 'E'
|
|
||||||
0x7F, 0x09, 0x09, 0x09, 0x01, // 0x46 'F'
|
|
||||||
0x3E, 0x41, 0x49, 0x49, 0x7A, // 0x47 'G'
|
|
||||||
0x7F, 0x08, 0x08, 0x08, 0x7F, // 0x48 'H'
|
|
||||||
0x00, 0x41, 0x7F, 0x41, 0x00, // 0x49 'I'
|
|
||||||
0x20, 0x40, 0x41, 0x3F, 0x01, // 0x4A 'J'
|
|
||||||
0x7F, 0x08, 0x14, 0x22, 0x41, // 0x4B 'K'
|
|
||||||
0x7F, 0x40, 0x40, 0x40, 0x40, // 0x4C 'L'
|
|
||||||
0x7F, 0x02, 0x0C, 0x02, 0x7F, // 0x4D 'M'
|
|
||||||
0x7F, 0x04, 0x08, 0x10, 0x7F, // 0x4E 'N'
|
|
||||||
0x3E, 0x41, 0x41, 0x41, 0x3E, // 0x4F 'O'
|
|
||||||
0x7F, 0x09, 0x09, 0x09, 0x06, // 0x50 'P'
|
|
||||||
0x3E, 0x41, 0x51, 0x21, 0x5E, // 0x51 'Q'
|
|
||||||
0x7F, 0x09, 0x19, 0x29, 0x46, // 0x52 'R'
|
|
||||||
0x46, 0x49, 0x49, 0x49, 0x31, // 0x53 'S'
|
|
||||||
0x01, 0x01, 0x7F, 0x01, 0x01, // 0x54 'T'
|
|
||||||
0x3F, 0x40, 0x40, 0x40, 0x3F, // 0x55 'U'
|
|
||||||
0x1F, 0x20, 0x40, 0x20, 0x1F, // 0x56 'V'
|
|
||||||
0x3F, 0x40, 0x38, 0x40, 0x3F, // 0x57 'W'
|
|
||||||
0x63, 0x14, 0x08, 0x14, 0x63, // 0x58 'X'
|
|
||||||
0x07, 0x08, 0x70, 0x08, 0x07, // 0x59 'Y'
|
|
||||||
0x61, 0x51, 0x49, 0x45, 0x43, // 0x5A 'Z'
|
|
||||||
0x00, 0x7F, 0x41, 0x41, 0x00, // 0x5B '['
|
|
||||||
0x02, 0x04, 0x08, 0x10, 0x20, // 0x5C '\'
|
|
||||||
0x00, 0x41, 0x41, 0x7F, 0x00, // 0x5D ']'
|
|
||||||
0x04, 0x02, 0x01, 0x02, 0x04, // 0x5E '^'
|
|
||||||
0x40, 0x40, 0x40, 0x40, 0x40, // 0x5F '_'
|
|
||||||
0x00, 0x01, 0x02, 0x04, 0x00, // 0x60 '`'
|
|
||||||
0x20, 0x54, 0x54, 0x54, 0x78, // 0x61 'a'
|
|
||||||
0x7F, 0x48, 0x44, 0x44, 0x38, // 0x62 'b'
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x20, // 0x63 'c'
|
|
||||||
0x38, 0x44, 0x44, 0x48, 0x7F, // 0x64 'd'
|
|
||||||
0x38, 0x54, 0x54, 0x54, 0x18, // 0x65 'e'
|
|
||||||
0x08, 0x7E, 0x09, 0x01, 0x02, // 0x66 'f'
|
|
||||||
0x0C, 0x52, 0x52, 0x52, 0x3E, // 0x67 'g'
|
|
||||||
0x7F, 0x08, 0x04, 0x04, 0x78, // 0x68 'h'
|
|
||||||
0x00, 0x44, 0x7D, 0x40, 0x00, // 0x69 'i'
|
|
||||||
0x20, 0x40, 0x44, 0x3D, 0x00, // 0x6A 'j'
|
|
||||||
0x7F, 0x10, 0x28, 0x44, 0x00, // 0x6B 'k'
|
|
||||||
0x00, 0x41, 0x7F, 0x40, 0x00, // 0x6C 'l'
|
|
||||||
0x7C, 0x04, 0x18, 0x04, 0x78, // 0x6D 'm'
|
|
||||||
0x7C, 0x08, 0x04, 0x04, 0x78, // 0x6E 'n'
|
|
||||||
0x38, 0x44, 0x44, 0x44, 0x38, // 0x6F 'o'
|
|
||||||
0x7C, 0x14, 0x14, 0x14, 0x08, // 0x70 'p'
|
|
||||||
0x08, 0x14, 0x14, 0x18, 0x7C, // 0x71 'q'
|
|
||||||
0x7C, 0x08, 0x04, 0x04, 0x08, // 0x72 'r'
|
|
||||||
0x48, 0x54, 0x54, 0x54, 0x20, // 0x73 's'
|
|
||||||
0x04, 0x3F, 0x44, 0x40, 0x20, // 0x74 't'
|
|
||||||
0x3C, 0x40, 0x40, 0x20, 0x7C, // 0x75 'u'
|
|
||||||
0x1C, 0x20, 0x40, 0x20, 0x1C, // 0x76 'v'
|
|
||||||
0x3C, 0x40, 0x30, 0x40, 0x3C, // 0x77 'w'
|
|
||||||
0x44, 0x28, 0x10, 0x28, 0x44, // 0x78 'x'
|
|
||||||
0x0C, 0x50, 0x50, 0x50, 0x3C, // 0x79 'y'
|
|
||||||
0x44, 0x64, 0x54, 0x4C, 0x44, // 0x7A 'z'
|
|
||||||
0x00, 0x08, 0x36, 0x41, 0x00, // 0x7B '{'
|
|
||||||
0x00, 0x00, 0x7F, 0x00, 0x00, // 0x7C '|'
|
|
||||||
0x00, 0x41, 0x36, 0x08, 0x00, // 0x7D '}'
|
|
||||||
0x10, 0x08, 0x08, 0x10, 0x08, // 0x7E '~'
|
|
||||||
]); |
|
||||||
|
|
||||||
// Replacement glyph for codepoints outside built-in coverage: filled 5×7 box
|
|
||||||
// prettier-ignore
|
|
||||||
const REPLACEMENT_GLYPH = new Uint8Array([0x7F, 0x41, 0x41, 0x41, 0x7F]); |
|
||||||
|
|
||||||
/** |
|
||||||
* Return the 5 column-bytes for a codepoint from the built-in font. |
|
||||||
* Returns REPLACEMENT_GLYPH for anything outside ASCII 0x20–0x7E. |
|
||||||
*/ |
|
||||||
export function builtinGlyph(cp: number): Uint8Array { |
|
||||||
if (cp >= 0x20 && cp <= 0x7E) { |
|
||||||
const off = (cp - 0x20) * GLYPH_W; |
|
||||||
return FONT_5x7.subarray(off, off + GLYPH_W); |
|
||||||
} |
|
||||||
return REPLACEMENT_GLYPH; |
|
||||||
} |
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Custom font (U8G2 binary format)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** |
|
||||||
* Look up a glyph in a U8G2 font blob. |
|
||||||
* Returns null if the codepoint is not found or the format is unsupported, |
|
||||||
* in which case the caller should fall back to builtinGlyph(). |
|
||||||
* |
|
||||||
* U8G2 font parsing is not yet fully implemented — this is a hook for future |
|
||||||
* integration with the u8g2-fonts Rust crate output. |
|
||||||
*/ |
|
||||||
export function u8g2Glyph( |
|
||||||
_cp: number, |
|
||||||
_fontData: Uint8Array, |
|
||||||
): { cols: Uint8Array; w: number; h: number; xOff: number; yOff: number } | null { |
|
||||||
// TODO: implement U8G2 binary glyph lookup
|
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Text → pixel-image rasteriser
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** |
|
||||||
* Rasterise `text` into a 1bpp pixel image. |
|
||||||
* Iterates over actual Unicode codepoints (handles surrogate pairs / emoji). |
|
||||||
* |
|
||||||
* - `cellHeight`: height of the destination cell in pixels; glyphs are |
|
||||||
* centred vertically within it. |
|
||||||
* - `customFont`: optional raw U8G2 font data; if provided and the glyph is |
|
||||||
* found, it is used instead of the built-in font. |
|
||||||
* |
|
||||||
* Returns a pixel image sized (textWidth × cellHeight) where each byte is 0 |
|
||||||
* (off) or 1 (on). |
|
||||||
*/ |
|
||||||
export function rasterizeText( |
|
||||||
text: string, |
|
||||||
cellHeight: number, |
|
||||||
customFont?: Uint8Array, |
|
||||||
): { pixels: Uint8Array; width: number; height: number } { |
|
||||||
// Collect glyph column data for each codepoint
|
|
||||||
const glyphs: Array<{ cols: Uint8Array; w: number; h: number; yOff: number }> = []; |
|
||||||
|
|
||||||
for (const char of text) { // ← real codepoint iteration
|
|
||||||
const cp = char.codePointAt(0) ?? 0x3F; // '?' fallback
|
|
||||||
|
|
||||||
// Try custom font first, fall back to built-in
|
|
||||||
const custom = customFont ? u8g2Glyph(cp, customFont) : null; |
|
||||||
if (custom) { |
|
||||||
glyphs.push({ cols: custom.cols, w: custom.w, h: custom.h, yOff: custom.yOff }); |
|
||||||
} else { |
|
||||||
glyphs.push({ cols: builtinGlyph(cp), w: GLYPH_W, h: GLYPH_H, yOff: 0 }); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Total pixel width = sum of advances (GLYPH_ADVANCE per glyph, no trailing gap)
|
|
||||||
const totalW = Math.max(1, glyphs.length * GLYPH_ADVANCE - 1); // remove last gap
|
|
||||||
const pixels = new Uint8Array(totalW * cellHeight); |
|
||||||
|
|
||||||
// Vertical centre of glyph within cell
|
|
||||||
const yBase = Math.max(0, Math.floor((cellHeight - GLYPH_H) / 2)); |
|
||||||
|
|
||||||
let x = 0; |
|
||||||
for (const g of glyphs) { |
|
||||||
const top = yBase + 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 += GLYPH_ADVANCE; |
|
||||||
} |
|
||||||
|
|
||||||
return { pixels, width: totalW, height: cellHeight }; |
|
||||||
} |
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
|
||||||
|
|
||||||
|
export * from "./types"; |
||||||
|
export * as _5x7 from "./5x7"; |
||||||
|
export * from "./u8g2"; |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
export class MonoDisplayFont { |
||||||
|
public static FONT_DATA: Uint8Array; |
||||||
|
} |
||||||
|
|
||||||
|
export type RasterizedText = { pixels: Uint8Array; width: number; height: number }; |
||||||
|
|
||||||
|
export type RasterizeTextFunction = ( |
||||||
|
text: string, |
||||||
|
cellHeight: number, |
||||||
|
font?: MonoDisplayFont, |
||||||
|
) => RasterizedText; |
||||||
@ -0,0 +1,411 @@ |
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// U8G2 font binary format constants (header offsets)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// The U8G2 font blob layout (all offsets are byte positions):
|
||||||
|
// [0] glyph_cnt – number of glyphs
|
||||||
|
// [1] bbx_mode – bounding-box mode
|
||||||
|
// [2] bits_per_0 – RLE bits for "off" run
|
||||||
|
// [3] bits_per_1 – RLE bits for "on" run
|
||||||
|
// [4] bits_per_char_width
|
||||||
|
// [5] bits_per_char_height
|
||||||
|
// [6] bits_per_char_x
|
||||||
|
// [7] bits_per_char_y
|
||||||
|
// [8] bits_per_delta_x
|
||||||
|
// [9] max_char_width
|
||||||
|
// [10] max_char_height
|
||||||
|
// [11] x_offset (signed)
|
||||||
|
// [12] y_offset (signed)
|
||||||
|
// [13] ascent_A
|
||||||
|
// [14] descent_g (negative)
|
||||||
|
// [15] ascent_para
|
||||||
|
// [16] descent_para
|
||||||
|
// [17..18] start_pos_upper (uint16 BE) – offset of uppercase glyph block
|
||||||
|
// [19..20] start_pos_lower (uint16 BE) – offset of lowercase glyph block
|
||||||
|
// [21..22] start_pos_unicode (uint16 BE) – offset of Unicode glyph block
|
||||||
|
// Glyph records follow the three jump-table blocks.
|
||||||
|
// Each glyph record:
|
||||||
|
// • A variable-length RLE bitstream (width × height bits) – MSB first
|
||||||
|
// • Followed by a fixed-width metadata section:
|
||||||
|
// dwidth (bits_per_delta_x bits) – horizontal advance
|
||||||
|
// width (bits_per_char_width)
|
||||||
|
// height (bits_per_char_height)
|
||||||
|
// x (bits_per_char_x, signed)
|
||||||
|
// y (bits_per_char_y, signed)
|
||||||
|
// The very first two bytes of each record encode the encoding (codepoint)
|
||||||
|
// as a uint16 LE, then a uint16 LE jump offset to the next record.
|
||||||
|
|
||||||
|
import type { MonoDisplayFont } from "./types"; |
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Built-in 5×7 font (printable ASCII 0x20–0x7E)
|
||||||
|
// Each entry is 5 column bytes, bit 0 = top row.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// prettier-ignore
|
||||||
|
const BUILTIN_FONT_DATA: Record<number, number[]> = { |
||||||
|
0x20: [0x00, 0x00, 0x00, 0x00, 0x00], // space
|
||||||
|
0x21: [0x00, 0x00, 0x5F, 0x00, 0x00], // !
|
||||||
|
0x22: [0x00, 0x07, 0x00, 0x07, 0x00], // "
|
||||||
|
0x23: [0x14, 0x7F, 0x14, 0x7F, 0x14], // #
|
||||||
|
0x24: [0x24, 0x2A, 0x7F, 0x2A, 0x12], // $
|
||||||
|
0x25: [0x23, 0x13, 0x08, 0x64, 0x62], // %
|
||||||
|
0x26: [0x36, 0x49, 0x55, 0x22, 0x50], // &
|
||||||
|
0x27: [0x00, 0x05, 0x03, 0x00, 0x00], // '
|
||||||
|
0x28: [0x00, 0x1C, 0x22, 0x41, 0x00], // (
|
||||||
|
0x29: [0x00, 0x41, 0x22, 0x1C, 0x00], // )
|
||||||
|
0x2A: [0x14, 0x08, 0x3E, 0x08, 0x14], // *
|
||||||
|
0x2B: [0x08, 0x08, 0x3E, 0x08, 0x08], // +
|
||||||
|
0x2C: [0x00, 0x50, 0x30, 0x00, 0x00], // ,
|
||||||
|
0x2D: [0x08, 0x08, 0x08, 0x08, 0x08], // -
|
||||||
|
0x2E: [0x00, 0x60, 0x60, 0x00, 0x00], // .
|
||||||
|
0x2F: [0x20, 0x10, 0x08, 0x04, 0x02], // /
|
||||||
|
0x30: [0x3E, 0x51, 0x49, 0x45, 0x3E], // 0
|
||||||
|
0x31: [0x00, 0x42, 0x7F, 0x40, 0x00], // 1
|
||||||
|
0x32: [0x42, 0x61, 0x51, 0x49, 0x46], // 2
|
||||||
|
0x33: [0x21, 0x41, 0x45, 0x4B, 0x31], // 3
|
||||||
|
0x34: [0x18, 0x14, 0x12, 0x7F, 0x10], // 4
|
||||||
|
0x35: [0x27, 0x45, 0x45, 0x45, 0x39], // 5
|
||||||
|
0x36: [0x3C, 0x4A, 0x49, 0x49, 0x30], // 6
|
||||||
|
0x37: [0x01, 0x71, 0x09, 0x05, 0x03], // 7
|
||||||
|
0x38: [0x36, 0x49, 0x49, 0x49, 0x36], // 8
|
||||||
|
0x39: [0x06, 0x49, 0x49, 0x29, 0x1E], // 9
|
||||||
|
0x3A: [0x00, 0x36, 0x36, 0x00, 0x00], // :
|
||||||
|
0x3B: [0x00, 0x56, 0x36, 0x00, 0x00], // ;
|
||||||
|
0x3C: [0x08, 0x14, 0x22, 0x41, 0x00], // <
|
||||||
|
0x3D: [0x14, 0x14, 0x14, 0x14, 0x14], // =
|
||||||
|
0x3E: [0x00, 0x41, 0x22, 0x14, 0x08], // >
|
||||||
|
0x3F: [0x02, 0x01, 0x51, 0x09, 0x06], // ?
|
||||||
|
0x40: [0x32, 0x49, 0x79, 0x41, 0x3E], // @
|
||||||
|
0x41: [0x7E, 0x11, 0x11, 0x11, 0x7E], // A
|
||||||
|
0x42: [0x7F, 0x49, 0x49, 0x49, 0x36], // B
|
||||||
|
0x43: [0x3E, 0x41, 0x41, 0x41, 0x22], // C
|
||||||
|
0x44: [0x7F, 0x41, 0x41, 0x22, 0x1C], // D
|
||||||
|
0x45: [0x7F, 0x49, 0x49, 0x49, 0x41], // E
|
||||||
|
0x46: [0x7F, 0x09, 0x09, 0x09, 0x01], // F
|
||||||
|
0x47: [0x3E, 0x41, 0x49, 0x49, 0x7A], // G
|
||||||
|
0x48: [0x7F, 0x08, 0x08, 0x08, 0x7F], // H
|
||||||
|
0x49: [0x00, 0x41, 0x7F, 0x41, 0x00], // I
|
||||||
|
0x4A: [0x20, 0x40, 0x41, 0x3F, 0x01], // J
|
||||||
|
0x4B: [0x7F, 0x08, 0x14, 0x22, 0x41], // K
|
||||||
|
0x4C: [0x7F, 0x40, 0x40, 0x40, 0x40], // L
|
||||||
|
0x4D: [0x7F, 0x02, 0x0C, 0x02, 0x7F], // M
|
||||||
|
0x4E: [0x7F, 0x04, 0x08, 0x10, 0x7F], // N
|
||||||
|
0x4F: [0x3E, 0x41, 0x41, 0x41, 0x3E], // O
|
||||||
|
0x50: [0x7F, 0x09, 0x09, 0x09, 0x06], // P
|
||||||
|
0x51: [0x3E, 0x41, 0x51, 0x21, 0x5E], // Q
|
||||||
|
0x52: [0x7F, 0x09, 0x19, 0x29, 0x46], // R
|
||||||
|
0x53: [0x46, 0x49, 0x49, 0x49, 0x31], // S
|
||||||
|
0x54: [0x01, 0x01, 0x7F, 0x01, 0x01], // T
|
||||||
|
0x55: [0x3F, 0x40, 0x40, 0x40, 0x3F], // U
|
||||||
|
0x56: [0x1F, 0x20, 0x40, 0x20, 0x1F], // V
|
||||||
|
0x57: [0x3F, 0x40, 0x38, 0x40, 0x3F], // W
|
||||||
|
0x58: [0x63, 0x14, 0x08, 0x14, 0x63], // X
|
||||||
|
0x59: [0x07, 0x08, 0x70, 0x08, 0x07], // Y
|
||||||
|
0x5A: [0x61, 0x51, 0x49, 0x45, 0x43], // Z
|
||||||
|
0x5B: [0x00, 0x7F, 0x41, 0x41, 0x00], // [
|
||||||
|
0x5C: [0x02, 0x04, 0x08, 0x10, 0x20], // backslash
|
||||||
|
0x5D: [0x00, 0x41, 0x41, 0x7F, 0x00], // ]
|
||||||
|
0x5E: [0x04, 0x02, 0x01, 0x02, 0x04], // ^
|
||||||
|
0x5F: [0x40, 0x40, 0x40, 0x40, 0x40], // _
|
||||||
|
0x60: [0x00, 0x01, 0x02, 0x04, 0x00], // `
|
||||||
|
0x61: [0x20, 0x54, 0x54, 0x54, 0x78], // a
|
||||||
|
0x62: [0x7F, 0x48, 0x44, 0x44, 0x38], // b
|
||||||
|
0x63: [0x38, 0x44, 0x44, 0x44, 0x20], // c
|
||||||
|
0x64: [0x38, 0x44, 0x44, 0x48, 0x7F], // d
|
||||||
|
0x65: [0x38, 0x54, 0x54, 0x54, 0x18], // e
|
||||||
|
0x66: [0x08, 0x7E, 0x09, 0x01, 0x02], // f
|
||||||
|
0x67: [0x0C, 0x52, 0x52, 0x52, 0x3E], // g
|
||||||
|
0x68: [0x7F, 0x08, 0x04, 0x04, 0x78], // h
|
||||||
|
0x69: [0x00, 0x44, 0x7D, 0x40, 0x00], // i
|
||||||
|
0x6A: [0x20, 0x40, 0x44, 0x3D, 0x00], // j
|
||||||
|
0x6B: [0x7F, 0x10, 0x28, 0x44, 0x00], // k
|
||||||
|
0x6C: [0x00, 0x41, 0x7F, 0x40, 0x00], // l
|
||||||
|
0x6D: [0x7C, 0x04, 0x18, 0x04, 0x78], // m
|
||||||
|
0x6E: [0x7C, 0x08, 0x04, 0x04, 0x78], // n
|
||||||
|
0x6F: [0x38, 0x44, 0x44, 0x44, 0x38], // o
|
||||||
|
0x70: [0x7C, 0x14, 0x14, 0x14, 0x08], // p
|
||||||
|
0x71: [0x08, 0x14, 0x14, 0x18, 0x7C], // q
|
||||||
|
0x72: [0x7C, 0x08, 0x04, 0x04, 0x08], // r
|
||||||
|
0x73: [0x48, 0x54, 0x54, 0x54, 0x20], // s
|
||||||
|
0x74: [0x04, 0x3F, 0x44, 0x40, 0x20], // t
|
||||||
|
0x75: [0x3C, 0x40, 0x40, 0x20, 0x7C], // u
|
||||||
|
0x76: [0x1C, 0x20, 0x40, 0x20, 0x1C], // v
|
||||||
|
0x77: [0x3C, 0x40, 0x30, 0x40, 0x3C], // w
|
||||||
|
0x78: [0x44, 0x28, 0x10, 0x28, 0x44], // x
|
||||||
|
0x79: [0x0C, 0x50, 0x50, 0x50, 0x3C], // y
|
||||||
|
0x7A: [0x44, 0x64, 0x54, 0x4C, 0x44], // z
|
||||||
|
0x7B: [0x00, 0x08, 0x36, 0x41, 0x00], // {
|
||||||
|
0x7C: [0x00, 0x00, 0x7F, 0x00, 0x00], // |
|
||||||
|
0x7D: [0x00, 0x41, 0x36, 0x08, 0x00], // }
|
||||||
|
0x7E: [0x10, 0x08, 0x08, 0x10, 0x08], // ~
|
||||||
|
}; |
||||||
|
|
||||||
|
// Built-in font metrics (5×7, 1px column gap → advance = 6)
|
||||||
|
export const GLYPH_W = 5; |
||||||
|
export const GLYPH_H = 7; |
||||||
|
export const GLYPH_ADVANCE = 6; // width + 1px gap
|
||||||
|
|
||||||
|
/** |
||||||
|
* Return column bytes for a codepoint from the built-in 5×7 font. |
||||||
|
* Falls back to '?' (0x3F) for unknown codepoints. |
||||||
|
*/ |
||||||
|
export function builtinGlyph(cp: number): Uint8Array { |
||||||
|
const cols = BUILTIN_FONT_DATA[cp] ?? BUILTIN_FONT_DATA[0x3F]!; |
||||||
|
return new Uint8Array(cols); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// U8G2 binary font parser
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Bit-level reader over a Uint8Array, MSB-first within each byte. */ |
||||||
|
class BitReader { |
||||||
|
private pos = 0; // bit position
|
||||||
|
constructor(private data: Uint8Array, startBit = 0) { |
||||||
|
this.pos = startBit; |
||||||
|
} |
||||||
|
|
||||||
|
read(n: number): number { |
||||||
|
let v = 0; |
||||||
|
for (let i = 0; i < n; i++) { |
||||||
|
const byteIdx = (this.pos) >> 3; |
||||||
|
const bitIdx = 7 - ((this.pos) & 7); // MSB first
|
||||||
|
v = (v << 1) | ((this.data[byteIdx]! >> bitIdx) & 1); |
||||||
|
this.pos++; |
||||||
|
} |
||||||
|
return v; |
||||||
|
} |
||||||
|
|
||||||
|
/** Read a signed value stored in `n` bits (two's complement). */ |
||||||
|
readSigned(n: number): number { |
||||||
|
const v = this.read(n); |
||||||
|
const sign = 1 << (n - 1); |
||||||
|
return v & sign ? v - (sign << 1) : v; |
||||||
|
} |
||||||
|
|
||||||
|
get bitPos(): number { return this.pos; } |
||||||
|
} |
||||||
|
|
||||||
|
/** Read a big-endian uint16 from a byte array at `offset`. */ |
||||||
|
function readU16BE(data: Uint8Array, offset: number): number { |
||||||
|
return ((data[offset]! << 8) | data[offset + 1]!) >>> 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Glyph result from a U8G2 font blob. |
||||||
|
* `cols` – column bytes (bit 0 = top row), length = w. |
||||||
|
*/ |
||||||
|
export interface U8G2GlyphData { |
||||||
|
cols: Uint8Array; |
||||||
|
w: number; |
||||||
|
h: number; |
||||||
|
yOff: number; // positive = shift down
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse header fields from a U8G2 font blob and return the key metrics |
||||||
|
* needed to decode glyphs. |
||||||
|
*/ |
||||||
|
function parseHeader(font: Uint8Array) { |
||||||
|
return { |
||||||
|
glyphCnt: font[0]!, |
||||||
|
bbxMode: font[1]!, |
||||||
|
bitsPerRle0: font[2]!, |
||||||
|
bitsPerRle1: font[3]!, |
||||||
|
bitsPerW: font[4]!, |
||||||
|
bitsPerH: font[5]!, |
||||||
|
bitsPerX: font[6]!, |
||||||
|
bitsPerY: font[7]!, |
||||||
|
bitsPerDx: font[8]!, |
||||||
|
maxCharW: font[9]!, |
||||||
|
maxCharH: font[10]!, |
||||||
|
xOff: font[11]! as number, // signed but treated as-is
|
||||||
|
yOff: font[12]! as number, |
||||||
|
ascentA: font[13]!, |
||||||
|
descentG: font[14]!, |
||||||
|
startUpper: readU16BE(font, 17), |
||||||
|
startLower: readU16BE(font, 19), |
||||||
|
startUnicode: readU16BE(font, 21), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Decode one glyph from the U8G2 font RLE bitstream at `byteOffset`. |
||||||
|
* Returns decoded data or null if the stream is malformed. |
||||||
|
* |
||||||
|
* U8G2 glyph encoding (bitstream, MSB-first): |
||||||
|
* [bitsPerW] w glyph bitmap width |
||||||
|
* [bitsPerH] h glyph bitmap height |
||||||
|
* [bitsPerX] x signed x bearing |
||||||
|
* [bitsPerY] y signed y bearing (positive = up from baseline) |
||||||
|
* [bitsPerDx] dwidth horizontal advance |
||||||
|
* Then w×h bits of RLE-encoded bitmap: |
||||||
|
* repeat: |
||||||
|
* 1-bit type (0 = off-run, 1 = on-run) |
||||||
|
* bitsPerRleN count bits |
||||||
|
*/ |
||||||
|
function decodeGlyphAt( |
||||||
|
font: Uint8Array, |
||||||
|
byteOffset: number, |
||||||
|
hdr: ReturnType<typeof parseHeader>, |
||||||
|
): U8G2GlyphData | null { |
||||||
|
try { |
||||||
|
const br = new BitReader(font, byteOffset * 8); |
||||||
|
|
||||||
|
const w = br.read(hdr.bitsPerW); |
||||||
|
const h = br.read(hdr.bitsPerH); |
||||||
|
const xBrg = br.readSigned(hdr.bitsPerX); |
||||||
|
const yBrg = br.readSigned(hdr.bitsPerY); // positive = up
|
||||||
|
br.read(hdr.bitsPerDx); // advance – not needed here
|
||||||
|
|
||||||
|
if (w === 0 || h === 0) { |
||||||
|
// Blank glyph (e.g. space)
|
||||||
|
return { cols: new Uint8Array(GLYPH_W), w: GLYPH_W, h: GLYPH_H, yOff: 0 }; |
||||||
|
} |
||||||
|
|
||||||
|
// Decode RLE bitmap into a flat bit array [row-major, top-to-bottom]
|
||||||
|
const totalBits = w * h; |
||||||
|
const bits = new Uint8Array(totalBits); |
||||||
|
let filled = 0; |
||||||
|
|
||||||
|
while (filled < totalBits) { |
||||||
|
const type = br.read(1); // 0=off, 1=on
|
||||||
|
const bitsN = type === 0 ? hdr.bitsPerRle0 : hdr.bitsPerRle1; |
||||||
|
const count = br.read(bitsN) + 1; // stored as count-1
|
||||||
|
const val = type & 1; |
||||||
|
for (let i = 0; i < count && filled < totalBits; i++) { |
||||||
|
bits[filled++] = val; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Convert row-major bitmap → column bytes (bit 0 = top row) to match
|
||||||
|
// the builtin font layout consumed by rasterizeText.
|
||||||
|
const cols = new Uint8Array(w); |
||||||
|
for (let row = 0; row < h; row++) { |
||||||
|
for (let col = 0; col < w; col++) { |
||||||
|
if (bits[row * w + col]) { |
||||||
|
// ts-ignore
|
||||||
|
cols[col] |= 1 << row; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// yBrg is measured upward from baseline in U8G2 conventions.
|
||||||
|
// Convert to a downward pixel offset relative to the top of GLYPH_H.
|
||||||
|
// ascentA = pixels above baseline for capital 'A'
|
||||||
|
// yOff = (ascentA - yBrg - h): rows to push down from the top of
|
||||||
|
// the glyph box so the baseline lands in the right place.
|
||||||
|
const yOff = Math.max(0, hdr.ascentA - yBrg - h); |
||||||
|
|
||||||
|
return { cols, w, h, yOff }; |
||||||
|
} catch { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Scan the correct block of a U8G2 font for `cp` and return decoded glyph |
||||||
|
* data, or null if not found. |
||||||
|
* |
||||||
|
* Each glyph record in the jump table starts with: |
||||||
|
* [1 byte] encoding (codepoint, 0x00–0xFF in ASCII blocks) |
||||||
|
* [1 byte] jump offset to next record (0 = end of block) |
||||||
|
* then the bitstream described in decodeGlyphAt. |
||||||
|
* |
||||||
|
* For the Unicode block the encoding is 2 bytes (uint16 LE) and the jump |
||||||
|
* offset is also 2 bytes (uint16 LE). |
||||||
|
*/ |
||||||
|
export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null { |
||||||
|
if (font.length < 23) return null; |
||||||
|
|
||||||
|
const hdr = parseHeader(font); |
||||||
|
|
||||||
|
let blockStart: number; |
||||||
|
let wide: boolean; // true for the Unicode block (2-byte encoding + 2-byte jump)
|
||||||
|
|
||||||
|
if (cp >= 0x20 && cp <= 0x5F) { |
||||||
|
blockStart = hdr.startUpper; |
||||||
|
wide = false; |
||||||
|
} else if (cp >= 0x60 && cp <= 0x9F) { |
||||||
|
blockStart = hdr.startLower; |
||||||
|
wide = false; |
||||||
|
} else { |
||||||
|
blockStart = hdr.startUnicode; |
||||||
|
wide = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (blockStart === 0 || blockStart >= font.length) return null; |
||||||
|
|
||||||
|
let pos = blockStart; |
||||||
|
|
||||||
|
while (pos < font.length) { |
||||||
|
let encoding: number; |
||||||
|
let jump: number; |
||||||
|
|
||||||
|
if (wide) { |
||||||
|
if (pos + 4 > font.length) break; |
||||||
|
encoding = (font[pos]! | (font[pos + 1]! << 8)) >>> 0; // LE uint16
|
||||||
|
jump = (font[pos + 2]! | (font[pos + 3]! << 8)) >>> 0; |
||||||
|
pos += 4; |
||||||
|
} else { |
||||||
|
if (pos + 2 > font.length) break; |
||||||
|
encoding = font[pos]!; |
||||||
|
jump = font[pos + 1]!; |
||||||
|
pos += 2; |
||||||
|
} |
||||||
|
|
||||||
|
if (jump === 0) break; // end-of-block sentinel
|
||||||
|
|
||||||
|
if (encoding === cp) { |
||||||
|
return decodeGlyphAt(font, pos, hdr); |
||||||
|
} |
||||||
|
|
||||||
|
pos += jump; // skip to next record
|
||||||
|
} |
||||||
|
|
||||||
|
return null; // codepoint not in font
|
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Text → pixel-image rasteriser (as specified)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function rasterizeText( |
||||||
|
text: string, |
||||||
|
cellHeight: number, |
||||||
|
font?: MonoDisplayFont, |
||||||
|
): { pixels: Uint8Array; width: number; height: number } { |
||||||
|
const glyphs: Array<{ cols: Uint8Array; w: number; h: number; yOff: number }> = []; |
||||||
|
|
||||||
|
for (const char of text) { |
||||||
|
const cp = char.codePointAt(0) ?? 0x3F; |
||||||
|
const custom = font ? u8g2Glyph(cp, font.FONT_DATA) : null; |
||||||
|
if (custom) { |
||||||
|
glyphs.push({ cols: custom.cols, w: custom.w, h: custom.h, yOff: custom.yOff }); |
||||||
|
} else { |
||||||
|
glyphs.push({ cols: builtinGlyph(cp), w: GLYPH_W, h: GLYPH_H, yOff: 0 }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const totalW = Math.max(1, glyphs.length * GLYPH_ADVANCE - 1); |
||||||
|
const pixels = new Uint8Array(totalW * cellHeight); |
||||||
|
|
||||||
|
const yBase = Math.max(0, Math.floor((cellHeight - GLYPH_H) / 2)); |
||||||
|
let x = 0; |
||||||
|
|
||||||
|
for (const g of glyphs) { |
||||||
|
const top = yBase + 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 += GLYPH_ADVANCE; |
||||||
|
} |
||||||
|
|
||||||
|
return { pixels, width: totalW, height: cellHeight }; |
||||||
|
} |
||||||
Loading…
Reference in new issue