//! Dense 1-bit pixel-font text frames on a fixed grid, bundled X11 8x13. use std::collections::HashMap; use std::sync::OnceLock; const FONT: &[u8] = include_bytes!("../fonts/8x13.bdf"); const MARGIN: usize = 0; /// The 8x13 font box's 11px ascent; extra pitch leading sits below it. const BASELINE: usize = 11; const MAX_EDGE: usize = 8000; pub struct Frame { pub width: u32, pub height: u32, pub png: Vec, } struct Glyph { width: usize, height: usize, off_x: i32, off_y: i32, /// One packed bit row per glyph row; column c is bit (width-1-c). rows: Vec, } struct Bdf { glyphs: HashMap, } fn bdf() -> &'static Bdf { static BDF: OnceLock = OnceLock::new(); BDF.get_or_init(|| parse_bdf(FONT).expect("bundled 8x13.bdf parses")) } fn parse_bdf(bytes: &[u8]) -> Result { let text = std::str::from_utf8(bytes).map_err(|e| e.to_string())?; let mut glyphs = HashMap::new(); let mut encoding: Option = None; let mut box_: Option<(usize, usize, i32, i32)> = None; let mut bitmap: Option> = None; for line in text.lines() { let mut fields = line.split_whitespace(); match fields.next() { Some("ENCODING") => { encoding = fields .next() .and_then(|n| n.parse::().ok()) .and_then(|n| { if (0..=0x10FFFF).contains(&n) { Some(n as u32) } else { None } }); } Some("BBX") => { let mut number = || fields.next().and_then(|n| n.parse::().ok()); box_ = match (number(), number(), number(), number()) { (Some(w), Some(h), Some(x), Some(y)) if w > 0 && h > 0 => { Some((w as usize, h as usize, x as i32, y as i32)) } _ => None, }; } Some("BITMAP") => { let (w, h, x, y) = box_.ok_or("BITMAP without a BBX")?; bitmap = Some(Vec::with_capacity(h)); let _ = (w, x, y); } Some("ENDCHAR") => { if let (Some(code), Some((w, h, x, y)), Some(rows)) = (encoding, box_, bitmap.take()) { if rows.len() == h { glyphs.insert( code, Glyph { width: w, height: h, off_x: x, off_y: y, rows, }, ); } } encoding = None; box_ = None; } Some(hex) if bitmap.is_some() && !hex.is_empty() => { if let Some(row) = bitmap.as_mut() { row.push(parse_hex_row(hex)); } } _ => {} } } if glyphs.is_empty() { return Err("no glyphs in the bundled font".into()); } Ok(Bdf { glyphs }) } /// Hex bitmap bytes packed big-endian into one bit row (fonts here are /// <= 16 wide). Column c of a glyph of BBX width w is bit /// `(w + 7) / 8 * 8 - 1 - c` of the row. fn parse_hex_row(hex: &str) -> u16 { let mut row = 0u16; for i in 0..hex.len() / 2 { match u16::from_str_radix(&hex[2 * i..2 * i + 2], 16) { Ok(byte) => row = (row << 8) | byte, Err(_) => return row, } } row } pub fn render(text: &str, advance: usize, pitch: usize, width: usize) -> Result { let (width, height, pixels) = paint(text, advance, pitch, width)?; encode(width, height, pixels) } fn paint( text: &str, advance: usize, pitch: usize, width: usize, ) -> Result<(usize, usize, Vec), String> { if !(6..=24).contains(&advance) { return Err(format!("advance must be 6 to 24, not {advance}")); } if !(10..=64).contains(&pitch) { return Err(format!("pitch must be 10 to 64, not {pitch}")); } if !(256..=4096).contains(&width) || width < 2 * MARGIN + advance { return Err(format!("width must be 256 to 4096, not {width}")); } let columns = (width - 2 * MARGIN) / advance; let font = &bdf().glyphs; let question = font.get(&u32::from(b'?')).ok_or("the font lacks '?'")?; let mut rows: Vec> = Vec::new(); for line in text.split('\n') { let chars: Vec = line.chars().collect(); if chars.is_empty() { rows.push(Vec::new()); continue; } for chunk in chars.chunks(columns) { rows.push(chunk.to_vec()); } } if rows.is_empty() { rows.push(Vec::new()); } let height = 2 * MARGIN + rows.len() * pitch; if height > MAX_EDGE { return Err(format!( "{height}px exceeds the {MAX_EDGE}px image edge; chunk the text into smaller frames" )); } let mut pixels = vec![255u8; width * height]; for (index, row) in rows.iter().enumerate() { let top = MARGIN + index * pitch; for (column, ch) in row.iter().enumerate() { let cell = MARGIN + column * advance; // U+2588 marks a newline: fill the whole cell box with ink. if *ch as u32 == 0x2588 { for py in top..top + pitch { for px in cell..cell + advance { if px < width && py < height { pixels[py * width + px] = 0; } } } continue; } let glyph = font.get(&(*ch as u32)).unwrap_or(question); let x = cell as isize + glyph.off_x as isize; let baseline = (top + BASELINE) as isize; let glyph_top = baseline - (glyph.height as isize + glyph.off_y as isize); let byte_bits = 8 * ((glyph.width + 7) / 8); for (r, bits) in glyph.rows.iter().enumerate() { for c in 0..glyph.width { if bits & (1 << (byte_bits - 1 - c)) != 0 { let px = x + c as isize; let py = glyph_top + r as isize; if px >= 0 && py >= 0 && (px as usize) < width && (py as usize) < height { pixels[py as usize * width + px as usize] = 0; } } } } } } Ok((width, height, pixels)) } fn encode(width: usize, height: usize, pixels: Vec) -> Result { // One bit per pixel, MSB first: ink is the 0 bit, white the 1 bit. let stride = (width + 7) / 8; let mut bits = vec![0xFFu8; stride * height]; for (y, row) in pixels.chunks(width).enumerate() { for (x, ink) in row.iter().enumerate() { if *ink < 128 { bits[y * stride + x / 8] &= !(0x80 >> (x % 8)); } } } let mut out = Vec::new(); let mut encoder = png::Encoder::new(&mut out, width as u32, height as u32); encoder.set_color(png::ColorType::Grayscale); encoder.set_depth(png::BitDepth::One); encoder.set_compression(png::Compression::High); let mut writer = encoder.write_header().expect("header fits in memory"); writer .write_image_data(&bits) .expect("pixels match the header"); drop(writer); Ok(Frame { width: width as u32, height: height as u32, png: out, }) } #[cfg(test)] mod tests { use super::*; fn ink(text: &str) -> usize { let (_, _, pixels) = paint(text, 11, 16, 1568).unwrap(); pixels.iter().filter(|&&p| p < 128).count() } #[test] fn glyphs_leave_ink() { assert!(ink("hello world") > 50, "text must leave ink"); assert_eq!(ink(""), 0); assert!(ink("A") > 5, "one glyph leaves ink"); } #[test] fn full_block_fills_its_whole_cell() { let (width, height, pixels) = paint("\u{2588}", 11, 16, 1568).unwrap(); assert_eq!((width, height), (1568, 16)); let ink = pixels.iter().filter(|&&p| p < 128).count(); assert_eq!(ink, 11 * 16, "the block fills exactly one cell box"); } #[test] fn every_ascii_glyph_leaves_ink() { let text: String = (0x21u32..=0x7e) .map(|c| char::from_u32(c).unwrap()) .collect(); assert!(ink(&text) > 1000, "the printable ASCII range is covered"); } }