1#![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#[derive(Debug, Clone, PartialEq)]
29pub struct BdfFont {
30 pub metadata: Metadata,
32
33 pub glyphs: Glyphs,
35
36 pub properties: Properties,
38}
39
40impl BdfFont {
41 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#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
78pub struct BoundingBox {
79 pub size: Coord,
81
82 pub offset: Coord,
84}
85
86#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
90pub struct Coord {
91 pub x: i32,
93
94 pub y: i32,
96}
97
98impl Coord {
99 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#[derive(Debug, PartialEq, thiserror::Error)]
123pub enum ParserError {
124 #[error("couldn't parse metadata")]
126 Metadata,
127
128 #[error("couldn't parse properties")]
130 Properties,
131
132 #[error("couldn't parse glyphs")]
134 Glyphs,
135
136 #[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('@'), 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'), 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}