Skip to main content

copper_bdf_parser/
lib.rs

1//! BDF parser.
2
3#![warn(missing_docs)]
4#![deny(unsafe_code)]
5#![deny(missing_debug_implementations)]
6
7mod glyph;
8mod metadata;
9mod parser;
10mod properties;
11
12pub use glyph::{Encoding, Glyph, Glyphs};
13pub use metadata::{Metadata, MetricsSet};
14pub use parser::ParserError;
15pub use properties::{Properties, Property, PropertyType};
16
17use crate::parser::{Line, Lines};
18
19/// BDF Font.
20#[derive(Debug, Clone, PartialEq)]
21pub struct Font {
22    /// Font metadata.
23    pub metadata: Metadata,
24
25    /// Glyphs.
26    pub glyphs: Glyphs,
27
28    /// Metrics.
29    pub metrics: Metrics,
30}
31
32impl Font {
33    /// Parses a BDF file.
34    pub fn parse(input: &str) -> Result<Self, ParserError> {
35        let mut lines = Lines::new(input);
36
37        let first_line = lines
38            .next()
39            .ok_or_else(|| ParserError::new("empty input"))?;
40
41        if first_line.keyword != "STARTFONT" || first_line.parameters != "2.1" {
42            return Err(ParserError::with_line(
43                "expected \"STARTFONT 2.1\"",
44                &first_line,
45            ));
46        }
47
48        let metadata = Metadata::parse(&mut lines)?;
49        let glyphs = Glyphs::parse(&mut lines, &metadata)?;
50        let metrics = Metrics::new(&metadata, &glyphs)?;
51
52        Ok(Font {
53            metadata,
54            glyphs,
55            metrics,
56        })
57    }
58}
59
60/// Bounding box.
61#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
62pub struct BoundingBox {
63    /// Offset to the lower left corner of the bounding box.
64    pub offset: Coord,
65
66    /// Size of the bounding box.
67    pub size: Coord,
68}
69
70impl BoundingBox {
71    pub(crate) fn parse(line: &Line<'_>) -> Option<Self> {
72        let [size_x, size_y, offset_x, offset_y] = line.parse_integer_parameters()?;
73
74        Some(Self {
75            offset: Coord::new(offset_x, offset_y),
76            size: Coord::new(size_x, size_y),
77        })
78    }
79
80    fn upper_right(&self) -> Coord {
81        Coord::new(
82            self.offset.x + self.size.x - 1,
83            self.offset.y + self.size.y - 1,
84        )
85    }
86
87    /// Calculates the smallest bounding box that surrounds two bounding boxes.
88    ///
89    /// # Panics
90    ///
91    /// Panics if any bounding box has a negative size.
92    pub fn union(&self, other: &Self) -> Self {
93        assert!(self.size.x >= 0);
94        assert!(self.size.y >= 0);
95        assert!(other.size.x >= 0);
96        assert!(other.size.y >= 0);
97
98        if other.size.x == 0 || other.size.y == 0 {
99            *self
100        } else if self.size.x == 0 || self.size.y == 0 {
101            *other
102        } else {
103            let self_ur = self.upper_right();
104            let other_ur = other.upper_right();
105
106            let x_min = self.offset.x.min(other.offset.x);
107            let y_min = self.offset.y.min(other.offset.y);
108            let x_max = self_ur.x.max(other_ur.x);
109            let y_max = self_ur.y.max(other_ur.y);
110
111            Self {
112                offset: Coord::new(x_min, y_min),
113                size: Coord::new(x_max - x_min + 1, y_max - y_min + 1),
114            }
115        }
116    }
117}
118
119/// Coordinate.
120///
121/// BDF files use a cartesian coordinate system, where the positive half-axis points upwards.
122#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
123pub struct Coord {
124    /// X coordinate.
125    pub x: i32,
126
127    /// Y coordinate.
128    pub y: i32,
129}
130
131impl Coord {
132    /// Creates a new coord.
133    pub const fn new(x: i32, y: i32) -> Self {
134        Self { x, y }
135    }
136
137    pub(crate) fn parse(line: &Line<'_>) -> Option<Self> {
138        let [x, y] = line.parse_integer_parameters()?;
139
140        Some(Self { x, y })
141    }
142}
143
144/// Metrics.
145#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
146pub struct Metrics {
147    /// Ascent above the baseline in pixels.
148    pub ascent: u32,
149
150    /// Descent above the baseline in pixels.
151    pub descent: u32,
152}
153
154impl Metrics {
155    fn new(metadata: &Metadata, glyphs: &Glyphs) -> Result<Self, ParserError> {
156        let ascent = metadata
157            .properties
158            .try_get::<u32>(Property::FontAscent)
159            .map_err(|_| ParserError::new("invalid value for FONT_ASCENT property"))?
160            .unwrap_or_else(|| glyphs.approximate_ascent());
161
162        let descent = metadata
163            .properties
164            .try_get::<u32>(Property::FontDescent)
165            .map_err(|_| ParserError::new("invalid value for FONT_DESCENT property"))?
166            .unwrap_or_else(|| glyphs.approximate_descent());
167
168        Ok(Self { ascent, descent })
169    }
170
171    /// Gets the line height in pixels.
172    pub const fn line_height(&self) -> u32 {
173        self.ascent + self.descent
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use crate::{glyph::GlyphWidth, properties::PropertyValue};
180
181    use super::*;
182    use indoc::indoc;
183
184    #[track_caller]
185    pub(crate) fn assert_parser_error(input: &str, message: &str, line_number: Option<usize>) {
186        assert_eq!(
187            Font::parse(input),
188            Err(ParserError {
189                message: message.to_string(),
190                line_number,
191            })
192        );
193    }
194    const FONT: &str = indoc! {r#"
195        STARTFONT 2.1
196        FONT "test font"
197        SIZE 16 75 75
198        FONTBOUNDINGBOX 16 24 0 0
199        STARTPROPERTIES 3
200        COPYRIGHT "Copyright123"
201        FONT_ASCENT 1
202        COMMENT comment
203        FONT_DESCENT 2
204        ENDPROPERTIES
205        CHARS 2
206        STARTCHAR Char 0
207        ENCODING 64
208        SWIDTH 480 0
209        DWIDTH 8 0
210        BBX 8 8 0 0
211        BITMAP
212        1f
213        01
214        ENDCHAR
215        STARTCHAR Char 1
216        ENCODING 65
217        SWIDTH 480 0
218        DWIDTH 8 0
219        BBX 8 8 0 0
220        BITMAP
221        2f
222        02
223        ENDCHAR
224        ENDFONT
225    "#};
226
227    fn test_font(font: &Font) {
228        assert_eq!(
229            font.metadata,
230            Metadata {
231                name: String::from("\"test font\""),
232                point_size: 16,
233                resolution: Coord::new(75, 75),
234                bounding_box: BoundingBox {
235                    size: Coord::new(16, 24),
236                    offset: Coord::new(0, 0),
237                },
238                metrics_set: MetricsSet::Horizontal,
239                properties: Properties::new(
240                    [
241                        (
242                            "COPYRIGHT".to_string(),
243                            PropertyValue::Text("Copyright123".to_string()),
244                        ),
245                        ("FONT_ASCENT".to_string(), PropertyValue::Int(1)),
246                        ("FONT_DESCENT".to_string(), PropertyValue::Int(2)),
247                    ]
248                    .into_iter()
249                    .collect(),
250                )
251            }
252        );
253
254        assert_eq!(
255            font.glyphs.iter().cloned().collect::<Vec<_>>(),
256            vec![
257                Glyph {
258                    bitmap: vec![0x1f, 0x01],
259                    bounding_box: BoundingBox {
260                        size: Coord::new(8, 8),
261                        offset: Coord::new(0, 0),
262                    },
263                    encoding: Encoding::Standard(64), // '@'
264                    name: "Char 0".to_string(),
265                    width_horizontal: Some(GlyphWidth {
266                        device: Coord::new(8, 0),
267                        scalable: Coord::new(480, 0),
268                    }),
269                    width_vertical: None,
270                    origin_offset: None,
271                },
272                Glyph {
273                    bitmap: vec![0x2f, 0x02],
274                    bounding_box: BoundingBox {
275                        size: Coord::new(8, 8),
276                        offset: Coord::new(0, 0),
277                    },
278                    encoding: Encoding::Standard(65), // 'A'
279                    name: "Char 1".to_string(),
280                    width_horizontal: Some(GlyphWidth {
281                        device: Coord::new(8, 0),
282                        scalable: Coord::new(480, 0),
283                    }),
284                    width_vertical: None,
285                    origin_offset: None,
286                },
287            ],
288        );
289    }
290
291    #[test]
292    fn parse_font() {
293        test_font(&Font::parse(FONT).unwrap())
294    }
295
296    #[test]
297    fn parse_font_without_endfont() {
298        let lines: Vec<_> = FONT
299            .lines()
300            .filter(|line| !line.contains("ENDFONT"))
301            .collect();
302        let input = lines.join("\n");
303
304        test_font(&Font::parse(&input).unwrap());
305    }
306
307    #[test]
308    fn parse_font_without_chars() {
309        let lines: Vec<_> = FONT
310            .lines()
311            .filter(|line| !line.contains("CHARS"))
312            .collect();
313        let input = lines.join("\n");
314
315        test_font(&Font::parse(&input).unwrap());
316    }
317
318    #[test]
319    fn parse_font_missing_swidth() {
320        let lines: Vec<_> = FONT
321            .lines()
322            .filter(|line| !line.contains("SWIDTH"))
323            .collect();
324        let input = lines.join("\n");
325
326        test_font(&Font::parse(&input).unwrap());
327    }
328
329    #[test]
330    fn parse_font_with_windows_line_endings() {
331        let lines: Vec<_> = FONT.lines().collect();
332        let input = lines.join("\r\n");
333
334        test_font(&Font::parse(&input).unwrap());
335    }
336
337    #[test]
338    fn parse_empty_font() {
339        assert_parser_error("", "empty input", None);
340    }
341
342    // TODO: Should it be OK to have garbage after ENDFONT?
343    #[test]
344    #[ignore]
345    fn parse_font_with_garbage_after_endfont() {
346        let lines: Vec<_> = FONT.lines().chain(std::iter::once("Invalid")).collect();
347        let input = lines.join("\n");
348
349        assert_parser_error(&input, "expected end of input", Some(28));
350    }
351
352    #[test]
353    fn parse_with_leading_whitespace() {
354        let lines: Vec<_> = std::iter::once("").chain(FONT.lines()).collect();
355        let input = lines.join("\n");
356
357        test_font(&Font::parse(&input).unwrap());
358    }
359
360    #[test]
361    fn invalid_first_line() {
362        let input = "\nSOMETHING 2.1";
363        assert_parser_error(input, "expected \"STARTFONT 2.1\"", Some(2));
364    }
365
366    #[test]
367    fn missing_font_name() {
368        let input = "STARTFONT 2.1\n";
369        assert_parser_error(input, "missing \"FONT\"", None);
370    }
371
372    const fn bb(offset_x: i32, offset_y: i32, size_x: i32, size_y: i32) -> BoundingBox {
373        BoundingBox {
374            offset: Coord::new(offset_x, offset_y),
375            size: Coord::new(size_x, size_y),
376        }
377    }
378
379    #[test]
380    fn union() {
381        for ((bb1, bb2), expected_union) in [
382            // Non overlapping
383            ((bb(0, 0, 4, 5), bb(4, 0, 4, 5)), bb(0, 0, 8, 5)),
384            ((bb(0, 0, 4, 5), bb(5, 0, 4, 5)), bb(0, 0, 9, 5)),
385            ((bb(0, 0, 4, 5), bb(-4, 0, 4, 5)), bb(-4, 0, 8, 5)),
386            ((bb(0, 0, 4, 5), bb(-6, 0, 4, 5)), bb(-6, 0, 10, 5)),
387            ((bb(0, 0, 4, 5), bb(0, 5, 4, 5)), bb(0, 0, 4, 10)),
388            ((bb(0, 0, 4, 5), bb(0, 6, 4, 5)), bb(0, 0, 4, 11)),
389            ((bb(0, 0, 4, 5), bb(0, -5, 4, 5)), bb(0, -5, 4, 10)),
390            ((bb(0, 0, 4, 5), bb(0, -10, 4, 5)), bb(0, -10, 4, 15)),
391            ((bb(1, 2, 3, 4), bb(5, 6, 7, 8)), bb(1, 2, 11, 12)),
392            // Overlapping
393            ((bb(0, 0, 4, 5), bb(2, 0, 4, 5)), bb(0, 0, 6, 5)),
394            ((bb(0, 0, 4, 5), bb(-3, 0, 4, 5)), bb(-3, 0, 7, 5)),
395            ((bb(0, 0, 4, 5), bb(0, 3, 4, 5)), bb(0, 0, 4, 8)),
396            ((bb(0, 0, 4, 5), bb(0, -2, 4, 5)), bb(0, -2, 4, 7)),
397            ((bb(1, 2, 5, 7), bb(5, 6, 3, 4)), bb(1, 2, 7, 8)),
398            // Inside
399            ((bb(-1, -2, 3, 5), bb(0, 0, 1, 2)), bb(-1, -2, 3, 5)),
400            // Zero sized
401            ((bb(0, 0, 0, 0), bb(0, 0, 0, 0)), bb(0, 0, 0, 0)),
402            ((bb(1, 2, 3, 4), bb(0, 0, 0, 0)), bb(1, 2, 3, 4)),
403            ((bb(1, 2, 3, 4), bb(0, 0, 1, 0)), bb(1, 2, 3, 4)),
404            ((bb(1, 2, 3, 4), bb(0, 0, 0, 1)), bb(1, 2, 3, 4)),
405            ((bb(0, 0, 0, 0), bb(1, 2, 3, 4)), bb(1, 2, 3, 4)),
406            ((bb(0, 0, 1, 0), bb(1, 2, 3, 4)), bb(1, 2, 3, 4)),
407            ((bb(0, 0, 0, 1), bb(1, 2, 3, 4)), bb(1, 2, 3, 4)),
408        ]
409        .into_iter()
410        {
411            assert_eq!(bb1.union(&bb2), expected_union, "{bb1:?}, {bb2:?}");
412        }
413    }
414}