From 3d60c4527c6c74a33014b08da0741da5fc400844 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Mon, 7 Jul 2025 20:26:44 +0200 Subject: [PATCH] Add PNG specimen output to teset-bdf-parser --- eg-font-converter/src/lib.rs | 3 +- tools/test-bdf-parser/Cargo.toml | 6 + tools/test-bdf-parser/src/lib.rs | 126 +++++++++++++--- tools/test-bdf-parser/src/main.rs | 150 ++++++++++++++++---- tools/test-bdf-parser/tests/tecate_suite.rs | 26 +--- tools/test-bdf-parser/tests/u8g2_suite.rs | 42 ++---- 6 files changed, 247 insertions(+), 106 deletions(-) diff --git a/eg-font-converter/src/lib.rs b/eg-font-converter/src/lib.rs index f476d30..a298e11 100644 --- a/eg-font-converter/src/lib.rs +++ b/eg-font-converter/src/lib.rs @@ -448,12 +448,13 @@ impl GlyphMapping for ConvertedFont { // TODO: assumes unicode let encoding = Encoding::Standard(c as u32); + // TODO: support replacement character self.glyphs .iter() .enumerate() .find(|(_, glyph)| glyph.encoding == encoding) .map(|(index, _)| index) - .unwrap() + .unwrap_or_default() } } diff --git a/tools/test-bdf-parser/Cargo.toml b/tools/test-bdf-parser/Cargo.toml index e6b4742..e6616f9 100644 --- a/tools/test-bdf-parser/Cargo.toml +++ b/tools/test-bdf-parser/Cargo.toml @@ -6,4 +6,10 @@ edition = "2018" [dependencies] bdf-parser = { path = "../../bdf-parser" } +eg-bdf = { path = "../../eg-bdf" } +eg-font-converter = { path = "../../eg-font-converter" } +owo-colors = "4.2.2" clap = { version = "4.5.40", features = [ "derive" ] } +anyhow = "1.0.98" +embedded-graphics = "0.8.1" +embedded-graphics-simulator = { version = "0.7.0", default-features = false } diff --git a/tools/test-bdf-parser/src/lib.rs b/tools/test-bdf-parser/src/lib.rs index fa7b547..ad19518 100644 --- a/tools/test-bdf-parser/src/lib.rs +++ b/tools/test-bdf-parser/src/lib.rs @@ -1,41 +1,123 @@ +use anyhow::Result; +use owo_colors::OwoColorize; use std::{ + ffi::OsStr, fs, io, path::{Path, PathBuf}, }; -use bdf_parser::Font; +use bdf_parser::{Font, ParserError}; -pub fn collect_font_files(dir: &Path) -> io::Result> { - let mut files = Vec::new(); +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct FontPath { + pub absolute: PathBuf, + pub relative: PathBuf, +} + +#[derive(Debug)] +pub struct FontFile { + pub path: FontPath, + pub parsed: Result, +} + +#[derive(Debug, Default)] +struct DirWalker { + files: Vec, + + prefix: PathBuf, +} + +impl DirWalker { + fn new bool>(path: &Path, filter: F) -> io::Result { + let mut self_ = Self::default(); + + self_.walk(path, &filter, true)?; + self_.files.sort(); + + Ok(self_) + } + + fn walk bool>( + &mut self, + path: &Path, + filter: &F, + root: bool, + ) -> io::Result<()> { + let file_name = path.file_name().unwrap(); + + if path.is_dir() { + let old_prefix = self.prefix.clone(); + if !root { + self.prefix.push(file_name); + } - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); + for entry in fs::read_dir(path)? { + let entry = entry?; - if path.is_file() && path.to_string_lossy().ends_with(".bdf") { - files.push(path.to_path_buf()); - } else if path.is_dir() { - let sub = collect_font_files(&path).unwrap(); - for subfile in sub { - files.push(subfile); - } + self.walk(&entry.path(), filter, false)?; } + + self.prefix = old_prefix; + } else if path.is_file() { + if path.extension() == Some(OsStr::new("bdf")) && filter(path) { + self.files.push(FontPath { + absolute: path.to_path_buf(), + relative: self.prefix.join(file_name), + }); + } + } else { + panic!("path is not a dir or file"); } + + Ok(()) } +} + +pub fn parse_fonts(path: &Path) -> Result> { + parse_fonts_with_filter(path, |_| true) +} + +pub fn parse_fonts_with_filter bool>( + path: &Path, + filter: F, +) -> Result> { + let paths = DirWalker::new(path, filter).map(|walker| walker.files)?; - files.sort(); + let files = paths + .into_iter() + .map(|path| { + let bdf = std::fs::read(&path.absolute).unwrap(); + let str = String::from_utf8_lossy(&bdf); + let parsed = Font::parse(&str); + + FontFile { path, parsed } + }) + .collect::>(); Ok(files) } -pub fn test_font_parse(filepath: &Path) -> Result<(), String> { - let bdf = std::fs::read(filepath).unwrap(); - let str = String::from_utf8_lossy(&bdf); - let font = Font::parse(&str); +pub fn print_parser_result(files: &[FontFile]) -> usize { + let mut num_errors = 0; + + for font_file in files { + if font_file.parsed.is_err() { + num_errors += 1; + } - match font { - Ok(_font) => Ok(()), - Err(e) => Err(e.to_string()), + print!("{0: <60}", font_file.path.relative.to_string_lossy()); + match &font_file.parsed { + Ok(_font) => println!("{}", "OK".green()), + Err(e) => println!("{} {:}", "Error:".red(), e), + } } + + println!( + "\n{} out of {} fonts passed ({} failed)\n", + files.len() - num_errors, + files.len(), + num_errors + ); + + num_errors } diff --git a/tools/test-bdf-parser/src/main.rs b/tools/test-bdf-parser/src/main.rs index 819c3ba..ccc8ad6 100644 --- a/tools/test-bdf-parser/src/main.rs +++ b/tools/test-bdf-parser/src/main.rs @@ -1,47 +1,141 @@ use clap::Parser; -use std::path::PathBuf; +use eg_bdf::BdfTextStyle; +use eg_font_converter::FontConverter; +use embedded_graphics::{ + mono_font::MonoTextStyle, + pixelcolor::Rgb888, + prelude::*, + primitives::{Line, PrimitiveStyle, StyledDrawable}, + text::{renderer::TextRenderer, Baseline, Text}, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; +use owo_colors::OwoColorize; +use std::{fs, path::PathBuf}; use test_bdf_parser::*; #[derive(Parser)] struct Arguments { + /// Output directory for font specimens in PNG format. + #[arg(long)] + png_out: Option, + + /// Output scale for PNG images. + #[arg(long, default_value = "1")] + png_scale: u32, + + /// Path to a BDF file or a directory containing BDF files. file_or_directory: PathBuf, } -pub fn main() { - let args: Arguments = Arguments::parse(); +fn draw_specimen(style: impl TextRenderer + Copy) -> SimulatorDisplay { + let single_line = Text::with_baseline( + "The quick brown fox jumps over the lazy dog.", + Point::zero(), + style, + Baseline::Top, + ); + + // 10 px minimum line height to ensure output even if metrics are wrong. + let single_line_height = single_line.bounding_box().size.height.max(10); + + let display_height = single_line_height * 3; + let display_width = (single_line.bounding_box().size.width + 10).max(display_height); + + let text_position = Point::new(5, single_line_height as i32); - if args.file_or_directory.is_dir() { - let fonts = - collect_font_files(&args.file_or_directory).expect("Could not get list of fonts"); + let mut display = SimulatorDisplay::::new(Size::new(display_width, display_height)); - let results = fonts.iter().map(|fpath| test_font_parse(fpath)); + // Draw baseline grid - let mut num_errors = 0; + for offset in [Point::zero(), Point::new(0, single_line_height as i32)] { + Line::with_delta( + text_position.y_axis() + offset, + Point::new(display_width as i32, 0), + ) + .draw_styled( + &PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_SLATE_GRAY, 1), + &mut display, + ) + .unwrap(); + } + + // Draw marker for X start position + + Line::with_delta(text_position.x_axis(), Point::new(0, display_height as i32)) + .draw_styled( + &PrimitiveStyle::with_stroke(Rgb888::CSS_DARK_SLATE_GRAY, 1), + &mut display, + ) + .unwrap(); + + let text = Text::new( + "The quick brown fox jumps over the lazy dog.\n0123456789", + text_position, + style, + ); - for (font, result) in fonts.iter().zip(results) { - if result.is_err() { - num_errors += 1; - } + // Draw bounding box + text.bounding_box() + .draw_styled( + &PrimitiveStyle::with_stroke(Rgb888::CSS_LIGHT_SLATE_GRAY, 1), + &mut display, + ) + .unwrap(); + + text.draw(&mut display).unwrap(); + + display +} + +pub fn main() { + let args: Arguments = Arguments::parse(); + + let fonts = parse_fonts(&args.file_or_directory).expect("Could not parse fonts"); + let num_errors = print_parser_result(&fonts); + + let output_settings = OutputSettingsBuilder::new().scale(args.png_scale).build(); + + if let Some(png_directory) = args.png_out { + for file in fonts.iter().filter(|file| file.parsed.is_ok()) { println!( - "{0: <60} {1:?}", - font.file_name().unwrap().to_str().unwrap(), - result + "Generating specimen: {}", + file.path.relative.to_string_lossy() ); - } - println!( - "\n{} out of {} fonts passed ({} failed)\n", - (fonts.len() - num_errors), - fonts.len(), - num_errors - ); - - assert_eq!(num_errors, 0, "Not all font files parsed successfully"); - } else if args.file_or_directory.is_file() { - test_font_parse(&args.file_or_directory).unwrap(); - } else { - panic!("Invalid path: {:?}", args.file_or_directory); + let output_file = png_directory.join(&file.path.relative); + let output_dir = output_file.parent().unwrap(); + + fs::create_dir_all(output_dir).unwrap(); + + let converter = FontConverter::with_file(&file.path.absolute, "FONT"); + + match converter.convert_eg_bdf() { + Ok(converted_bdf) => { + let bdf_specimen = + draw_specimen(BdfTextStyle::new(&converted_bdf.as_font(), Rgb888::WHITE)); + bdf_specimen + .to_rgb_output_image(&output_settings) + .save_png(output_file.with_extension("bdf.png")) + .unwrap(); + } + Err(e) => println!("{} {e}", "Error (eg-bdf):".red()), + }; + + match converter.convert_mono_font() { + Ok(converted_mono) => { + let mono_specimen = + draw_specimen(MonoTextStyle::new(&converted_mono.as_font(), Rgb888::WHITE)); + mono_specimen + .to_rgb_output_image(&output_settings) + .save_png(output_file.with_extension("mono.png")) + .unwrap(); + } + Err(e) => println!("{} {e}", "Error (mono):".red()), + }; + } } + + assert_eq!(num_errors, 0, "Not all font files parsed successfully"); } diff --git a/tools/test-bdf-parser/tests/tecate_suite.rs b/tools/test-bdf-parser/tests/tecate_suite.rs index d8e3afc..6b0f807 100644 --- a/tools/test-bdf-parser/tests/tecate_suite.rs +++ b/tools/test-bdf-parser/tests/tecate_suite.rs @@ -8,30 +8,8 @@ fn it_parses_all_tecate_fonts() { .canonicalize() .unwrap(); - let fonts = collect_font_files(&fontdir).expect("Could not get list of fonts"); - - let results = fonts.iter().map(|fpath| test_font_parse(fpath)); - - let mut num_errors = 0; - - for (font, result) in fonts.iter().zip(results) { - if result.is_err() { - num_errors += 1; - } - - println!( - "{0: <60} {1:?}", - font.file_name().unwrap().to_str().unwrap(), - result - ); - } - - println!( - "\n{} out of {} fonts passed ({} failed)\n", - (fonts.len() - num_errors), - fonts.len(), - num_errors - ); + let fonts = parse_fonts(&fontdir).expect("Could not parse fonts"); + let num_errors = print_parser_result(&fonts); assert_eq!(num_errors, 0, "Not all font files parsed successfully"); } diff --git a/tools/test-bdf-parser/tests/u8g2_suite.rs b/tools/test-bdf-parser/tests/u8g2_suite.rs index cffba09..ab89241 100644 --- a/tools/test-bdf-parser/tests/u8g2_suite.rs +++ b/tools/test-bdf-parser/tests/u8g2_suite.rs @@ -1,43 +1,23 @@ -use std::{path::Path, ffi::OsStr}; +use std::path::Path; use test_bdf_parser::*; +fn filter(path: &Path) -> bool { + match path.file_name().unwrap().to_str().unwrap() { + "u8x8extra.bdf" => false, // broken header + "emoticons21.bdf" => false, // invalid COPYRIGHT property in line 7 + _ => true, + } +} + #[test] fn it_parses_all_u8g2_fonts() { let fontdir = Path::new("../../target/fonts/u8g2/tools/font/bdf") .canonicalize() .unwrap(); - let fonts = collect_font_files(&fontdir).expect("Could not get list of u8g2 fonts"); - - let results = fonts - .iter() - // u8x8extra.bdf has a broken header - .filter(|path| path.file_name() != Some(OsStr::new("u8x8extra.bdf"))) - // emoticons21.bdf has an invalid COPYRIGHT property in line 7 - .filter(|path| path.file_name() != Some(OsStr::new("emoticons21.bdf"))) - .map(|fpath| test_font_parse(fpath)); - - let mut num_errors = 0; - - for (font, result) in fonts.iter().zip(results) { - if result.is_err() { - num_errors += 1; - } - - println!( - "{0: <30} {1:?}", - font.file_name().unwrap().to_str().unwrap(), - result - ); - } - - println!( - "\n{} out of {} fonts passed ({} failed)\n", - (fonts.len() - num_errors), - fonts.len(), - num_errors - ); + let fonts = parse_fonts_with_filter(&fontdir, filter).expect("Could not parse fonts"); + let num_errors = print_parser_result(&fonts); assert_eq!(num_errors, 0, "Not all font files parsed successfully"); }