embedded_graphics/mono_font/mod.rs
1//! Monospaced bitmap fonts.
2//!
3//! This module contains support for drawing monospaced bitmap fonts and provides
4//! several [built-in fonts].
5//!
6//! Additional custom fonts can be added by the application or other crates. This
7//! is demonstrated in the `text-custom-font` example in the [examples repository].
8//!
9//! # Examples
10//!
11//! The [`text` module] contains examples how these fonts can be used in an application.
12//!
13//! # Built-in fonts
14//!
15//! Each built-in font is provided in different glyph subsets. The ASCII variant is the smallest
16//! subset which saves memory in embedded applications, but only covers all characters of the English
17//! language. The ISO 8859 subsets support a wide range of languages, see
18//! [Wikipedia](https://en.wikipedia.org/wiki/ISO/IEC_8859#The_parts_of_ISO/IEC_8859) for a list of
19//! languages.
20//!
21//! The table below shows the ASCII variant of the built-in fonts. See the [subset modules](#modules) for
22//! an overview of the complete character set included in the other variants.
23//!
24// WARNING: The table between START-FONT-TABLE and END-FONT-TABLE is generated.
25// Use `just convert-fonts` to update the table.
26//START-FONT-TABLE-ASCII
27//! | Type | Screenshot | | Type | Screenshot |
28//! |------|------------|-|------|------------|
29//! | `FONT_4X6` |  | | `FONT_7X13_ITALIC` |  |
30//! | `FONT_5X7` |  | | `FONT_7X14` |  |
31//! | `FONT_5X8` |  | | `FONT_7X14_BOLD` |  |
32//! | `FONT_6X9` |  | | `FONT_8X13` |  |
33//! | `FONT_6X10` |  | | `FONT_8X13_BOLD` |  |
34//! | `FONT_6X12` |  | | `FONT_8X13_ITALIC` |  |
35//! | `FONT_6X13` |  | | `FONT_9X15` |  |
36//! | `FONT_6X13_BOLD` |  | | `FONT_9X15_BOLD` |  |
37//! | `FONT_6X13_ITALIC` |  | | `FONT_9X18` |  |
38//! | `FONT_7X13` |  | | `FONT_9X18_BOLD` |  |
39//! | `FONT_7X13_BOLD` |  | | `FONT_10X20` |  |
40//END-FONT-TABLE
41//!
42//! [built-in fonts]: #built-in-fonts
43//! [`text` module]: super::text#examples
44//! [examples repository]: https://github.com/embedded-graphics/examples
45
46mod draw_target;
47mod generated;
48pub mod mapping;
49mod mono_text_style;
50
51use core::fmt;
52
53pub use generated::*;
54pub use mono_text_style::{MonoTextStyle, MonoTextStyleBuilder};
55
56use crate::{
57 geometry::{OriginDimensions, Point, Size},
58 image::{ImageRaw, SubImage},
59 mono_font::mapping::GlyphMapping,
60 pixelcolor::BinaryColor,
61 primitives::Rectangle,
62};
63
64/// Monospaced bitmap font.
65///
66/// See the [module documentation] for more information about using fonts.
67///
68/// [module documentation]: self
69#[derive(Clone, Copy)]
70pub struct MonoFont<'a> {
71 /// Raw image data containing the font.
72 pub image: ImageRaw<'a, BinaryColor>,
73
74 /// Size of a single character in pixel.
75 pub character_size: Size,
76
77 /// Spacing between characters.
78 ///
79 /// The spacing defines how many empty pixels are added horizontally between adjacent characters
80 /// on a single line of text.
81 pub character_spacing: u32,
82
83 /// The baseline.
84 ///
85 /// Offset from the top of the glyph bounding box to the baseline.
86 pub baseline: u32,
87
88 /// Strikethrough decoration dimensions.
89 pub strikethrough: DecorationDimensions,
90
91 /// Underline decoration dimensions.
92 pub underline: DecorationDimensions,
93
94 /// Glyph mapping.
95 pub glyph_mapping: &'a dyn GlyphMapping,
96}
97
98impl MonoFont<'_> {
99 /// Returns a subimage for a glyph.
100 pub(crate) fn glyph(&self, c: char) -> SubImage<'_, ImageRaw<BinaryColor>> {
101 if self.character_size.width == 0 || self.image.size().width < self.character_size.width {
102 return SubImage::new_unchecked(&self.image, Rectangle::zero());
103 }
104
105 let glyphs_per_row = self.image.size().width / self.character_size.width;
106
107 // Char _code_ offset from first char, most often a space
108 // E.g. first char = ' ' (32), target char = '!' (33), offset = 33 - 32 = 1
109 let glyph_index = self.glyph_mapping.index(c) as u32;
110 let row = glyph_index / glyphs_per_row;
111
112 // Top left corner of character, in pixels
113 let char_x = (glyph_index - (row * glyphs_per_row)) * self.character_size.width;
114 let char_y = row * self.character_size.height;
115
116 SubImage::new_unchecked(
117 &self.image,
118 Rectangle::new(
119 Point::new(char_x as i32, char_y as i32),
120 self.character_size,
121 ),
122 )
123 }
124}
125
126impl PartialEq for MonoFont<'_> {
127 #[allow(trivial_casts)]
128 fn eq(&self, other: &Self) -> bool {
129 self.image == other.image
130 && self.character_size == other.character_size
131 && self.character_spacing == other.character_spacing
132 && self.baseline == other.baseline
133 && self.strikethrough == other.strikethrough
134 && self.underline == other.underline
135 && core::ptr::eq(
136 self.glyph_mapping as *const dyn GlyphMapping as *const u8,
137 other.glyph_mapping as *const dyn GlyphMapping as *const u8,
138 )
139 }
140}
141
142impl fmt::Debug for MonoFont<'_> {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.debug_struct("MonoFont")
145 .field("image", &self.image)
146 .field("character_size", &self.character_size)
147 .field("character_spacing", &self.character_spacing)
148 .field("baseline", &self.baseline)
149 .field("strikethrough", &self.strikethrough)
150 .field("underline", &self.underline)
151 .field("glyph_mapping", &"?")
152 .finish_non_exhaustive()
153 }
154}
155
156#[cfg(feature = "defmt")]
157impl ::defmt::Format for MonoFont<'_> {
158 fn format(&self, f: ::defmt::Formatter) {
159 ::defmt::write!(
160 f,
161 "MonoFont {{ image: {}, character_size: {}, character_spacing: {}, baseline: {}, strikethrough: {}, underline: {}, .. }}",
162 &self.image,
163 &self.character_size,
164 &self.character_spacing,
165 &self.baseline,
166 &self.strikethrough,
167 &self.underline,
168
169 )
170 }
171}
172
173/// Decoration dimensions.
174///
175/// `DecorationDimensions` is used to specify the position and height of underline and strikethrough
176/// decorations in [`MonoFont`]s.
177///
178#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
179#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
180pub struct DecorationDimensions {
181 /// Offset from the top of the character to the top of the decoration.
182 pub offset: u32,
183 /// Height of the decoration.
184 pub height: u32,
185}
186
187impl DecorationDimensions {
188 /// Creates new decoration dimensions.
189 pub const fn new(offset: u32, height: u32) -> Self {
190 Self { offset, height }
191 }
192
193 /// Creates a new default strikethrough decoration for the given glyph height.
194 pub const fn default_strikethrough(glyph_height: u32) -> Self {
195 Self {
196 offset: glyph_height.saturating_sub(1) / 2,
197 height: 1,
198 }
199 }
200
201 /// Creates a new default underline decoration for the given glyph height.
202 pub const fn default_underline(glyph_height: u32) -> Self {
203 Self {
204 offset: glyph_height + 1,
205 height: 1,
206 }
207 }
208
209 fn to_rectangle(&self, position: Point, width: u32) -> Rectangle {
210 let top_left = position + Size::new(0, self.offset);
211 let size = Size::new(width, self.height);
212
213 Rectangle::new(top_left, size)
214 }
215}
216
217const NULL_FONT: MonoFont = MonoFont {
218 image: ImageRaw::new(&[], 1),
219 character_size: Size::zero(),
220 character_spacing: 0,
221 baseline: 0,
222 strikethrough: DecorationDimensions::new(0, 0),
223 underline: DecorationDimensions::new(0, 0),
224 glyph_mapping: &mapping::ASCII,
225};
226
227#[cfg(test)]
228pub(crate) mod tests {
229 use arrayvec::ArrayString;
230
231 use super::*;
232 use crate::{
233 framebuffer::{buffer_size, Framebuffer},
234 geometry::{Dimensions, Point},
235 image::{GetPixel, Image},
236 mock_display::MockDisplay,
237 mono_font::{mapping::Mapping, MonoTextStyleBuilder},
238 pixelcolor::{
239 raw::{LittleEndian, RawU1},
240 BinaryColor,
241 },
242 text::{Baseline, Text},
243 Drawable,
244 };
245
246 /// Draws a text using the given font and checks it against the expected pattern.
247 #[track_caller]
248 pub fn assert_text_from_pattern(text: &str, font: &MonoFont, pattern: &[&str]) {
249 let style = MonoTextStyleBuilder::new()
250 .font(font)
251 .text_color(BinaryColor::On)
252 .build();
253
254 let mut display = MockDisplay::new();
255 Text::with_baseline(text, Point::zero(), style, Baseline::Top)
256 .draw(&mut display)
257 .unwrap();
258
259 display.assert_pattern(pattern);
260 }
261
262 /// Test if the baseline constant is set correctly.
263 ///
264 /// This test assumes that the character `A` is on the baseline.
265 pub fn test_baseline(font: &MonoFont) {
266 let style = MonoTextStyleBuilder::new()
267 .font(font)
268 .text_color(BinaryColor::On)
269 .build();
270
271 // Draw 'A' character to determine it's baseline
272 let mut display = MockDisplay::new();
273 Text::with_baseline("A", Point::zero(), style, Baseline::Top)
274 .draw(&mut display)
275 .unwrap();
276
277 let baseline = display.affected_area().bottom_right().unwrap().y as u32;
278
279 assert_eq!(font.baseline, baseline);
280 }
281
282 #[test]
283 fn baseline() {
284 test_baseline(&ascii::FONT_4X6);
285 test_baseline(&ascii::FONT_5X7);
286 test_baseline(&ascii::FONT_5X8);
287 test_baseline(&ascii::FONT_6X10);
288 test_baseline(&ascii::FONT_6X12);
289 test_baseline(&ascii::FONT_6X13_BOLD);
290 test_baseline(&ascii::FONT_6X13);
291 test_baseline(&ascii::FONT_6X13_ITALIC);
292 test_baseline(&ascii::FONT_6X9);
293 test_baseline(&ascii::FONT_7X13_BOLD);
294 test_baseline(&ascii::FONT_7X13);
295 test_baseline(&ascii::FONT_7X13_ITALIC);
296 test_baseline(&ascii::FONT_7X14_BOLD);
297 test_baseline(&ascii::FONT_7X14);
298 test_baseline(&ascii::FONT_8X13_BOLD);
299 test_baseline(&ascii::FONT_8X13);
300 test_baseline(&ascii::FONT_8X13_ITALIC);
301 test_baseline(&ascii::FONT_9X15_BOLD);
302 test_baseline(&ascii::FONT_9X15);
303 test_baseline(&ascii::FONT_9X18_BOLD);
304 test_baseline(&ascii::FONT_9X18);
305 test_baseline(&ascii::FONT_10X20);
306 }
307
308 /// (Statically) test that [`MonoFont: Send + Sync`].
309 fn _mono_font_is_sync()
310 where
311 for<'a> MonoFont<'a>: Send + Sync,
312 {
313 }
314
315 fn new_framebuffer() -> Framebuffer<
316 BinaryColor,
317 RawU1,
318 LittleEndian,
319 96,
320 200,
321 { buffer_size::<BinaryColor>(96, 200) },
322 > {
323 Framebuffer::new()
324 }
325
326 fn dump_framebuffer<T: GetPixel<Color = BinaryColor> + Dimensions, const N: usize>(
327 framebuffer: &T,
328 output: &mut ArrayString<N>,
329 ) {
330 let bb = framebuffer.bounding_box();
331
332 for y in bb.rows() {
333 for x in bb.columns() {
334 let c = match framebuffer.pixel(Point::new(x, y)).unwrap() {
335 BinaryColor::Off => ' ',
336 BinaryColor::On => '#',
337 };
338 output.push(c);
339 }
340 output.push('\n');
341 }
342 }
343
344 #[test]
345 fn draw_font_subsets() {
346 let fonts = &[
347 (Mapping::Ascii, ascii::FONT_6X13),
348 (Mapping::Iso8859_1, iso_8859_1::FONT_6X13),
349 (Mapping::Iso8859_10, iso_8859_10::FONT_6X13),
350 (Mapping::Iso8859_13, iso_8859_13::FONT_6X13),
351 (Mapping::Iso8859_14, iso_8859_14::FONT_6X13),
352 (Mapping::Iso8859_15, iso_8859_15::FONT_6X13),
353 (Mapping::Iso8859_16, iso_8859_16::FONT_6X13),
354 (Mapping::Iso8859_2, iso_8859_2::FONT_6X13),
355 (Mapping::Iso8859_3, iso_8859_3::FONT_6X13),
356 (Mapping::Iso8859_4, iso_8859_4::FONT_6X13),
357 (Mapping::Iso8859_5, iso_8859_5::FONT_6X13),
358 (Mapping::Iso8859_7, iso_8859_7::FONT_6X13),
359 (Mapping::Iso8859_9, iso_8859_9::FONT_6X13),
360 (Mapping::JisX0201, jis_x0201::FONT_6X13),
361 ];
362
363 for (mapping, font) in fonts {
364 let mut expected = new_framebuffer();
365 Image::new(&font.image, Point::zero())
366 .draw(&mut expected)
367 .unwrap();
368
369 let chars_per_row = (font.image.size().width / font.character_size.width) as usize;
370
371 let mut text = ArrayString::<1024>::new();
372 for (i, c) in mapping.glyph_mapping().chars().enumerate() {
373 if i % chars_per_row == 0 && i != 0 {
374 text.push('\n');
375 }
376 text.push(c);
377 }
378
379 let mut output = new_framebuffer();
380 Text::with_baseline(
381 &text,
382 Point::zero(),
383 MonoTextStyle::new(&font, BinaryColor::On),
384 Baseline::Top,
385 )
386 .draw(&mut output)
387 .unwrap();
388
389 if expected != output {
390 let mut message = ArrayString::<65536>::new();
391 message.push_str("Output:\n");
392 dump_framebuffer(&output, &mut message);
393 message.push_str("\nExpected:\n");
394 dump_framebuffer(&expected, &mut message);
395
396 panic!("{}", message)
397 }
398 }
399 }
400
401 #[test]
402 fn zero_width_image() {
403 const ZERO_WIDTH: MonoFont = MonoFont {
404 image: ImageRaw::new(&[], 0),
405 character_size: Size::zero(),
406 character_spacing: 0,
407 baseline: 0,
408 strikethrough: DecorationDimensions::new(0, 0),
409 underline: DecorationDimensions::new(0, 0),
410 glyph_mapping: &mapping::ASCII,
411 };
412
413 let mut display = MockDisplay::new();
414 Text::new(
415 " ",
416 Point::new_equal(20),
417 MonoTextStyle::new(&ZERO_WIDTH, BinaryColor::On),
418 )
419 .draw(&mut display)
420 .unwrap();
421
422 display.assert_pattern(&[]);
423 }
424
425 #[test]
426 fn image_width_less_than_character_width() {
427 const NOT_WIDE_ENOUGH: MonoFont = MonoFont {
428 image: ImageRaw::new(&[0xAA, 0xAA], 4),
429 character_size: Size::new(5, 2),
430 character_spacing: 0,
431 baseline: 0,
432 strikethrough: DecorationDimensions::new(0, 0),
433 underline: DecorationDimensions::new(0, 0),
434 glyph_mapping: &mapping::ASCII,
435 };
436
437 let mut display = MockDisplay::new();
438 Text::new(
439 " ",
440 Point::new_equal(20),
441 MonoTextStyle::new(&NOT_WIDE_ENOUGH, BinaryColor::On),
442 )
443 .draw(&mut display)
444 .unwrap();
445
446 display.assert_pattern(&[]);
447 }
448
449 #[test]
450 fn image_height_less_than_character_height() {
451 const NOT_HIGH_ENOUGH: MonoFont = MonoFont {
452 image: ImageRaw::new(&[0xAA, 0xAA], 4),
453 character_size: Size::new(4, 1),
454 character_spacing: 0,
455 baseline: 0,
456 strikethrough: DecorationDimensions::new(0, 0),
457 underline: DecorationDimensions::new(0, 0),
458 glyph_mapping: &mapping::ASCII,
459 };
460
461 let mut display = MockDisplay::new();
462 Text::new(
463 " ",
464 Point::zero(),
465 MonoTextStyle::new(&NOT_HIGH_ENOUGH, BinaryColor::On),
466 )
467 .draw(&mut display)
468 .unwrap();
469
470 display.assert_pattern(&["# #"]);
471 }
472}