bdf_parser/
lib.rs

1//! BDF parser.
2
3#![deny(unsafe_code)]
4#![deny(missing_debug_implementations)]
5#![deny(missing_docs)]
6
7use nom::{
8    bytes::complete::tag,
9    character::complete::{multispace0, space1},
10    combinator::{eof, map, opt},
11    sequence::separated_pair,
12    IResult,
13};
14
15#[macro_use]
16mod helpers;
17
18mod glyph;
19mod metadata;
20mod properties;
21
22pub use glyph::{Glyph, Glyphs};
23use helpers::*;
24pub use metadata::Metadata;
25pub use properties::{Properties, Property, PropertyError};
26
27/// BDF Font.
28#[derive(Debug, Clone, PartialEq)]
29pub struct BdfFont {
30    /// Font metadata.
31    pub metadata: Metadata,
32
33    /// Glyphs.
34    pub glyphs: Glyphs,
35
36    /// Properties.
37    pub properties: Properties,
38}
39
40impl BdfFont {
41    /// Parses a BDF file.
42    ///
43    /// BDF files are expected to be ASCII encoded according to the BDF specification. Any non
44    /// ASCII characters in strings will be replaced by the `U+FFFD` replacement character.
45    pub fn parse(input: &[u8]) -> Result<Self, ParserError> {
46        let (input, metadata) = Metadata::parse(input).map_err(|_| ParserError::Metadata)?;
47        let input = skip_whitespace(input);
48        let (input, properties) = Properties::parse(input).map_err(|_| ParserError::Properties)?;
49        let input = skip_whitespace(input);
50        let (input, glyphs) = Glyphs::parse(input).map_err(|_| ParserError::Glyphs)?;
51        let input = skip_whitespace(input);
52        let (input, _) = end_font(input).unwrap();
53        let input = skip_whitespace(input);
54        end_of_file(input).map_err(|_| ParserError::EndOfFile)?;
55
56        Ok(Self {
57            properties,
58            metadata,
59            glyphs,
60        })
61    }
62}
63
64fn skip_whitespace(input: &[u8]) -> &[u8] {
65    multispace0::<_, nom::error::Error<_>>(input).unwrap().0
66}
67
68fn end_font(input: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
69    opt(tag("ENDFONT"))(input)
70}
71
72fn end_of_file(input: &[u8]) -> IResult<&[u8], &[u8]> {
73    eof(input)
74}
75
76/// Bounding box.
77#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
78pub struct BoundingBox {
79    /// Size of the bounding box.
80    pub size: Coord,
81
82    /// Offset to the lower left corner of the bounding box.
83    pub offset: Coord,
84}
85
86/// Coordinate.
87///
88/// BDF files use a cartesian coordinate system, where the positive half-axis points upwards.
89#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
90pub struct Coord {
91    /// X coordinate.
92    pub x: i32,
93
94    /// Y coordinate.
95    pub y: i32,
96}
97
98impl Coord {
99    /// Creates a new coord.
100    pub fn new(x: i32, y: i32) -> Self {
101        Self { x, y }
102    }
103
104    pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
105        map(
106            separated_pair(parse_to_i32, space1, parse_to_i32),
107            |(x, y)| Self::new(x, y),
108        )(input)
109    }
110}
111
112impl BoundingBox {
113    pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Self> {
114        map(
115            separated_pair(Coord::parse, space1, Coord::parse),
116            |(size, offset)| Self { size, offset },
117        )(input)
118    }
119}
120
121/// Parser error.
122#[derive(Debug, PartialEq, thiserror::Error)]
123pub enum ParserError {
124    /// Metadata.
125    #[error("couldn't parse metadata")]
126    Metadata,
127
128    /// Properties.
129    #[error("couldn't parse properties")]
130    Properties,
131
132    /// Glyphs.
133    #[error("couldn't parse glyphs")]
134    Glyphs,
135
136    /// Unexpected input at the end of the file.
137    #[error("unexpected input at the end of the file")]
138    EndOfFile,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use indoc::indoc;
145
146    const FONT: &'static str = indoc! {r#"
147        STARTFONT 2.1
148        FONT "test font"
149        SIZE 16 75 75
150        FONTBOUNDINGBOX 16 24 0 0
151        STARTPROPERTIES 3
152        COPYRIGHT "Copyright123"
153        FONT_ASCENT 1
154        FONT_DESCENT 2
155        ENDPROPERTIES
156        STARTCHAR Char 0
157        ENCODING 64
158        DWIDTH 8 0
159        BBX 8 8 0 0
160        BITMAP
161        1f
162        01
163        ENDCHAR
164        STARTCHAR Char 1
165        ENCODING 65
166        DWIDTH 8 0
167        BBX 8 8 0 0
168        BITMAP
169        2f
170        02
171        ENDCHAR
172        ENDFONT
173    "#};
174
175    fn test_font(font: &BdfFont) {
176        assert_eq!(
177            font.metadata,
178            Metadata {
179                version: 2.1,
180                name: String::from("\"test font\""),
181                point_size: 16,
182                resolution: Coord::new(75, 75),
183                bounding_box: BoundingBox {
184                    size: Coord::new(16, 24),
185                    offset: Coord::new(0, 0),
186                },
187            }
188        );
189
190        assert_eq!(
191            font.glyphs.iter().cloned().collect::<Vec<_>>(),
192            vec![
193                Glyph {
194                    bitmap: vec![0x1f, 0x01],
195                    bounding_box: BoundingBox {
196                        size: Coord::new(8, 8),
197                        offset: Coord::new(0, 0),
198                    },
199                    encoding: Some('@'), //64
200                    name: "Char 0".to_string(),
201                    device_width: Coord::new(8, 0),
202                    scalable_width: None,
203                },
204                Glyph {
205                    bitmap: vec![0x2f, 0x02],
206                    bounding_box: BoundingBox {
207                        size: Coord::new(8, 8),
208                        offset: Coord::new(0, 0),
209                    },
210                    encoding: Some('A'), //65
211                    name: "Char 1".to_string(),
212                    device_width: Coord::new(8, 0),
213                    scalable_width: None,
214                },
215            ],
216        );
217
218        assert_eq!(
219            font.properties.try_get(Property::Copyright),
220            Ok("Copyright123".to_string())
221        );
222        assert_eq!(font.properties.try_get(Property::FontAscent), Ok(1));
223        assert_eq!(font.properties.try_get(Property::FontDescent), Ok(2));
224    }
225
226    #[test]
227    fn parse_font() {
228        test_font(&BdfFont::parse(FONT.as_bytes()).unwrap())
229    }
230
231    #[test]
232    fn parse_font_without_endfont() {
233        let lines: Vec<_> = FONT
234            .lines()
235            .filter(|line| !line.contains("ENDFONT"))
236            .collect();
237        let input = lines.join("\n");
238
239        test_font(&BdfFont::parse(input.as_bytes()).unwrap());
240    }
241
242    #[test]
243    fn parse_font_with_windows_line_endings() {
244        let lines: Vec<_> = FONT.lines().collect();
245        let input = lines.join("\r\n");
246
247        test_font(&BdfFont::parse(input.as_bytes()).unwrap());
248    }
249
250    #[test]
251    fn parse_font_with_garbage_after_endfont() {
252        let lines: Vec<_> = FONT.lines().chain(std::iter::once("Invalid")).collect();
253        let input = lines.join("\n");
254
255        assert_eq!(
256            BdfFont::parse(input.as_bytes()),
257            Err(ParserError::EndOfFile)
258        );
259    }
260}