1#![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#[derive(Debug, Clone, PartialEq)]
21pub struct Font {
22 pub metadata: Metadata,
24
25 pub glyphs: Glyphs,
27
28 pub metrics: Metrics,
30}
31
32impl Font {
33 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#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
62pub struct BoundingBox {
63 pub offset: Coord,
65
66 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 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#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
123pub struct Coord {
124 pub x: i32,
126
127 pub y: i32,
129}
130
131impl Coord {
132 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#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
146pub struct Metrics {
147 pub ascent: u32,
149
150 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 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), 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), 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 #[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 ((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 ((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 ((bb(-1, -2, 3, 5), bb(0, 0, 1, 2)), bb(-1, -2, 3, 5)),
400 ((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}