From 067fb51d539750c7f704086b149c13ea2886bc63 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 18:41:11 +0200 Subject: [PATCH 01/12] Fix clippy warning --- eg-bdf/src/serialized.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index e811f1d..87c4063 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -121,7 +121,7 @@ impl<'a> SerializedBdfFont<'a> { }, }, device_width: u32::from(kerning), - bitmap_data: &self + bitmap_data: self .data .get((self.header_size() + data_index as usize)..)?, }) From bc52e40523be754a0dddc8f3faf94771b9311a07 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 18:48:21 +0200 Subject: [PATCH 02/12] Improve SerializedBdfFont docs --- eg-bdf/src/serialized.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index 87c4063..c671c28 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -13,22 +13,29 @@ const fn get_be_u32(data: &[u8], idx: usize) -> u32 { u32::from_be_bytes([data[idx], data[idx + 1], data[idx + 2], data[idx + 3]]) } +/// Serialized BDF font. +/// +/// # Binary Format +/// +/// All integers are stored in big endian byte order. +/// /// * Header (12 Bytes): -/// - Ascent (pixels, u16 BE) -/// - Descent (pixels, u16 BE) -/// - Replacement Character (index into character table, u32 BE) -/// - Character Table Length (entries, u32 BE) +/// * Ascent (pixels, `u16`) +/// * Descent (pixels, `u16`) +/// * Replacement Character (index into character table, `u32`) +/// * Character Table Length (number of entries, `u32`) /// /// * Glyph Table (17 Bytes Per Entry): -/// - corresponding codepoint (u32 BE) -/// - top_left.x (i16 BE) -/// - top_left.y (i16 BE) -/// - size.width (u16 BE) -/// - size.height (u16 BE) -/// - device_width (pixels, u8) -/// - data index (bytes from start of data, u32 BE) +/// * corresponding codepoint (`u32`) +/// * `top_left.x` (`i16`) +/// * `top_left.y` (`i16`) +/// * `size.width` (`u16`) +/// * `size.height` (`u16`) +/// * `device_width` (pixels, `u8`) +/// * data index (bytes from start of data, `u32`) /// -/// Font bitmap data is stored afterwards +/// The bitmap data for all glyphs is stored after the header and glyph table. Each glyph bitmap +/// starts at a byte boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SerializedBdfFont<'a> { /// The raw u8 data of the serialized font From 6a417d01f5eede8173f8c66570e2cb4c93cbe429 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 19:26:44 +0200 Subject: [PATCH 03/12] Use glyph instead of character and fix possible panics --- eg-bdf/src/serialized.rs | 79 +++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index c671c28..0bb756f 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -23,7 +23,7 @@ const fn get_be_u32(data: &[u8], idx: usize) -> u32 { /// * Ascent (pixels, `u16`) /// * Descent (pixels, `u16`) /// * Replacement Character (index into character table, `u32`) -/// * Character Table Length (number of entries, `u32`) +/// * Glyph Table Length (number of entries, `u32`) /// /// * Glyph Table (17 Bytes Per Entry): /// * corresponding codepoint (`u32`) @@ -41,18 +41,19 @@ pub struct SerializedBdfFont<'a> { /// The raw u8 data of the serialized font data: &'a [u8], } + impl<'a> SerializedBdfFont<'a> { // Structure Sizes const HEADER_SIZE: usize = 12; - const CHARACTER_TABLE_ENTRY_SIZE: usize = 17; + const GLYPH_TABLE_ENTRY_SIZE: usize = 17; // Offsets for the header const ASCENT_OFFSET: usize = 0; const DESCENT_OFFSET: usize = 2; const REPLACEMENT_OFFSET: usize = 4; - const CHAR_TABLE_LEN_OFFSET: usize = 8; + const GLYPH_COUNT_OFFSET: usize = 8; - // Offsets for the character table entries + // Offsets for the glyph table entries const CODEPOINT_OFFSET: usize = 0; const TOPLEFTX_OFFSET: usize = 4; const TOPLEFTY_OFFSET: usize = 6; @@ -61,20 +62,18 @@ impl<'a> SerializedBdfFont<'a> { const KERN_OFFSET: usize = 12; const IDX_OFFSET: usize = 13; - const fn character_table_data( + const fn glyph_data( &self, index: usize, - ) -> Option<&[u8; SerializedBdfFont::CHARACTER_TABLE_ENTRY_SIZE]> { + ) -> Option<&[u8; SerializedBdfFont::GLYPH_TABLE_ENTRY_SIZE]> { let (_header, data) = self .data - .split_at(Self::HEADER_SIZE + (index * Self::CHARACTER_TABLE_ENTRY_SIZE)); + .split_at(Self::HEADER_SIZE + (index * Self::GLYPH_TABLE_ENTRY_SIZE)); data.first_chunk() } /// Verifies data in a way that prevents panics and returns a serialized font if the data is valid - /// - /// TODO: Make this const - pub fn new(data: &'a [u8]) -> Result { + pub const fn new(data: &'a [u8]) -> Result { // No header if data.len() < Self::HEADER_SIZE { return Err("No header"); @@ -83,9 +82,7 @@ impl<'a> SerializedBdfFont<'a> { // Safe to construct a font and index its header let font = Self { data }; - // Character table length invalid - - // Character table too small + // Glyph table too small if data.len() < font.header_size() { return Err("No metadata"); } @@ -94,29 +91,35 @@ impl<'a> SerializedBdfFont<'a> { Ok(font) } - /// Returns the length of the glyph table - pub const fn character_count(self) -> u32 { - get_be_u32(self.data, Self::CHAR_TABLE_LEN_OFFSET) + /// Returns the number of glyphs. + pub const fn glyph_count(self) -> usize { + get_be_u32(self.data, Self::GLYPH_COUNT_OFFSET) as usize } /// Returns the offset of the data block const fn header_size(self) -> usize { - Self::HEADER_SIZE + ((self.character_count() as usize) * Self::CHARACTER_TABLE_ENTRY_SIZE) + Self::HEADER_SIZE + self.glyph_count() * Self::GLYPH_TABLE_ENTRY_SIZE } - /// Returns a BdfGlyph in the glyph table - pub fn character_table(self, idx: usize) -> Option> { - let ctd = self.character_table_data(idx)?; - let corresponding_character = char::from_u32(get_be_u32(ctd, Self::CODEPOINT_OFFSET)); - let top_left_x = get_be_i16(ctd, Self::TOPLEFTX_OFFSET); - let top_left_y = get_be_i16(ctd, Self::TOPLEFTY_OFFSET); - let width = get_be_u16(ctd, Self::SIZEX_OFFSET); - let height = get_be_u16(ctd, Self::SIZEY_OFFSET); - let kerning = ctd[Self::KERN_OFFSET]; - let data_index = get_be_u32(ctd, Self::IDX_OFFSET); + /// Gets the glyph for the given index. + /// + /// Returns [`None`] if the index is out of bounds. + pub fn lookup_by_index(self, index: usize) -> Option> { + let glyph = self.glyph_data(index)?; + let corresponding_character = char::from_u32(get_be_u32(glyph, Self::CODEPOINT_OFFSET))?; + let top_left_x = get_be_i16(glyph, Self::TOPLEFTX_OFFSET); + let top_left_y = get_be_i16(glyph, Self::TOPLEFTY_OFFSET); + let width = get_be_u16(glyph, Self::SIZEX_OFFSET); + let height = get_be_u16(glyph, Self::SIZEY_OFFSET); + let kerning = glyph[Self::KERN_OFFSET]; + + let data_index = get_be_u32(glyph, Self::IDX_OFFSET); + let bitmap_data = self + .data + .get((self.header_size() + data_index as usize)..)?; Some(DisplayBdfGlyph { - character: corresponding_character.unwrap(), + character: corresponding_character, bounding_box: Rectangle { top_left: Point { x: i32::from(top_left_x), @@ -128,9 +131,7 @@ impl<'a> SerializedBdfFont<'a> { }, }, device_width: u32::from(kerning), - bitmap_data: self - .data - .get((self.header_size() + data_index as usize)..)?, + bitmap_data: bitmap_data, }) } } @@ -146,18 +147,20 @@ impl<'a> ProportionalFont<'a> for SerializedBdfFont<'a> { fn replacement_glyph(&self) -> DisplayBdfGlyph<'_> { let rpos = get_be_u32(self.data, Self::REPLACEMENT_OFFSET); - self.character_table(rpos as usize) + self.lookup_by_index(rpos as usize) .expect("Replacement character isn't valid") } fn lookup(&self, c: char) -> Option> { // TODO, make this a binary search - for i in 0..(self.character_count() as usize) { - let tested_character = self - .character_table(i) - .expect("Character table is corrupted"); - if tested_character.character == c { - return Some(tested_character); + for i in 0..self.glyph_count() { + let Some(glyph) = self.lookup_by_index(i) else { + // skip invalid glyphs + continue; + }; + + if glyph.character == c { + return Some(glyph); } } From f20daf6529a1d21c92a27ef655f7e5a6e5d66b13 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 20:57:53 +0200 Subject: [PATCH 04/12] Add basic serialized font test --- eg-bdf/src/serialized.rs | 183 ++++++++++++++++++++++++++++++--------- 1 file changed, 143 insertions(+), 40 deletions(-) diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index 0bb756f..5e58796 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -1,6 +1,25 @@ use crate::{DisplayBdfGlyph, ProportionalFont, ProportionalTextStyle}; use embedded_graphics::{prelude::*, primitives::Rectangle}; +// Structure Sizes +const HEADER_SIZE: usize = 12; +const GLYPH_TABLE_ENTRY_SIZE: usize = 17; + +// Offsets for the header +const ASCENT_OFFSET: usize = 0; +const DESCENT_OFFSET: usize = 2; +const REPLACEMENT_OFFSET: usize = 4; +const GLYPH_COUNT_OFFSET: usize = 8; + +// Offsets for the glyph table entries +const CODEPOINT_OFFSET: usize = 0; +const TOPLEFTX_OFFSET: usize = 4; +const TOPLEFTY_OFFSET: usize = 6; +const SIZEX_OFFSET: usize = 8; +const SIZEY_OFFSET: usize = 10; +const KERN_OFFSET: usize = 12; +const IDX_OFFSET: usize = 13; + const fn get_be_i16(data: &[u8], idx: usize) -> i16 { i16::from_be_bytes([data[idx], data[idx + 1]]) } @@ -43,39 +62,17 @@ pub struct SerializedBdfFont<'a> { } impl<'a> SerializedBdfFont<'a> { - // Structure Sizes - const HEADER_SIZE: usize = 12; - const GLYPH_TABLE_ENTRY_SIZE: usize = 17; - - // Offsets for the header - const ASCENT_OFFSET: usize = 0; - const DESCENT_OFFSET: usize = 2; - const REPLACEMENT_OFFSET: usize = 4; - const GLYPH_COUNT_OFFSET: usize = 8; - - // Offsets for the glyph table entries - const CODEPOINT_OFFSET: usize = 0; - const TOPLEFTX_OFFSET: usize = 4; - const TOPLEFTY_OFFSET: usize = 6; - const SIZEX_OFFSET: usize = 8; - const SIZEY_OFFSET: usize = 10; - const KERN_OFFSET: usize = 12; - const IDX_OFFSET: usize = 13; - - const fn glyph_data( - &self, - index: usize, - ) -> Option<&[u8; SerializedBdfFont::GLYPH_TABLE_ENTRY_SIZE]> { + const fn glyph_data(&self, index: usize) -> Option<&[u8; GLYPH_TABLE_ENTRY_SIZE]> { let (_header, data) = self .data - .split_at(Self::HEADER_SIZE + (index * Self::GLYPH_TABLE_ENTRY_SIZE)); + .split_at(HEADER_SIZE + (index * GLYPH_TABLE_ENTRY_SIZE)); data.first_chunk() } /// Verifies data in a way that prevents panics and returns a serialized font if the data is valid pub const fn new(data: &'a [u8]) -> Result { // No header - if data.len() < Self::HEADER_SIZE { + if data.len() < HEADER_SIZE { return Err("No header"); } @@ -93,12 +90,12 @@ impl<'a> SerializedBdfFont<'a> { /// Returns the number of glyphs. pub const fn glyph_count(self) -> usize { - get_be_u32(self.data, Self::GLYPH_COUNT_OFFSET) as usize + get_be_u32(self.data, GLYPH_COUNT_OFFSET) as usize } /// Returns the offset of the data block const fn header_size(self) -> usize { - Self::HEADER_SIZE + self.glyph_count() * Self::GLYPH_TABLE_ENTRY_SIZE + HEADER_SIZE + self.glyph_count() * GLYPH_TABLE_ENTRY_SIZE } /// Gets the glyph for the given index. @@ -106,14 +103,14 @@ impl<'a> SerializedBdfFont<'a> { /// Returns [`None`] if the index is out of bounds. pub fn lookup_by_index(self, index: usize) -> Option> { let glyph = self.glyph_data(index)?; - let corresponding_character = char::from_u32(get_be_u32(glyph, Self::CODEPOINT_OFFSET))?; - let top_left_x = get_be_i16(glyph, Self::TOPLEFTX_OFFSET); - let top_left_y = get_be_i16(glyph, Self::TOPLEFTY_OFFSET); - let width = get_be_u16(glyph, Self::SIZEX_OFFSET); - let height = get_be_u16(glyph, Self::SIZEY_OFFSET); - let kerning = glyph[Self::KERN_OFFSET]; - - let data_index = get_be_u32(glyph, Self::IDX_OFFSET); + let corresponding_character = char::from_u32(get_be_u32(glyph, CODEPOINT_OFFSET))?; + let top_left_x = get_be_i16(glyph, TOPLEFTX_OFFSET); + let top_left_y = get_be_i16(glyph, TOPLEFTY_OFFSET); + let width = get_be_u16(glyph, SIZEX_OFFSET); + let height = get_be_u16(glyph, SIZEY_OFFSET); + let kerning = glyph[KERN_OFFSET]; + + let data_index = get_be_u32(glyph, IDX_OFFSET); let bitmap_data = self .data .get((self.header_size() + data_index as usize)..)?; @@ -135,18 +132,19 @@ impl<'a> SerializedBdfFont<'a> { }) } } + impl<'a> ProportionalFont<'a> for SerializedBdfFont<'a> { fn metrics(&self) -> crate::proportional::Metrics { crate::proportional::Metrics { - ascent: u32::from(get_be_u16(self.data, Self::ASCENT_OFFSET)), - descent: u32::from(get_be_u16(self.data, Self::DESCENT_OFFSET)), - line_height: u32::from(get_be_u16(self.data, Self::ASCENT_OFFSET)) - + u32::from(get_be_u16(self.data, Self::DESCENT_OFFSET)), + ascent: u32::from(get_be_u16(self.data, ASCENT_OFFSET)), + descent: u32::from(get_be_u16(self.data, DESCENT_OFFSET)), + line_height: u32::from(get_be_u16(self.data, ASCENT_OFFSET)) + + u32::from(get_be_u16(self.data, DESCENT_OFFSET)), } } fn replacement_glyph(&self) -> DisplayBdfGlyph<'_> { - let rpos = get_be_u32(self.data, Self::REPLACEMENT_OFFSET); + let rpos = get_be_u32(self.data, REPLACEMENT_OFFSET); self.lookup_by_index(rpos as usize) .expect("Replacement character isn't valid") } @@ -170,3 +168,108 @@ impl<'a> ProportionalFont<'a> for SerializedBdfFont<'a> { /// Stylized serialized BDF text pub type SerializedBdfTextStyle<'a, C> = ProportionalTextStyle<'a, SerializedBdfFont<'a>, C>; + +#[cfg(test)] +mod tests { + use embedded_graphics::{mock_display::MockDisplay, pixelcolor::BinaryColor, text::Text}; + + use crate::proportional::Metrics; + + use super::*; + + #[rustfmt::skip] + const TEST_FONT: &[u8] = &[ + // header + 0x00, 0x04, // ascent = 4 + 0x00, 0x01, // descent = 1 + 0x00, 0x00, 0x00, 0x01, // replacement glyph at index 1 + 0x00, 0x00, 0x00, 0x02, // 2 glyphs + + // 'a' glyph + 0x00, 0x00, 0x00, b'a', // codepoint + 0x00, 0x00, // top_left_x = 0 + 0xFF, 0xFD, // top_left_y = -3 + 0x00, 0x04, // size_x = 4 + 0x00, 0x04, // size_y = 4 + 0x06, // device_width = 6 + 0x00, 0x00, 0x00, 0x00, // bitmap offset + + // 'b' glyph + 0x00, 0x00, 0x00, b'b', // codepoint + 0x00, 0x00, // top_left_x = 0 + 0xFF, 0xFC, // top_left_y = -4 + 0x00, 0x05, // size_x = 5 + 0x00, 0x05, // size_y = 5 + 0x05, // device_width = 5 + 0x00, 0x00, 0x00, 4 * 4 / 8, // bitmap offset + + // glyph bitmaps + 0b11111001, 0b10011111, + 0b00001_000, 0b10_00100_0, 0b1000_1000, 0b0_0000000, + ]; + + #[test] + fn metadata() { + let font = SerializedBdfFont::new(TEST_FONT).unwrap(); + + assert_eq!( + font.metrics(), + Metrics { + ascent: 4, + descent: 1, + line_height: 5 + } + ); + + assert_eq!(font.glyph_count(), 2); + + assert!(font.lookup_by_index(0).is_some()); + assert!(font.lookup_by_index(1).is_some()); + assert!(font.lookup_by_index(2).is_none()); + + let _a = font.lookup('a').unwrap(); + let b = font.lookup('b').unwrap(); + assert_eq!(font.lookup('c'), None); + + let replacement = font.replacement_glyph(); + assert_eq!(replacement, b); + } + + fn draw_text(font: &SerializedBdfFont, text: &str) -> MockDisplay { + let text_style = SerializedBdfTextStyle::new(font, BinaryColor::On); + + let mut display = MockDisplay::new(); + Text::new(text, Point::new(0, 4), text_style) + .draw(&mut display) + .unwrap(); + + display + } + + #[test] + fn draw() { + let font = SerializedBdfFont::new(TEST_FONT).unwrap(); + draw_text(&font, "ab?").assert_pattern(&[ + " # #", + "#### # # ", + "# # # # ", + "# # # # ", + "#### # # ", + ]); + } + + #[test] + fn truncated_font() { + assert_eq!(SerializedBdfFont::new(&[]), Err("No header")); + assert_eq!( + SerializedBdfFont::new(&TEST_FONT[0..HEADER_SIZE]), + Err("No metadata") + ); + + // TODO: check presence of replacement glyph in `new` + // let no_glyph_bitmaps_font = + // SerializedBdfFont::new(&TEST_FONT[0..HEADER_SIZE + 2 * GLYPH_TABLE_ENTRY_SIZE]) + // .unwrap(); + // draw_text(&no_glyph_bitmaps_font, "abc").assert_pattern(&[]); + } +} From ccd6b379252be438c6e5a98c000f2f030381701a Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:16:02 +0200 Subject: [PATCH 05/12] Remove panic if replacement glyph isn't available in serialized font --- eg-bdf/src/proportional.rs | 62 +++++++++++++++++++++++--------------- eg-bdf/src/serialized.rs | 17 +++++------ 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/eg-bdf/src/proportional.rs b/eg-bdf/src/proportional.rs index d35b37c..133fb39 100644 --- a/eg-bdf/src/proportional.rs +++ b/eg-bdf/src/proportional.rs @@ -15,29 +15,28 @@ pub struct Metrics { pub line_height: u32, } -/// A proportional font -pub trait ProportionalFont<'a>: Clone { - /// Returns a struct containing ascent, descent, baseline_offset, and line_height - fn metrics(&self) -> Metrics; - /// Finds a BdfGlyph for a character - fn lookup(&self, c: char) -> Option>; - /// Finds the replacement glyph - fn replacement_glyph(&'a self) -> DisplayBdfGlyph<'a>; - - /// Returns the baseline offset +impl Metrics { + /// Returns the baseline offset. fn baseline_offset(&self, baseline: Baseline) -> i32 { match baseline { - Baseline::Top => self.metrics().ascent.saturating_sub(1) as i32, - Baseline::Bottom => -(self.metrics().descent as i32), - Baseline::Middle => (self.metrics().ascent as i32 - self.metrics().descent as i32) / 2, + Baseline::Top => self.ascent.saturating_sub(1) as i32, + Baseline::Bottom => -(self.descent as i32), + Baseline::Middle => (self.ascent as i32 - self.descent as i32) / 2, Baseline::Alphabetic => 0, } } +} - /// Returns a glyph, or a replacement character if no corresponding glyph exists - fn glyph_or_replacement(&'a self, c: char) -> DisplayBdfGlyph<'a> { - self.lookup(c).unwrap_or(self.replacement_glyph()) - } +/// A proportional font +pub trait ProportionalFont<'a>: Clone { + /// Returns the font metrics. + fn metrics(&self) -> Metrics; + + /// Finds the glyph for the given character. + fn lookup(&self, c: char) -> Option>; + + /// Returns the replacement glyph + fn replacement_glyph(&self) -> Option>; } impl<'a> ProportionalFont<'a> for BdfFont<'a> { @@ -49,8 +48,8 @@ impl<'a> ProportionalFont<'a> for BdfFont<'a> { } } - fn replacement_glyph(&'a self) -> DisplayBdfGlyph<'a> { - self.glyphs[self.replacement_character].into_glyph(self) + fn replacement_glyph(&self) -> Option> { + Some(self.glyphs[self.replacement_character].into_glyph(self)) } fn lookup(&self, c: char) -> Option> { @@ -104,10 +103,18 @@ impl<'a, C: PixelColor, F: ProportionalFont<'a>> TextRenderer for ProportionalTe where D: DrawTarget, { - let mut position = position + Point::new(0, self.font.baseline_offset(baseline)); + let metrics = self.font.metrics(); + let mut position = position + Point::new(0, metrics.baseline_offset(baseline)); for c in text.chars() { - let glyph = self.font.glyph_or_replacement(c); + let Some(glyph) = self + .font + .lookup(c) + .or_else(|| self.font.replacement_glyph()) + else { + // skip invalid characters entirely if replacement glyph isn't available + continue; + }; glyph.draw(position, self.color, target)?; @@ -127,17 +134,24 @@ impl<'a, C: PixelColor, F: ProportionalFont<'a>> TextRenderer for ProportionalTe where D: DrawTarget, { - let position = position + Point::new(0, self.font.baseline_offset(baseline)); + let metrics = self.font.metrics(); + let position = position + Point::new(0, metrics.baseline_offset(baseline)); Ok(position + Size::new(width, 0)) } fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics { - let position = position + Point::new(0, self.font.baseline_offset(baseline)); + let metrics = self.font.metrics(); + let position = position + Point::new(0, metrics.baseline_offset(baseline)); let dx = text .chars() - .map(|c| self.font.glyph_or_replacement(c).device_width) + .filter_map(|c| { + self.font + .lookup(c) + .or_else(|| self.font.replacement_glyph()) + .map(|g| g.device_width) + }) .sum(); // TODO: calculate correct bounding box diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index 5e58796..4bc96ed 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -128,7 +128,7 @@ impl<'a> SerializedBdfFont<'a> { }, }, device_width: u32::from(kerning), - bitmap_data: bitmap_data, + bitmap_data, }) } } @@ -143,10 +143,9 @@ impl<'a> ProportionalFont<'a> for SerializedBdfFont<'a> { } } - fn replacement_glyph(&self) -> DisplayBdfGlyph<'_> { + fn replacement_glyph(&self) -> Option> { let rpos = get_be_u32(self.data, REPLACEMENT_OFFSET); self.lookup_by_index(rpos as usize) - .expect("Replacement character isn't valid") } fn lookup(&self, c: char) -> Option> { @@ -232,7 +231,7 @@ mod tests { assert_eq!(font.lookup('c'), None); let replacement = font.replacement_glyph(); - assert_eq!(replacement, b); + assert_eq!(replacement, Some(b)); } fn draw_text(font: &SerializedBdfFont, text: &str) -> MockDisplay { @@ -266,10 +265,10 @@ mod tests { Err("No metadata") ); - // TODO: check presence of replacement glyph in `new` - // let no_glyph_bitmaps_font = - // SerializedBdfFont::new(&TEST_FONT[0..HEADER_SIZE + 2 * GLYPH_TABLE_ENTRY_SIZE]) - // .unwrap(); - // draw_text(&no_glyph_bitmaps_font, "abc").assert_pattern(&[]); + // drawing a font without any glyph bitmap data shouldn't panic + let no_glyph_bitmaps_font = + SerializedBdfFont::new(&TEST_FONT[0..HEADER_SIZE + 2 * GLYPH_TABLE_ENTRY_SIZE]) + .unwrap(); + draw_text(&no_glyph_bitmaps_font, "abc").assert_pattern(&[]); } } From 4f2aa57208b41886ea8e9144018ac188eacb8095 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:17:54 +0200 Subject: [PATCH 06/12] Move ProportionalFont impl for BdfFont into lib.rs --- eg-bdf/src/lib.rs | 24 ++++++++++++++++++++++++ eg-bdf/src/proportional.rs | 25 ++----------------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/eg-bdf/src/lib.rs b/eg-bdf/src/lib.rs index 4c88f9a..8efa4de 100644 --- a/eg-bdf/src/lib.rs +++ b/eg-bdf/src/lib.rs @@ -25,6 +25,8 @@ mod serialized; pub use proportional::{ProportionalFont, ProportionalTextStyle}; pub use serialized::{SerializedBdfFont, SerializedBdfTextStyle}; +use crate::proportional::Metrics; + /// BDF font. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BdfFont<'a> { @@ -64,6 +66,28 @@ impl<'a> BdfGlyph { } } +impl<'a> ProportionalFont<'a> for BdfFont<'a> { + fn metrics(&self) -> Metrics { + Metrics { + ascent: self.ascent, + descent: self.descent, + line_height: self.ascent + self.descent, + } + } + + fn replacement_glyph(&self) -> Option> { + Some(self.glyphs[self.replacement_character].into_glyph(self)) + } + + fn lookup(&self, c: char) -> Option> { + if let Some(&g) = self.glyphs.iter().find(|g| g.character == c) { + Some(g.into_glyph(self)) + } else { + None + } + } +} + /// Unserialized BDF text style pub type BdfTextStyle<'a, C> = ProportionalTextStyle<'a, BdfFont<'a>, C>; diff --git a/eg-bdf/src/proportional.rs b/eg-bdf/src/proportional.rs index 133fb39..494bf4d 100644 --- a/eg-bdf/src/proportional.rs +++ b/eg-bdf/src/proportional.rs @@ -1,4 +1,3 @@ -use crate::{BdfFont, DisplayBdfGlyph}; use embedded_graphics::{ prelude::*, primitives::Rectangle, @@ -8,6 +7,8 @@ use embedded_graphics::{ }, }; +use crate::DisplayBdfGlyph; + #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub struct Metrics { pub ascent: u32, @@ -39,28 +40,6 @@ pub trait ProportionalFont<'a>: Clone { fn replacement_glyph(&self) -> Option>; } -impl<'a> ProportionalFont<'a> for BdfFont<'a> { - fn metrics(&self) -> Metrics { - Metrics { - ascent: self.ascent, - descent: self.descent, - line_height: self.ascent + self.descent, - } - } - - fn replacement_glyph(&self) -> Option> { - Some(self.glyphs[self.replacement_character].into_glyph(self)) - } - - fn lookup(&self, c: char) -> Option> { - if let Some(&g) = self.glyphs.iter().find(|g| g.character == c) { - Some(g.into_glyph(self)) - } else { - None - } - } -} - /// A generalized text style for proportional fonts #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProportionalTextStyle<'a, F: ProportionalFont<'a>, C: PixelColor> { From b039d3796df09c3097a95a727ae1eee1f7146c20 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:21:31 +0200 Subject: [PATCH 07/12] Change type device width field in serialized fonts to u16 The size of a glyph's bitmap is stored as a u16 and the device width must be able to span the range of values. --- eg-bdf/src/serialized.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/eg-bdf/src/serialized.rs b/eg-bdf/src/serialized.rs index 4bc96ed..4bc6526 100644 --- a/eg-bdf/src/serialized.rs +++ b/eg-bdf/src/serialized.rs @@ -3,7 +3,7 @@ use embedded_graphics::{prelude::*, primitives::Rectangle}; // Structure Sizes const HEADER_SIZE: usize = 12; -const GLYPH_TABLE_ENTRY_SIZE: usize = 17; +const GLYPH_TABLE_ENTRY_SIZE: usize = 18; // Offsets for the header const ASCENT_OFFSET: usize = 0; @@ -17,8 +17,8 @@ const TOPLEFTX_OFFSET: usize = 4; const TOPLEFTY_OFFSET: usize = 6; const SIZEX_OFFSET: usize = 8; const SIZEY_OFFSET: usize = 10; -const KERN_OFFSET: usize = 12; -const IDX_OFFSET: usize = 13; +const DEVICE_WIDTH_OFFSET: usize = 12; +const IDX_OFFSET: usize = 14; const fn get_be_i16(data: &[u8], idx: usize) -> i16 { i16::from_be_bytes([data[idx], data[idx + 1]]) @@ -44,13 +44,13 @@ const fn get_be_u32(data: &[u8], idx: usize) -> u32 { /// * Replacement Character (index into character table, `u32`) /// * Glyph Table Length (number of entries, `u32`) /// -/// * Glyph Table (17 Bytes Per Entry): +/// * Glyph Table (18 Bytes Per Entry): /// * corresponding codepoint (`u32`) /// * `top_left.x` (`i16`) /// * `top_left.y` (`i16`) /// * `size.width` (`u16`) /// * `size.height` (`u16`) -/// * `device_width` (pixels, `u8`) +/// * `device_width` (pixels, `u16`) /// * data index (bytes from start of data, `u32`) /// /// The bitmap data for all glyphs is stored after the header and glyph table. Each glyph bitmap @@ -108,7 +108,7 @@ impl<'a> SerializedBdfFont<'a> { let top_left_y = get_be_i16(glyph, TOPLEFTY_OFFSET); let width = get_be_u16(glyph, SIZEX_OFFSET); let height = get_be_u16(glyph, SIZEY_OFFSET); - let kerning = glyph[KERN_OFFSET]; + let device_width = get_be_u16(glyph, DEVICE_WIDTH_OFFSET); let data_index = get_be_u32(glyph, IDX_OFFSET); let bitmap_data = self @@ -127,7 +127,7 @@ impl<'a> SerializedBdfFont<'a> { height: u32::from(height), }, }, - device_width: u32::from(kerning), + device_width: u32::from(device_width), bitmap_data, }) } @@ -190,7 +190,7 @@ mod tests { 0xFF, 0xFD, // top_left_y = -3 0x00, 0x04, // size_x = 4 0x00, 0x04, // size_y = 4 - 0x06, // device_width = 6 + 0x00, 0x06, // device_width = 6 0x00, 0x00, 0x00, 0x00, // bitmap offset // 'b' glyph @@ -199,7 +199,7 @@ mod tests { 0xFF, 0xFC, // top_left_y = -4 0x00, 0x05, // size_x = 5 0x00, 0x05, // size_y = 5 - 0x05, // device_width = 5 + 0x00, 0x05, // device_width = 5 0x00, 0x00, 0x00, 4 * 4 / 8, // bitmap offset // glyph bitmaps From fe208583dd6bbdba59d9fa49df6086eb427a9ce3 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:36:25 +0200 Subject: [PATCH 08/12] Reenable eg_bdf_az test --- eg-font-converter/tests/build.rs | 4 +- eg-font-converter/tests/expected/eg_bdf_az.rs | 50 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/eg-font-converter/tests/build.rs b/eg-font-converter/tests/build.rs index 65e4359..cce9ed8 100644 --- a/eg-font-converter/tests/build.rs +++ b/eg-font-converter/tests/build.rs @@ -36,12 +36,12 @@ fn assert_data(data: &[u8], expected: &[u8], width: u32) { #[test] fn eg_bdf_az() { - /* let font = FontConverter::with_file("../eg-bdf-examples/examples/6x10.bdf", "EG_BDF_AZ") + let font = FontConverter::with_file("../eg-bdf-examples/examples/6x10.bdf", "EG_BDF_AZ") .glyphs('a'..='z') .convert_eg_bdf() .unwrap(); - assert_eq!(font.rust(), include_str!("expected/eg_bdf_az.rs")); */ + assert_eq!(font.rust(), include_str!("expected/eg_bdf_az.rs")); } #[test] diff --git a/eg-font-converter/tests/expected/eg_bdf_az.rs b/eg-font-converter/tests/expected/eg_bdf_az.rs index a875c1a..2f1db25 100644 --- a/eg-font-converter/tests/expected/eg_bdf_az.rs +++ b/eg-font-converter/tests/expected/eg_bdf_az.rs @@ -26,151 +26,151 @@ pub const EG_BDF_AZ: ::eg_bdf::BdfFont = { character: 'b', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 60usize, + start_index: 8usize, }, ::eg_bdf::BdfGlyph { character: 'c', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 120usize, + start_index: 16usize, }, ::eg_bdf::BdfGlyph { character: 'd', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 180usize, + start_index: 24usize, }, ::eg_bdf::BdfGlyph { character: 'e', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 240usize, + start_index: 32usize, }, ::eg_bdf::BdfGlyph { character: 'f', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 300usize, + start_index: 40usize, }, ::eg_bdf::BdfGlyph { character: 'g', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 360usize, + start_index: 48usize, }, ::eg_bdf::BdfGlyph { character: 'h', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 420usize, + start_index: 56usize, }, ::eg_bdf::BdfGlyph { character: 'i', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 480usize, + start_index: 64usize, }, ::eg_bdf::BdfGlyph { character: 'j', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 540usize, + start_index: 72usize, }, ::eg_bdf::BdfGlyph { character: 'k', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 600usize, + start_index: 80usize, }, ::eg_bdf::BdfGlyph { character: 'l', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 660usize, + start_index: 88usize, }, ::eg_bdf::BdfGlyph { character: 'm', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 720usize, + start_index: 96usize, }, ::eg_bdf::BdfGlyph { character: 'n', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 780usize, + start_index: 104usize, }, ::eg_bdf::BdfGlyph { character: 'o', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 840usize, + start_index: 112usize, }, ::eg_bdf::BdfGlyph { character: 'p', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 900usize, + start_index: 120usize, }, ::eg_bdf::BdfGlyph { character: 'q', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 960usize, + start_index: 128usize, }, ::eg_bdf::BdfGlyph { character: 'r', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1020usize, + start_index: 136usize, }, ::eg_bdf::BdfGlyph { character: 's', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1080usize, + start_index: 144usize, }, ::eg_bdf::BdfGlyph { character: 't', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1140usize, + start_index: 152usize, }, ::eg_bdf::BdfGlyph { character: 'u', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1200usize, + start_index: 160usize, }, ::eg_bdf::BdfGlyph { character: 'v', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1260usize, + start_index: 168usize, }, ::eg_bdf::BdfGlyph { character: 'w', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1320usize, + start_index: 176usize, }, ::eg_bdf::BdfGlyph { character: 'x', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1380usize, + start_index: 184usize, }, ::eg_bdf::BdfGlyph { character: 'y', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1440usize, + start_index: 192usize, }, ::eg_bdf::BdfGlyph { character: 'z', bounding_box: rect(0i32, -7i32, 6u32, 10u32), device_width: 6u32, - start_index: 1500usize, + start_index: 200usize, }, ], } From 9d2e31d2db75ed880a1de32396b45f375bf2da78 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:39:52 +0200 Subject: [PATCH 09/12] Fix font serializer after device_width type change --- eg-font-converter/src/serializer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eg-font-converter/src/serializer.rs b/eg-font-converter/src/serializer.rs index f1a128d..71cb330 100644 --- a/eg-font-converter/src/serializer.rs +++ b/eg-font-converter/src/serializer.rs @@ -25,7 +25,7 @@ pub fn serialize(font: BdfFont) -> anyhow::Result> { append_be_data!(glyph.bounding_box.top_left.y, i16); append_be_data!(glyph.bounding_box.size.width, u16); append_be_data!(glyph.bounding_box.size.height, u16); - append_be_data!(glyph.device_width, u8); + append_be_data!(glyph.device_width, u16); append_be_data!(glyph.start_index, u32); } From b8d3ea1fb01d58ee022b5a812aa8fa4ee7ab7b56 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:44:59 +0200 Subject: [PATCH 10/12] Default to unserialized BDF font in font viewer --- eg-bdf-examples/examples/font_viewer.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/eg-bdf-examples/examples/font_viewer.rs b/eg-bdf-examples/examples/font_viewer.rs index 989bb9e..5b0176c 100644 --- a/eg-bdf-examples/examples/font_viewer.rs +++ b/eg-bdf-examples/examples/font_viewer.rs @@ -57,7 +57,7 @@ fn draw + Copy>( draw_text(display, &text); let position = display.bounding_box().anchor_point(AnchorPoint::BottomLeft) - + Point::new(5, -(line_height as i32) * 3 / 2); + + Point::new(5, -(line_height as i32) * 3); Line::with_delta(position.y_axis(), Point::zero() + display.size().x_axis()) .into_styled(PrimitiveStyle::with_stroke(Rgb888::CSS_DIM_GRAY, 1)) @@ -130,7 +130,7 @@ fn try_main() -> Result<()> { .build(); let line_height = bdf_font.ascent + bdf_font.descent; - let display_height = line_height * 8; + let display_height = line_height * 10; let display_width = (line_height * 25).max(display_height); let display_size = Size::new(display_width, display_height); @@ -142,7 +142,7 @@ fn try_main() -> Result<()> { let mut window = Window::new("Font viewer", &settings); let mut use_mono_font = false; - let use_serialized_font = true; + let mut use_serialized_font = false; 'main_loop: loop { window.update(&display); @@ -153,6 +153,14 @@ fn try_main() -> Result<()> { Keycode::M => { use_mono_font = !use_mono_font; } + Keycode::S => { + if use_mono_font { + use_mono_font = false; + use_serialized_font = true; + } else { + use_serialized_font = !use_serialized_font; + } + } _ => {} }, SimulatorEvent::Quit => break 'main_loop, @@ -160,7 +168,7 @@ fn try_main() -> Result<()> { } } - let mut hint = "Press M to toggle".to_string(); + let mut hint = "Press M or S to toggle".to_string(); display.clear(Rgb888::BLACK).unwrap(); if use_mono_font { From cb6afac283c8010c0d86a46f731b7d650e3b4a5f Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 21:51:04 +0200 Subject: [PATCH 11/12] Make Metrics public Metrics is returned in the public trait ProportionalFont and should therefore also be public. --- eg-bdf/src/lib.rs | 4 +--- eg-bdf/src/proportional.rs | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/eg-bdf/src/lib.rs b/eg-bdf/src/lib.rs index 8efa4de..e772781 100644 --- a/eg-bdf/src/lib.rs +++ b/eg-bdf/src/lib.rs @@ -22,11 +22,9 @@ use embedded_graphics::{ mod proportional; mod serialized; -pub use proportional::{ProportionalFont, ProportionalTextStyle}; +pub use proportional::{ProportionalFont, ProportionalTextStyle, Metrics}; pub use serialized::{SerializedBdfFont, SerializedBdfTextStyle}; -use crate::proportional::Metrics; - /// BDF font. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct BdfFont<'a> { diff --git a/eg-bdf/src/proportional.rs b/eg-bdf/src/proportional.rs index 494bf4d..f47aa9f 100644 --- a/eg-bdf/src/proportional.rs +++ b/eg-bdf/src/proportional.rs @@ -9,10 +9,14 @@ use embedded_graphics::{ use crate::DisplayBdfGlyph; +/// Font metrics. #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub struct Metrics { + /// Ascent above the baseline in pixels. pub ascent: u32, + /// Descent below the baseline in pixels. pub descent: u32, + /// Line height in pixels. pub line_height: u32, } From daae6970411977086ba4fffd9e47eea57f56e905 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 28 Jun 2026 22:00:05 +0200 Subject: [PATCH 12/12] Reformat code --- eg-bdf/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eg-bdf/src/lib.rs b/eg-bdf/src/lib.rs index e772781..6ac9647 100644 --- a/eg-bdf/src/lib.rs +++ b/eg-bdf/src/lib.rs @@ -22,7 +22,7 @@ use embedded_graphics::{ mod proportional; mod serialized; -pub use proportional::{ProportionalFont, ProportionalTextStyle, Metrics}; +pub use proportional::{Metrics, ProportionalFont, ProportionalTextStyle}; pub use serialized::{SerializedBdfFont, SerializedBdfTextStyle}; /// BDF font.