//! Monospaced bitmap fonts.
//!
//! This module contains support for drawing monospaced bitmap fonts and provides
//! several [built-in fonts].
//!
//! Additional custom fonts can be added by the application or other crates. This
//! is demonstrated in the `text-custom-font` example in the [examples repository].
//!
//! # Examples
//!
//! The [`text` module] contains examples how these fonts can be used in an application.
//!
//! # Built-in fonts
//!
//! Each built-in font is provided in different glyph subsets. The ASCII variant is the smallest
//! subset which saves memory in embedded applications, but only covers all characters of the English
//! language. The ISO 8859 subsets support a wide range of languages, see
//! [Wikipedia](https://en.wikipedia.org/wiki/ISO/IEC_8859#The_parts_of_ISO/IEC_8859) for a list of
//! languages.
//!
//! The table below shows the ASCII variant of the built-in fonts. See the [subset modules](#modules) for
//! an overview of the complete character set included in the other variants.
//!
// WARNING: The table between START-FONT-TABLE and END-FONT-TABLE is generated.
// Use `just convert-fonts` to update the table.
//START-FONT-TABLE-ASCII
//! | Type | Screenshot | | Type | Screenshot |
//! |------|------------|-|------|------------|
//! | `FONT_4X6` |  | | `FONT_7X13_ITALIC` |  |
//! | `FONT_5X7` |  | | `FONT_7X14` |  |
//! | `FONT_5X8` |  | | `FONT_7X14_BOLD` |  |
//! | `FONT_6X9` |  | | `FONT_8X13` |  |
//! | `FONT_6X10` |  | | `FONT_8X13_BOLD` |  |
//! | `FONT_6X12` |  | | `FONT_8X13_ITALIC` |  |
//! | `FONT_6X13` |  | | `FONT_9X15` |  |
//! | `FONT_6X13_BOLD` |  | | `FONT_9X15_BOLD` |  |
//! | `FONT_6X13_ITALIC` |  | | `FONT_9X18` |  |
//! | `FONT_7X13` |  | | `FONT_9X18_BOLD` |  |
//! | `FONT_7X13_BOLD` |  | | `FONT_10X20` |  |
//END-FONT-TABLE
//!
//! [built-in fonts]: #built-in-fonts
//! [`text` module]: super::text#examples
//! [examples repository]: https://github.com/embedded-graphics/examples
mod draw_target;
mod generated;
pub mod mapping;
mod mono_text_style;
use core::fmt;
pub use generated::*;
pub use mono_text_style::{MonoTextStyle, MonoTextStyleBuilder};
use crate::{
geometry::{OriginDimensions, Point, Size},
image::{ImageRaw, SubImage},
mono_font::mapping::GlyphMapping,
pixelcolor::BinaryColor,
primitives::Rectangle,
};
/// Monospaced bitmap font.
///
/// See the [module documentation] for more information about using fonts.
///
/// [module documentation]: self
#[derive(Clone, Copy)]
pub struct MonoFont<'a> {
/// Raw image data containing the font.
pub image: ImageRaw<'a, BinaryColor>,
/// Size of a single character in pixel.
pub character_size: Size,
/// Spacing between characters.
///
/// The spacing defines how many empty pixels are added horizontally between adjacent characters
/// on a single line of text.
pub character_spacing: u32,
/// The baseline.
///
/// Offset from the top of the glyph bounding box to the baseline.
pub baseline: u32,
/// Strikethrough decoration dimensions.
pub strikethrough: DecorationDimensions,
/// Underline decoration dimensions.
pub underline: DecorationDimensions,
/// Glyph mapping.
pub glyph_mapping: &'a dyn GlyphMapping,
}
impl MonoFont<'_> {
/// Returns a subimage for a glyph.
pub(crate) fn glyph(&self, c: char) -> SubImage<'_, ImageRaw<BinaryColor>> {
if self.character_size.width == 0 || self.image.size().width < self.character_size.width {
return SubImage::new_unchecked(&self.image, Rectangle::zero());
}
let glyphs_per_row = self.image.size().width / self.character_size.width;
// Char _code_ offset from first char, most often a space
// E.g. first char = ' ' (32), target char = '!' (33), offset = 33 - 32 = 1
let glyph_index = self.glyph_mapping.index(c) as u32;
let row = glyph_index / glyphs_per_row;
// Top left corner of character, in pixels
let char_x = (glyph_index - (row * glyphs_per_row)) * self.character_size.width;
let char_y = row * self.character_size.height;
SubImage::new_unchecked(
&self.image,
Rectangle::new(
Point::new(char_x as i32, char_y as i32),
self.character_size,
),
)
}
}
impl PartialEq for MonoFont<'_> {
#[allow(trivial_casts)]
fn eq(&self, other: &Self) -> bool {
self.image == other.image
&& self.character_size == other.character_size
&& self.character_spacing == other.character_spacing
&& self.baseline == other.baseline
&& self.strikethrough == other.strikethrough
&& self.underline == other.underline
&& core::ptr::eq(
self.glyph_mapping as *const dyn GlyphMapping as *const u8,
other.glyph_mapping as *const dyn GlyphMapping as *const u8,
)
}
}
impl fmt::Debug for MonoFont<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MonoFont")
.field("image", &self.image)
.field("character_size", &self.character_size)
.field("character_spacing", &self.character_spacing)
.field("baseline", &self.baseline)
.field("strikethrough", &self.strikethrough)
.field("underline", &self.underline)
.field("glyph_mapping", &"?")
.finish_non_exhaustive()
}
}
#[cfg(feature = "defmt")]
impl ::defmt::Format for MonoFont<'_> {
fn format(&self, f: ::defmt::Formatter) {
::defmt::write!(
f,
"MonoFont {{ image: {}, character_size: {}, character_spacing: {}, baseline: {}, strikethrough: {}, underline: {}, .. }}",
&self.image,
&self.character_size,
&self.character_spacing,
&self.baseline,
&self.strikethrough,
&self.underline,
)
}
}
/// Decoration dimensions.
///
/// `DecorationDimensions` is used to specify the position and height of underline and strikethrough
/// decorations in [`MonoFont`]s.
///
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct DecorationDimensions {
/// Offset from the top of the character to the top of the decoration.
pub offset: u32,
/// Height of the decoration.
pub height: u32,
}
impl DecorationDimensions {
/// Creates new decoration dimensions.
pub const fn new(offset: u32, height: u32) -> Self {
Self { offset, height }
}
/// Creates a new default strikethrough decoration for the given glyph height.
pub const fn default_strikethrough(glyph_height: u32) -> Self {
Self {
offset: glyph_height.saturating_sub(1) / 2,
height: 1,
}
}
/// Creates a new default underline decoration for the given glyph height.
pub const fn default_underline(glyph_height: u32) -> Self {
Self {
offset: glyph_height + 1,
height: 1,
}
}
fn to_rectangle(&self, position: Point, width: u32) -> Rectangle {
let top_left = position + Size::new(0, self.offset);
let size = Size::new(width, self.height);
Rectangle::new(top_left, size)
}
}
const NULL_FONT: MonoFont = MonoFont {
image: ImageRaw::new(&[], 1),
character_size: Size::zero(),
character_spacing: 0,
baseline: 0,
strikethrough: DecorationDimensions::new(0, 0),
underline: DecorationDimensions::new(0, 0),
glyph_mapping: &mapping::ASCII,
};
#[cfg(test)]
pub(crate) mod tests {
use arrayvec::ArrayString;
use super::*;
use crate::{
framebuffer::{buffer_size, Framebuffer},
geometry::{Dimensions, Point},
image::{GetPixel, Image},
mock_display::MockDisplay,
mono_font::{mapping::Mapping, MonoTextStyleBuilder},
pixelcolor::{
raw::{LittleEndian, RawU1},
BinaryColor,
},
text::{Baseline, Text},
Drawable,
};
/// Draws a text using the given font and checks it against the expected pattern.
#[track_caller]
pub fn assert_text_from_pattern(text: &str, font: &MonoFont, pattern: &[&str]) {
let style = MonoTextStyleBuilder::new()
.font(font)
.text_color(BinaryColor::On)
.build();
let mut display = MockDisplay::new();
Text::with_baseline(text, Point::zero(), style, Baseline::Top)
.draw(&mut display)
.unwrap();
display.assert_pattern(pattern);
}
/// Test if the baseline constant is set correctly.
///
/// This test assumes that the character `A` is on the baseline.
pub fn test_baseline(font: &MonoFont) {
let style = MonoTextStyleBuilder::new()
.font(font)
.text_color(BinaryColor::On)
.build();
// Draw 'A' character to determine it's baseline
let mut display = MockDisplay::new();
Text::with_baseline("A", Point::zero(), style, Baseline::Top)
.draw(&mut display)
.unwrap();
let baseline = display.affected_area().bottom_right().unwrap().y as u32;
assert_eq!(font.baseline, baseline);
}
#[test]
fn baseline() {
test_baseline(&ascii::FONT_4X6);
test_baseline(&ascii::FONT_5X7);
test_baseline(&ascii::FONT_5X8);
test_baseline(&ascii::FONT_6X10);
test_baseline(&ascii::FONT_6X12);
test_baseline(&ascii::FONT_6X13_BOLD);
test_baseline(&ascii::FONT_6X13);
test_baseline(&ascii::FONT_6X13_ITALIC);
test_baseline(&ascii::FONT_6X9);
test_baseline(&ascii::FONT_7X13_BOLD);
test_baseline(&ascii::FONT_7X13);
test_baseline(&ascii::FONT_7X13_ITALIC);
test_baseline(&ascii::FONT_7X14_BOLD);
test_baseline(&ascii::FONT_7X14);
test_baseline(&ascii::FONT_8X13_BOLD);
test_baseline(&ascii::FONT_8X13);
test_baseline(&ascii::FONT_8X13_ITALIC);
test_baseline(&ascii::FONT_9X15_BOLD);
test_baseline(&ascii::FONT_9X15);
test_baseline(&ascii::FONT_9X18_BOLD);
test_baseline(&ascii::FONT_9X18);
test_baseline(&ascii::FONT_10X20);
}
/// (Statically) test that [`MonoFont: Send + Sync`].
fn _mono_font_is_sync()
where
for<'a> MonoFont<'a>: Send + Sync,
{
}
fn new_framebuffer() -> Framebuffer<
BinaryColor,
RawU1,
LittleEndian,
96,
200,
{ buffer_size::<BinaryColor>(96, 200) },
> {
Framebuffer::new()
}
fn dump_framebuffer<T: GetPixel<Color = BinaryColor> + Dimensions, const N: usize>(
framebuffer: &T,
output: &mut ArrayString<N>,
) {
let bb = framebuffer.bounding_box();
for y in bb.rows() {
for x in bb.columns() {
let c = match framebuffer.pixel(Point::new(x, y)).unwrap() {
BinaryColor::Off => ' ',
BinaryColor::On => '#',
};
output.push(c);
}
output.push('\n');
}
}
#[test]
fn draw_font_subsets() {
let fonts = &[
(Mapping::Ascii, ascii::FONT_6X13),
(Mapping::Iso8859_1, iso_8859_1::FONT_6X13),
(Mapping::Iso8859_10, iso_8859_10::FONT_6X13),
(Mapping::Iso8859_13, iso_8859_13::FONT_6X13),
(Mapping::Iso8859_14, iso_8859_14::FONT_6X13),
(Mapping::Iso8859_15, iso_8859_15::FONT_6X13),
(Mapping::Iso8859_16, iso_8859_16::FONT_6X13),
(Mapping::Iso8859_2, iso_8859_2::FONT_6X13),
(Mapping::Iso8859_3, iso_8859_3::FONT_6X13),
(Mapping::Iso8859_4, iso_8859_4::FONT_6X13),
(Mapping::Iso8859_5, iso_8859_5::FONT_6X13),
(Mapping::Iso8859_7, iso_8859_7::FONT_6X13),
(Mapping::Iso8859_9, iso_8859_9::FONT_6X13),
(Mapping::JisX0201, jis_x0201::FONT_6X13),
];
for (mapping, font) in fonts {
let mut expected = new_framebuffer();
Image::new(&font.image, Point::zero())
.draw(&mut expected)
.unwrap();
let chars_per_row = (font.image.size().width / font.character_size.width) as usize;
let mut text = ArrayString::<1024>::new();
for (i, c) in mapping.glyph_mapping().chars().enumerate() {
if i % chars_per_row == 0 && i != 0 {
text.push('\n');
}
text.push(c);
}
let mut output = new_framebuffer();
Text::with_baseline(
&text,
Point::zero(),
MonoTextStyle::new(&font, BinaryColor::On),
Baseline::Top,
)
.draw(&mut output)
.unwrap();
if expected != output {
let mut message = ArrayString::<65536>::new();
message.push_str("Output:\n");
dump_framebuffer(&output, &mut message);
message.push_str("\nExpected:\n");
dump_framebuffer(&expected, &mut message);
panic!("{}", message)
}
}
}
#[test]
fn zero_width_image() {
const ZERO_WIDTH: MonoFont = MonoFont {
image: ImageRaw::new(&[], 0),
character_size: Size::zero(),
character_spacing: 0,
baseline: 0,
strikethrough: DecorationDimensions::new(0, 0),
underline: DecorationDimensions::new(0, 0),
glyph_mapping: &mapping::ASCII,
};
let mut display = MockDisplay::new();
Text::new(
" ",
Point::new_equal(20),
MonoTextStyle::new(&ZERO_WIDTH, BinaryColor::On),
)
.draw(&mut display)
.unwrap();
display.assert_pattern(&[]);
}
#[test]
fn image_width_less_than_character_width() {
const NOT_WIDE_ENOUGH: MonoFont = MonoFont {
image: ImageRaw::new(&[0xAA, 0xAA], 4),
character_size: Size::new(5, 2),
character_spacing: 0,
baseline: 0,
strikethrough: DecorationDimensions::new(0, 0),
underline: DecorationDimensions::new(0, 0),
glyph_mapping: &mapping::ASCII,
};
let mut display = MockDisplay::new();
Text::new(
" ",
Point::new_equal(20),
MonoTextStyle::new(&NOT_WIDE_ENOUGH, BinaryColor::On),
)
.draw(&mut display)
.unwrap();
display.assert_pattern(&[]);
}
#[test]
fn image_height_less_than_character_height() {
const NOT_HIGH_ENOUGH: MonoFont = MonoFont {
image: ImageRaw::new(&[0xAA, 0xAA], 4),
character_size: Size::new(4, 1),
character_spacing: 0,
baseline: 0,
strikethrough: DecorationDimensions::new(0, 0),
underline: DecorationDimensions::new(0, 0),
glyph_mapping: &mapping::ASCII,
};
let mut display = MockDisplay::new();
Text::new(
" ",
Point::zero(),
MonoTextStyle::new(&NOT_HIGH_ENOUGH, BinaryColor::On),
)
.draw(&mut display)
.unwrap();
display.assert_pattern(&["# #"]);
}
}