From 270f20f96dd0e24d843498742faa002323164aba Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Fri, 6 Mar 2026 18:24:27 +0100 Subject: [PATCH 1/2] Ignore ascent metric in MonoFont conversion --- eg-font-converter/src/mono_font.rs | 52 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/eg-font-converter/src/mono_font.rs b/eg-font-converter/src/mono_font.rs index d6b9b9e..9777d3d 100644 --- a/eg-font-converter/src/mono_font.rs +++ b/eg-font-converter/src/mono_font.rs @@ -1,19 +1,19 @@ use std::{fs, io, ops::RangeInclusive, path::Path}; -use anyhow::{bail, Context, Result}; -use bdf_parser::{Encoding, Font, Glyph}; +use anyhow::{bail, ensure, Context, Result}; +use bdf_parser::{BoundingBox, Encoding, Font, Glyph}; use eg_bdf::BdfTextStyle; use embedded_graphics::{ image::ImageRaw, mono_font::{mapping::Mapping, DecorationDimensions, MonoFont}, pixelcolor::BinaryColor, prelude::*, - text::{Baseline, Text}, + text::Text, }; use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay}; use quote::{format_ident, quote}; -use crate::{ConvertedFont, EgBdfOutput}; +use crate::{eg_bdf_font::bounding_box_to_rectangle, ConvertedFont, EgBdfOutput}; /// Font conversion output for [`MonoFont`]. #[derive(Debug)] @@ -35,27 +35,12 @@ pub struct MonoFontOutput { impl MonoFontOutput { pub(crate) fn new(bdf: EgBdfOutput) -> Result { let font = bdf.as_font(); - let metrics = &bdf.font.bdf.metrics; let style = BdfTextStyle::new(&font, BinaryColor::On); let glyphs_per_row = 16; //TODO: make configurable let columns = glyphs_per_row; // TODO: allow smaller column count let rows = font.glyphs.len().div_ceil(glyphs_per_row); - let character_size = bdf.bounding_box().size; - let character_spacing = 0; - let baseline = metrics.ascent.saturating_sub(1); - let strikethrough = DecorationDimensions::new( - bdf.font.strikethrough_position, - bdf.font.strikethrough_thickness, - ); - let underline = - DecorationDimensions::new(bdf.font.underline_position, bdf.font.underline_thickness); - - let mut bitmap = SimulatorDisplay::new( - character_size.component_mul(Size::new(columns as u32, rows as u32)), - ); - let mapping = glyphs_to_mapping(&bdf.font.glyphs); // Rearrange the glyphs in the correct order if a mapping is used, @@ -80,9 +65,32 @@ impl MonoFontOutput { .collect() }; + let mut bounding_box = BoundingBox::default(); + for glyph in glyphs.iter() { + bounding_box = bounding_box.union(&glyph.bounding_box); + } + let bounding_box = bounding_box_to_rectangle(&bounding_box); + + let character_size = bounding_box.size; + let character_spacing = 0; + ensure!(bounding_box.top_left.y <= 0); + let baseline = -bounding_box.top_left.y as u32; + let strikethrough = DecorationDimensions::new( + bdf.font.strikethrough_position, + bdf.font.strikethrough_thickness, + ); + let underline = + DecorationDimensions::new(bdf.font.underline_position, bdf.font.underline_thickness); + + let mut bitmap = SimulatorDisplay::new( + character_size.component_mul(Size::new(columns as u32, rows as u32)), + ); + for (i, glyph) in glyphs.iter().enumerate() { - let x = (i % glyphs_per_row) as i32 * character_size.width as i32; - let y = (i / glyphs_per_row) as i32 * character_size.height as i32; + let (row, column) = (i / glyphs_per_row, i % glyphs_per_row); + let p = Point::zero() + + Size::new(column as u32, row as u32).component_mul(character_size) + - bounding_box.top_left; // TODO: assumes unicode let c = match glyph.encoding { @@ -90,7 +98,7 @@ impl MonoFontOutput { _ => bail!("invalid encoding: '{:?}'", glyph.encoding), }; - Text::with_baseline(&String::from(c), Point::new(x, y), style, Baseline::Top) + Text::new(&String::from(c), p, style) .draw(&mut bitmap) .unwrap(); } From b5af678a757bff397f1313eb395301910103810e Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Sun, 8 Mar 2026 19:58:07 +0100 Subject: [PATCH 2/2] Add glyph alignment setting --- eg-bdf-examples/examples/font_viewer.rs | 51 +++++++++++++++--- eg-font-converter/src/lib.rs | 26 +++++++++ eg-font-converter/src/mono_font.rs | 70 ++++++++++++++++++------- 3 files changed, 123 insertions(+), 24 deletions(-) diff --git a/eg-bdf-examples/examples/font_viewer.rs b/eg-bdf-examples/examples/font_viewer.rs index b791666..44b3080 100644 --- a/eg-bdf-examples/examples/font_viewer.rs +++ b/eg-bdf-examples/examples/font_viewer.rs @@ -4,7 +4,7 @@ use eg_font_converter::{FontConverter, Mapping}; use embedded_graphics::{ geometry::AnchorPoint, mono_font::{ascii::FONT_6X10, MonoTextStyle}, - pixelcolor::Rgb888, + pixelcolor::{Gray8, Rgb888}, prelude::*, primitives::{Line, PrimitiveStyle}, text::{renderer::TextRenderer, Alignment, Baseline, Text, TextStyleBuilder}, @@ -19,6 +19,40 @@ fn main() { } } +fn draw_character_bounding_boxes>( + display: &mut SimulatorDisplay, + text: &Text, +) where + S: Clone, +{ + let mut dummy_display = display.clone(); + + let mut p = text.position; + let mut toggle = false; + + for c in text.text.chars().map(|c| c.to_string()) { + if c == "\n" { + p.x = text.position.x; + } + + let t = Text { + text: &c, + position: p, + ..text.clone() + }; + + let fill_color = Gray8::new(if toggle { 30 } else { 50 }).into(); + + t.bounding_box() + .into_styled(PrimitiveStyle::with_fill(fill_color)) + .draw(display) + .unwrap(); + + p = t.draw(&mut dummy_display).unwrap(); + toggle = !toggle; + } +} + /// Draws a text and its bounding box. fn draw_text>( display: &mut SimulatorDisplay, @@ -36,6 +70,7 @@ fn draw + Copy>( display: &mut SimulatorDisplay, style: S, line_height: u32, + highlight_characters: bool, ) { let abc = ('a'..='z').collect::(); let digits = ('0'..='9').collect::(); @@ -54,6 +89,9 @@ fn draw + Copy>( .unwrap(); let text = Text::new(&string, position, style); + if highlight_characters { + draw_character_bounding_boxes(display, &text); + } draw_text(display, &text); let position = display.bounding_box().anchor_point(AnchorPoint::BottomLeft) @@ -138,6 +176,7 @@ fn try_main() -> Result<()> { let mut window = Window::new("Font viewer", &settings); let mut use_mono_font = false; + let mut highlight_characters = false; 'main_loop: loop { window.update(&display); @@ -145,9 +184,9 @@ fn try_main() -> Result<()> { for event in window.events() { match event { SimulatorEvent::KeyDown { keycode, .. } => match keycode { - Keycode::M => { - use_mono_font = !use_mono_font; - } + Keycode::M => use_mono_font = !use_mono_font, + Keycode::C => highlight_characters = !highlight_characters, + Keycode::Q => break 'main_loop, _ => {} }, SimulatorEvent::Quit => break 'main_loop, @@ -160,12 +199,12 @@ fn try_main() -> Result<()> { display.clear(Rgb888::BLACK).unwrap(); if use_mono_font { let style = MonoTextStyle::new(&mono_font, Rgb888::WHITE); - draw(&mut display, style, line_height); + draw(&mut display, style, line_height, highlight_characters); hint.insert_str(0, "Mono | "); } else { let style = BdfTextStyle::new(&bdf_font, Rgb888::WHITE); - draw(&mut display, style, line_height); + draw(&mut display, style, line_height, highlight_characters); hint.insert_str(0, "Bdf | "); } diff --git a/eg-font-converter/src/lib.rs b/eg-font-converter/src/lib.rs index cd2375d..82a1333 100644 --- a/eg-font-converter/src/lib.rs +++ b/eg-font-converter/src/lib.rs @@ -107,6 +107,7 @@ pub struct FontConverter<'a> { data_file_extension: String, data_file_path: Option, comments: Vec, + glyph_alignment: GlyphAlignment, glyphs: BTreeSet, missing_glyph_substitute: Option, @@ -134,6 +135,7 @@ impl<'a> FontConverter<'a> { data_file_extension: "data".to_string(), data_file_path: None, comments: Vec::new(), + glyph_alignment: GlyphAlignment::Center, glyphs: BTreeSet::new(), missing_glyph_substitute: None, } @@ -252,6 +254,17 @@ impl<'a> FontConverter<'a> { self } + /// Set the glyph alignment in `MonoFont`s. + /// + /// When a proportional font is converted into a `MonoFont` all glyphs have to use the same + /// bounding box, which matches the largest glyph in the included glyph subset. This setting + /// determines how smaller glyphs are aligned in this bounding box. + pub fn glyph_alignment(mut self, glyph_alignment: GlyphAlignment) -> Self { + self.glyph_alignment = glyph_alignment; + + self + } + fn convert(&self) -> Result { ensure!( is_valid_identifier(&self.name), @@ -325,6 +338,7 @@ impl<'a> FontConverter<'a> { data_file_extension: self.data_file_extension.clone(), data_file_path: self.data_file_path.clone(), comments: self.comments.clone(), + glyph_alignment: self.glyph_alignment, underline_position, underline_thickness, strikethrough_position, @@ -382,6 +396,7 @@ struct ConvertedFont { pub data_file_extension: String, pub data_file_path: Option, pub comments: Vec, + pub glyph_alignment: GlyphAlignment, pub glyphs: Vec, pub replacement_character: usize, @@ -516,6 +531,17 @@ impl Visibility { } } +/// Glyph alignment. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum GlyphAlignment { + /// Left aligned. + Left, + /// Center aligned. + Center, + /// Right aligned. + Right, +} + #[cfg(test)] mod tests { use super::*; diff --git a/eg-font-converter/src/mono_font.rs b/eg-font-converter/src/mono_font.rs index 9777d3d..630fb13 100644 --- a/eg-font-converter/src/mono_font.rs +++ b/eg-font-converter/src/mono_font.rs @@ -1,19 +1,20 @@ use std::{fs, io, ops::RangeInclusive, path::Path}; use anyhow::{bail, ensure, Context, Result}; -use bdf_parser::{BoundingBox, Encoding, Font, Glyph}; +use bdf_parser::{BoundingBox, Coord, Encoding, Font, Glyph}; use eg_bdf::BdfTextStyle; use embedded_graphics::{ image::ImageRaw, mono_font::{mapping::Mapping, DecorationDimensions, MonoFont}, pixelcolor::BinaryColor, prelude::*, + primitives::Rectangle, text::Text, }; use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay}; use quote::{format_ident, quote}; -use crate::{eg_bdf_font::bounding_box_to_rectangle, ConvertedFont, EgBdfOutput}; +use crate::{eg_bdf_font::bounding_box_to_rectangle, ConvertedFont, EgBdfOutput, GlyphAlignment}; /// Font conversion output for [`MonoFont`]. #[derive(Debug)] @@ -32,6 +33,29 @@ pub struct MonoFontOutput { mapping: Option, } +fn determine_glyph_metrics<'a>(glyphs: impl Iterator) -> (Rectangle, u32) { + // Default to 1x1 pixel bounding box at the origin. + // This ensures that the relative positioning of glyphs that don't touch the baseline is + // maintained in case only this kind of glyph is included. + let mut bounding_box = BoundingBox { + offset: Coord::new(0, 0), + size: Coord::new(1, 1), + }; + + let mut max_dwidth = 0; + for glyph in glyphs { + bounding_box = bounding_box.union(&glyph.bounding_box); + if let Some(width) = glyph.width_horizontal { + max_dwidth = max_dwidth.max(width.device.x); + } + } + + ( + bounding_box_to_rectangle(&bounding_box), + max_dwidth.try_into().unwrap(), + ) +} + impl MonoFontOutput { pub(crate) fn new(bdf: EgBdfOutput) -> Result { let font = bdf.as_font(); @@ -65,16 +89,14 @@ impl MonoFontOutput { .collect() }; - let mut bounding_box = BoundingBox::default(); - for glyph in glyphs.iter() { - bounding_box = bounding_box.union(&glyph.bounding_box); - } - let bounding_box = bounding_box_to_rectangle(&bounding_box); + ensure!(!glyphs.is_empty(), "Font must contain at least one glyph"); + + let (bounding_box, max_dwidth) = determine_glyph_metrics(glyphs.iter()); let character_size = bounding_box.size; - let character_spacing = 0; - ensure!(bounding_box.top_left.y <= 0); - let baseline = -bounding_box.top_left.y as u32; + let character_spacing = max_dwidth.saturating_sub(character_size.width); + + let baseline: u32 = (-bounding_box.top_left.y).try_into().unwrap(); let strikethrough = DecorationDimensions::new( bdf.font.strikethrough_position, bdf.font.strikethrough_thickness, @@ -87,20 +109,32 @@ impl MonoFontOutput { ); for (i, glyph) in glyphs.iter().enumerate() { - let (row, column) = (i / glyphs_per_row, i % glyphs_per_row); - let p = Point::zero() - + Size::new(column as u32, row as u32).component_mul(character_size) - - bounding_box.top_left; - // TODO: assumes unicode let c = match glyph.encoding { Encoding::Standard(index) => char::from_u32(index).unwrap(), _ => bail!("invalid encoding: '{:?}'", glyph.encoding), }; - Text::new(&String::from(c), p, style) - .draw(&mut bitmap) - .unwrap(); + let (row, column) = (i / glyphs_per_row, i % glyphs_per_row); + + let glyph_position = + Point::zero() + Size::new(column as u32, row as u32).component_mul(character_size); + let baseline_offset = Point::new(-glyph.bounding_box.offset.x, baseline as i32); + let alignment_offset = match bdf.font.glyph_alignment { + GlyphAlignment::Left => 0, + GlyphAlignment::Center => { + (character_size.width as i32 - glyph.bounding_box.size.x) / 2 + } + GlyphAlignment::Right => character_size.width as i32 - glyph.bounding_box.size.x, + }; + + Text::new( + &String::from(c), + glyph_position + baseline_offset + Point::new(alignment_offset, 0), + style, + ) + .draw(&mut bitmap) + .unwrap(); } let data = bitmap.to_be_bytes();