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 | |
46 | mod draw_target; |
47 | mod generated; |
48 | pub mod mapping; |
49 | mod mono_text_style; |
50 | |
51 | use core::fmt; |
52 | |
53 | pub use generated::*; |
54 | pub use mono_text_style::{MonoTextStyle, MonoTextStyleBuilder}; |
55 | |
56 | use 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)] |
70 | pub 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 | |
98 | impl 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 | |
126 | impl 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 | b:other.glyph_mapping as *const dyn GlyphMapping as *const u8, |
138 | ) |
139 | } |
140 | } |
141 | |
142 | impl fmt::Debug for MonoFont<'_> { |
143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
144 | f&mut DebugStruct<'_, '_>.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(name:"glyph_mapping" , &"?" ) |
152 | .finish_non_exhaustive() |
153 | } |
154 | } |
155 | |
156 | #[cfg (feature = "defmt" )] |
157 | impl ::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))] |
180 | pub 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 | |
187 | impl 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 | |
217 | const NULL_FONT: MonoFont = MonoFont { |
218 | image: ImageRaw::new(&[], width:1), |
219 | character_size: Size::zero(), |
220 | character_spacing: 0, |
221 | baseline: 0, |
222 | strikethrough: DecorationDimensions::new(offset:0, height:0), |
223 | underline: DecorationDimensions::new(offset:0, height:0), |
224 | glyph_mapping: &mapping::ASCII, |
225 | }; |
226 | |
227 | #[cfg (test)] |
228 | pub(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 | } |
473 | |