diff --git a/bdf-parser/src/glyph.rs b/bdf-parser/src/glyph.rs index e788797..f38947f 100644 --- a/bdf-parser/src/glyph.rs +++ b/bdf-parser/src/glyph.rs @@ -293,6 +293,10 @@ impl Glyphs { } } + if glyphs.is_empty() { + return Err(ParserError::new("no CHARS in font")); + } + Ok(Self { glyphs }) } @@ -315,6 +319,32 @@ impl Glyphs { pub fn iter(&self) -> impl Iterator { self.glyphs.iter() } + + /// Approximates the ascent. + /// + /// See section 8.2.1 FONT_ASCENT in https://www.x.org/docs/XLFD/xlfd.pdf. + pub(crate) fn approximate_ascent(&self) -> u32 { + self.glyphs + .iter() + .map(|glyph| glyph.bounding_box.size.y - glyph.bounding_box.offset.y) + .max() + .unwrap_or_default() + .try_into() + .unwrap() + } + + /// Approximates the descent. + /// + /// See section 8.2.2 FONT_DESCENT in https://www.x.org/docs/XLFD/xlfd.pdf. + pub(crate) fn approximate_descent(&self) -> u32 { + self.glyphs + .iter() + .map(|glyph| -glyph.bounding_box.offset.y) + .max() + .unwrap_or_default() + .try_into() + .unwrap() + } } #[cfg(test)] diff --git a/bdf-parser/src/lib.rs b/bdf-parser/src/lib.rs index 9829d72..be8da27 100644 --- a/bdf-parser/src/lib.rs +++ b/bdf-parser/src/lib.rs @@ -12,7 +12,7 @@ mod properties; pub use glyph::{Encoding, Glyph, Glyphs}; pub use metadata::{Metadata, MetricsSet}; pub use parser::ParserError; -pub use properties::{Properties, Property, PropertyError, PropertyType}; +pub use properties::{Properties, Property, PropertyType}; use crate::parser::{Line, Lines}; @@ -24,6 +24,9 @@ pub struct Font { /// Glyphs. pub glyphs: Glyphs, + + /// Metrics. + pub metrics: Metrics, } impl Font { @@ -44,8 +47,13 @@ impl Font { let metadata = Metadata::parse(&mut lines)?; let glyphs = Glyphs::parse(&mut lines, &metadata)?; + let metrics = Metrics::new(&metadata, &glyphs)?; - Ok(Font { metadata, glyphs }) + Ok(Font { + metadata, + glyphs, + metrics, + }) } } @@ -133,6 +141,39 @@ impl Coord { } } +/// Metrics. +#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct Metrics { + /// Ascent above the baseline in pixels. + pub ascent: u32, + + /// Descent above the baseline in pixels. + pub descent: u32, +} + +impl Metrics { + fn new(metadata: &Metadata, glyphs: &Glyphs) -> Result { + let ascent = metadata + .properties + .try_get::(Property::FontAscent) + .map_err(|_| ParserError::new("invalid value for FONT_ASCENT property"))? + .unwrap_or_else(|| glyphs.approximate_ascent()); + + let descent = metadata + .properties + .try_get::(Property::FontDescent) + .map_err(|_| ParserError::new("invalid value for FONT_DESCENT property"))? + .unwrap_or_else(|| glyphs.approximate_descent()); + + Ok(Self { ascent, descent }) + } + + /// Gets the line height in pixels. + pub const fn line_height(&self) -> u32 { + self.ascent + self.descent + } +} + #[cfg(test)] mod tests { use crate::{glyph::GlyphWidth, properties::PropertyValue}; diff --git a/bdf-parser/src/metadata.rs b/bdf-parser/src/metadata.rs index 45213ca..8fe4e14 100644 --- a/bdf-parser/src/metadata.rs +++ b/bdf-parser/src/metadata.rs @@ -138,7 +138,10 @@ mod tests { FONTBOUNDINGBOX 0 1 2 3 SIZE 1 2 3 COMMENT "comment" - CHARS 0 + CHARS 1 + STARTCHAR 0 + BITMAP + ENDCHAR ENDFONT "#}; @@ -193,7 +196,10 @@ mod tests { SIZE 1 2 3 COMMENT "comment" METRICSSET 2 - CHARS 0 + CHARS 1 + STARTCHAR 0 + BITMAP + ENDCHAR ENDFONT "#}; diff --git a/bdf-parser/src/properties.rs b/bdf-parser/src/properties.rs index 4cc4e7b..3ea7638 100644 --- a/bdf-parser/src/properties.rs +++ b/bdf-parser/src/properties.rs @@ -176,19 +176,25 @@ impl Properties { /// Tries to get a property. /// - /// Returns an error if the property doesn't exist or the value has the wrong type. - pub fn try_get(&self, property: Property) -> Result { + /// Returns `None` if the property doesn't exits and an error if the value has the wrong type. + pub fn try_get( + &self, + property: Property, + ) -> Result, PropertyTypeError> { self.try_get_by_name(&property.to_string()) } /// Tries to get a property by name. /// - /// Returns an error if the property doesn't exist or the value has the wrong type. - pub fn try_get_by_name(&self, name: &str) -> Result { + /// Returns `None` if the property doesn't exits and an error if the value has the wrong type. + pub fn try_get_by_name( + &self, + name: &str, + ) -> Result, PropertyTypeError> { self.properties .get(name) - .ok_or_else(|| PropertyError::Undefined(name.to_string())) - .and_then(TryFrom::try_from) + .map(|value| value.try_into()) + .transpose() } /// Returns `true` if no properties exist. @@ -200,12 +206,13 @@ impl Properties { /// Marker trait for property value types. pub trait PropertyType where - Self: for<'a> TryFrom<&'a PropertyValue, Error = PropertyError>, + Self: for<'a> TryFrom<&'a PropertyValue, Error = PropertyTypeError>, { } impl PropertyType for String {} impl PropertyType for i32 {} +impl PropertyType for u32 {} #[derive(Debug, Clone, PartialEq, Eq)] pub enum PropertyValue { @@ -214,38 +221,43 @@ pub enum PropertyValue { } impl TryFrom<&PropertyValue> for String { - type Error = PropertyError; + type Error = PropertyTypeError; fn try_from(value: &PropertyValue) -> Result { match value { PropertyValue::Text(text) => Ok(text.clone()), - _ => Err(PropertyError::WrongType), + _ => Err(PropertyTypeError), } } } impl TryFrom<&PropertyValue> for i32 { - type Error = PropertyError; + type Error = PropertyTypeError; fn try_from(value: &PropertyValue) -> Result { match value { PropertyValue::Int(int) => Ok(*int), - _ => Err(PropertyError::WrongType), + _ => Err(PropertyTypeError), } } } -/// Error returned by property getters. -#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)] -pub enum PropertyError { - /// Undefined property. - #[error("property \"{0}\" is undefined")] - Undefined(String), - /// Wrong property type. - #[error("wrong property type")] - WrongType, +impl TryFrom<&PropertyValue> for u32 { + type Error = PropertyTypeError; + + fn try_from(value: &PropertyValue) -> Result { + match value { + PropertyValue::Int(int) if *int >= 0 => Ok(*int as u32), + _ => Err(PropertyTypeError), + } + } } +/// Invalid property type error. +#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord)] +#[error("invalid property type")] +pub struct PropertyTypeError; + #[cfg(test)] mod tests { use super::*; @@ -271,7 +283,7 @@ mod tests { ] { assert_eq!( properties.try_get_by_name::(key).unwrap(), - expected.to_string(), + Some(expected.to_string()), "key=\"{key}\"" ); } @@ -289,16 +301,19 @@ mod tests { let mut lines = Lines::new(INPUT); let properties = Properties::parse(&mut lines).unwrap(); - for (key, expected) in [ - ("POS_INT", 10), // - ("NEG_INT", -20), - ] { - assert_eq!( - properties.try_get_by_name::(key).unwrap(), - expected, - "key=\"{key}\"" - ); - } + assert_eq!(properties.try_get_by_name::("POS_INT"), Ok(Some(10))); + assert_eq!(properties.try_get_by_name::("NEG_INT"), Ok(Some(-20))); + + assert_eq!(properties.try_get_by_name::("POS_INT"), Ok(Some(10))); + assert_eq!( + properties.try_get_by_name::("NEG_INT"), + Err(PropertyTypeError) + ); + + assert_eq!( + properties.try_get_by_name::("POS_INT"), + Err(PropertyTypeError) + ); } #[test] diff --git a/eg-bdf-examples/examples/font_viewer.rs b/eg-bdf-examples/examples/font_viewer.rs index 1fae14a..b791666 100644 --- a/eg-bdf-examples/examples/font_viewer.rs +++ b/eg-bdf-examples/examples/font_viewer.rs @@ -105,19 +105,19 @@ fn try_main() -> Result<()> { .nth(1) .ok_or_else(|| anyhow!("missing filename"))?; - let converted_font = FontConverter::with_file(&file, "BDF_FILE") + let converter = FontConverter::with_file(&file, "BDF_FILE") .glyphs(Mapping::Ascii) .missing_glyph_substitute('?'); - let output = converted_font + let bdf_output = converter .convert_eg_bdf() .with_context(|| "couldn't convert font")?; - let bdf_font = output.as_font(); + let bdf_font = bdf_output.as_font(); - let output = converted_font + let mono_output = converter .convert_mono_font() .with_context(|| "couldn't convert font")?; - let mono_font = output.as_font(); + let mono_font = mono_output.as_font(); let hints_style = MonoTextStyle::new(&FONT_6X10, Rgb888::CSS_DIM_GRAY); let bottom_right = TextStyleBuilder::new() @@ -125,7 +125,6 @@ fn try_main() -> Result<()> { .alignment(Alignment::Right) .build(); - // TODO: add metrics getter let line_height = bdf_font.ascent + bdf_font.descent; let display_height = line_height * 8; let display_width = (line_height * 25).max(display_height); diff --git a/eg-font-converter/src/eg_bdf_font.rs b/eg-font-converter/src/eg_bdf_font.rs index 919f6a8..2b1513d 100644 --- a/eg-font-converter/src/eg_bdf_font.rs +++ b/eg-font-converter/src/eg_bdf_font.rs @@ -1,7 +1,7 @@ use std::{fs, io, path::Path}; use anyhow::Result; -use bdf_parser::{BoundingBox, Encoding}; +use bdf_parser::{BoundingBox, Encoding, Metrics}; use bitvec::{prelude::*, vec::BitVec}; use eg_bdf::{BdfFont, BdfGlyph}; use embedded_graphics::{ @@ -84,11 +84,14 @@ impl EgBdfOutput { let constant_name = format_ident!("{}", self.font.name); let data_file = self.font.data_file().to_string_lossy().to_string(); let ConvertedFont { + bdf, replacement_character, - ascent, - descent, .. - } = self.font; + } = &self.font; + + let Metrics { + ascent, descent, .. + } = bdf.metrics; let glyphs = self.glyphs.iter().map(|glyph| { let BdfGlyph { @@ -150,10 +153,12 @@ impl EgBdfOutput { /// Returns the converted font as a [`BdfFont`]. pub fn as_font(&self) -> BdfFont<'_> { + let metrics = &self.font.bdf.metrics; + BdfFont { replacement_character: self.font.replacement_character, - ascent: self.font.ascent, - descent: self.font.descent, + ascent: metrics.ascent, + descent: metrics.descent, glyphs: &self.glyphs, data: self.data(), } diff --git a/eg-font-converter/src/lib.rs b/eg-font-converter/src/lib.rs index a298e11..3021c54 100644 --- a/eg-font-converter/src/lib.rs +++ b/eg-font-converter/src/lib.rs @@ -72,7 +72,7 @@ #![deny(unsafe_code)] use anyhow::{anyhow, ensure, Context, Result}; -use bdf_parser::{Encoding, Font, Glyph, Property}; +use bdf_parser::{Encoding, Font, Glyph}; use embedded_graphics::mono_font::mapping::GlyphMapping; use std::{ collections::BTreeSet, @@ -305,27 +305,10 @@ impl<'a> FontConverter<'a> { .collect::, _>>()? }; - // TODO: handle missing (incorrect?) properties - let ascent = bdf - .metadata - .properties - .try_get::(Property::FontAscent) - .ok() - .filter(|v| *v >= 0) - .unwrap_or_default() as u32; //TODO: convert to error - - let descent = bdf - .metadata - .properties - .try_get::(Property::FontDescent) - .ok() - .filter(|v| *v >= 0) - .unwrap_or_default() as u32; //TODO: convert to error - // TODO: read from BDF and use correct fallbacks (https://www.x.org/docs/XLFD/xlfd.pdf 3.2.30) - let underline_position = ascent + 1; + let underline_position = bdf.metrics.ascent + 1; let underline_thickness = 1; - let strikethrough_position = (ascent + descent) / 2; + let strikethrough_position = bdf.metrics.line_height() / 2; let strikethrough_thickness = 1; let mut font = ConvertedFont { @@ -339,8 +322,6 @@ impl<'a> FontConverter<'a> { data_file_extension: self.data_file_extension.clone(), data_file_path: self.data_file_path.clone(), comments: self.comments.clone(), - ascent, - descent, underline_position, underline_thickness, strikethrough_position, @@ -402,9 +383,6 @@ struct ConvertedFont { pub glyphs: Vec, pub replacement_character: usize, - pub ascent: u32, - pub descent: u32, - pub underline_position: u32, pub underline_thickness: u32, pub strikethrough_position: u32, diff --git a/eg-font-converter/src/mono_font.rs b/eg-font-converter/src/mono_font.rs index 62cc934..d6b9b9e 100644 --- a/eg-font-converter/src/mono_font.rs +++ b/eg-font-converter/src/mono_font.rs @@ -35,6 +35,7 @@ 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 @@ -43,7 +44,7 @@ impl MonoFontOutput { let character_size = bdf.bounding_box().size; let character_spacing = 0; - let baseline = bdf.font.ascent.saturating_sub(1); + let baseline = metrics.ascent.saturating_sub(1); let strikethrough = DecorationDimensions::new( bdf.font.strikethrough_position, bdf.font.strikethrough_thickness, diff --git a/tools/test-bdf-parser/src/main.rs b/tools/test-bdf-parser/src/main.rs index ccc8ad6..7d2e69d 100644 --- a/tools/test-bdf-parser/src/main.rs +++ b/tools/test-bdf-parser/src/main.rs @@ -36,10 +36,9 @@ fn draw_specimen(style: impl TextRenderer + Copy) -> SimulatorDi 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 single_line_height = single_line.bounding_box().size.height; - let display_height = single_line_height * 3; + let display_height = (single_line_height * 3).max(10); let display_width = (single_line.bounding_box().size.width + 10).max(display_height); let text_position = Point::new(5, single_line_height as i32);