embedded_ttf/
lib.rs

1//! Font rendering (ttf and otf) with embedded-graphics.
2//!
3//! Embedded graphics provides static mono font rendering directly from the code.
4//! But it can render any font if the proper trait is implemented.
5//!
6//! This is an implementation that uses the [rusttype](https://gitlab.redox-os.org/redox-os/rusttype)
7//! crate to parse ttf and otf fonts before rendering them on a `DrawTarget`
8//!
9//! # Usage
10//!
11//! Use [`FontTextStyleBuilder`] to easily create a [`FontTextStyle`] object.
12//!
13//! This style can then be directly used with embedded graphics' [`Text`] struct.
14//!
15//! ```
16//! let mut display: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(350, 200));
17//!
18//! let style = FontTextStyleBuilder::new(
19//!     Font::try_from_bytes(include_bytes!("../assets/Roboto-Regular.ttf")).unwrap())
20//!     .font_size(16)
21//!     .text_color(Rgb565::WHITE)
22//!     .build();
23//!
24//! Text::new("Hello World!", Point::new(15, 30), style).draw(&mut display)?;
25//! ```
26//!
27//! # Antialiasing
28//!
29//! TrueType fonts are much nicer with antialiasing. However, embedded_graphics does not support
30//! retrieving current pixel while drawing, which prevents alpha blending and antialiasing.
31//!
32//! If you have a background color, the color is known and antialiasing is applied.
33//! Otherwise, you can use the [`AntiAliasing`] enum either to disable antialiasing or to define
34//! an antialiasing background color.
35
36#![cfg_attr(not(feature = "std"), no_std)]
37
38#[cfg(not(feature = "std"))]
39extern crate alloc;
40
41#[cfg(not(feature = "std"))]
42use num_traits::float::FloatCore;
43
44#[cfg(feature = "std")]
45use std as stdlib;
46
47#[cfg(not(feature = "std"))]
48mod stdlib {
49    pub use ::alloc::vec;
50    pub use core::*;
51}
52
53use stdlib::{f32, vec::Vec};
54
55use embedded_graphics::{
56    draw_target::DrawTarget,
57    pixelcolor::Rgb888,
58    prelude::*,
59    primitives::Rectangle,
60    text::{
61        renderer::{CharacterStyle, TextMetrics, TextRenderer},
62        Baseline, DecorationColor,
63    },
64};
65
66use rusttype::Font;
67
68/// Antialiasing can be challenging with embedded graphics since the background pixel is not known
69/// during the drawing process.
70#[derive(Debug, Clone)]
71pub enum AntiAliasing<C> {
72    /// Use the font background color (default), choose this if you defined a background color.
73    /// This is equivalent to SolidColor if the background color is defined,
74    /// This is equivalent to None if the background color is not defined
75    BackgroundColor,
76    /// Use given color as a "known" background color.
77    SolidColor(C),
78    /// Replace the alpha channel with a simple transparency (cutoff at 50%), choose this if you don't know the background at all.
79    None,
80}
81
82/// Style properties for text using a ttf and otf font.
83///
84/// A `FontTextStyle` can be applied to a [`Text`] object to define how the text is drawn.
85///
86#[derive(Debug, Clone)]
87pub struct FontTextStyle<C> {
88    /// Text color.
89    pub text_color: Option<C>,
90
91    /// Background color.
92    pub background_color: Option<C>,
93
94    /// How to apply antialiasing.
95    pub anti_aliasing: AntiAliasing<C>,
96
97    /// Underline color.
98    pub underline_color: DecorationColor<C>,
99
100    /// Strikethrough color.
101    pub strikethrough_color: DecorationColor<C>,
102
103    /// Font size.
104    pub font_size: u32,
105
106    /// Font from rusttype.
107    font: Font<'static>,
108}
109
110impl<C: PixelColor> FontTextStyle<C> {
111    /// Creates a text style with a transparent background.
112    pub fn new(font: Font<'static>, text_color: C, font_size: u32) -> Self {
113        FontTextStyleBuilder::new(font)
114            .text_color(text_color)
115            .font_size(font_size)
116            .build()
117    }
118
119    /// Resolves a decoration color.
120    fn resolve_decoration_color(&self, color: DecorationColor<C>) -> Option<C> {
121        match color {
122            DecorationColor::None => None,
123            DecorationColor::TextColor => self.text_color,
124            DecorationColor::Custom(c) => Some(c),
125        }
126    }
127
128    fn draw_background<D>(
129        &self,
130        width: u32,
131        position: Point,
132        target: &mut D,
133    ) -> Result<(), D::Error>
134    where
135        D: DrawTarget<Color = C>,
136    {
137        if width == 0 {
138            return Ok(());
139        }
140
141        if let Some(background_color) = self.background_color {
142            target.fill_solid(
143                &Rectangle::new(position, Size::new(width, self.font_size)),
144                background_color,
145            )?;
146        }
147
148        Ok(())
149    }
150
151    fn draw_strikethrough<D>(
152        &self,
153        width: u32,
154        position: Point,
155        target: &mut D,
156    ) -> Result<(), D::Error>
157    where
158        D: DrawTarget<Color = C>,
159    {
160        if let Some(strikethrough_color) = self.resolve_decoration_color(self.strikethrough_color) {
161            let top_left = position + Point::new(0, self.font_size as i32 / 2);
162            // small strikethrough width
163            let size = Size::new(width, self.font_size / 30 + 1);
164
165            target.fill_solid(&Rectangle::new(top_left, size), strikethrough_color)?;
166        }
167
168        Ok(())
169    }
170
171    fn draw_underline<D>(&self, width: u32, position: Point, target: &mut D) -> Result<(), D::Error>
172    where
173        D: DrawTarget<Color = C>,
174    {
175        if let Some(underline_color) = self.resolve_decoration_color(self.underline_color) {
176            let top_left = position + Point::new(0, self.font_size as i32);
177            // small underline width
178            let size = Size::new(width, self.font_size / 30 + 1);
179
180            target.fill_solid(&Rectangle::new(top_left, size), underline_color)?;
181        }
182
183        Ok(())
184    }
185}
186
187impl<C: PixelColor> CharacterStyle for FontTextStyle<C> {
188    type Color = C;
189
190    fn set_text_color(&mut self, text_color: Option<Self::Color>) {
191        self.text_color = text_color;
192    }
193
194    fn set_background_color(&mut self, background_color: Option<Self::Color>) {
195        self.background_color = background_color;
196        if background_color.is_some() {
197            // best antialiasing in this case
198            self.anti_aliasing = AntiAliasing::BackgroundColor;
199        }
200    }
201
202    fn set_underline_color(&mut self, underline_color: DecorationColor<Self::Color>) {
203        self.underline_color = underline_color;
204    }
205
206    fn set_strikethrough_color(&mut self, strikethrough_color: DecorationColor<Self::Color>) {
207        self.strikethrough_color = strikethrough_color;
208    }
209}
210
211impl<C> TextRenderer for FontTextStyle<C>
212where
213    C: PixelColor + Into<Rgb888> + From<Rgb888> + stdlib::fmt::Debug,
214{
215    type Color = C;
216
217    fn draw_string<D>(
218        &self,
219        text: &str,
220        position: Point,
221        _baseline: Baseline,
222        target: &mut D,
223    ) -> Result<Point, D::Error>
224    where
225        D: DrawTarget<Color = Self::Color>,
226    {
227        let scale = rusttype::Scale::uniform(self.font_size as f32);
228
229        let v_metrics = self.font.v_metrics(scale);
230        let offset = rusttype::point(0.0, v_metrics.ascent);
231
232        let glyphs: Vec<rusttype::PositionedGlyph> =
233            self.font.layout(text, scale, offset).collect();
234
235        let width = glyphs
236            .iter()
237            .rev()
238            .filter_map(|g| {
239                g.pixel_bounding_box()
240                    .map(|b| b.min.x as f32 + g.unpositioned().h_metrics().advance_width)
241            })
242            .next()
243            .unwrap_or(0.0)
244            .ceil() as i32;
245
246        let height = self.font_size as i32;
247
248        let mut pixels = Vec::new();
249
250        if let Some(text_color) = self.text_color {
251            for g in glyphs.iter() {
252                if let Some(bb) = g.pixel_bounding_box() {
253                    g.draw(|off_x, off_y, v| {
254                        let off_x = off_x as i32 + bb.min.x;
255                        let off_y = off_y as i32 + bb.min.y;
256                        // There's still a possibility that the glyph clips the boundaries of the bitmap
257                        if off_x >= 0 && off_x < width as i32 && off_y >= 0 && off_y < height as i32
258                        {
259                            let text_a = (v * 255.0) as u32;
260
261                            let bg_color = match self.anti_aliasing {
262                                AntiAliasing::BackgroundColor => self.background_color,
263                                AntiAliasing::SolidColor(c) => Some(c),
264                                AntiAliasing::None => None,
265                            };
266                            match bg_color {
267                                None => if text_a > 127 {
268                                    pixels.push(Pixel(
269                                        Point::new(position.x + off_x, position.y + off_y),
270                                        text_color
271                                    ));
272                                }
273                                Some(color) => {
274                                    let a = text_a as u16;
275                                    let fg = text_color.into();
276                                    let bg = color.into();
277                                    // blend with background color
278                                    let new_r = (a * fg.r() as u16 + (255-a) * bg.r() as u16) / 255;
279                                    let new_g = (a * fg.g() as u16 + (255-a) * bg.g() as u16) / 255;
280                                    let new_b = (a * fg.b() as u16 + (255-a) * bg.b() as u16) / 255;
281
282                                    pixels.push(Pixel(
283                                        Point::new(position.x + off_x, position.y + off_y),
284                                        Rgb888::new(new_r as u8, new_g as u8, new_b as u8).into(),
285                                    ));
286                                }
287                            }
288                        }
289                    });
290                }
291            }
292        }
293
294        self.draw_background(width as u32, position, target)?;
295        target.draw_iter(pixels)?;
296        self.draw_strikethrough(width as u32, position, target)?;
297        self.draw_underline(width as u32, position, target)?;
298
299        Ok(position + Point::new(width, 0))
300    }
301
302    fn draw_whitespace<D>(
303        &self,
304        width: u32,
305        position: Point,
306        _baseline: Baseline,
307        target: &mut D,
308    ) -> Result<Point, D::Error>
309    where
310        D: DrawTarget<Color = Self::Color>,
311    {
312        self.draw_background(width, position, target)?;
313        self.draw_strikethrough(width, position, target)?;
314        self.draw_underline(width, position, target)?;
315
316        Ok(position + Size::new(width, 0))
317    }
318
319    fn measure_string(&self, text: &str, position: Point, _baseline: Baseline) -> TextMetrics {
320        let scale = rusttype::Scale::uniform(self.font_size as f32);
321        let v_metrics = self.font.v_metrics(scale);
322        let offset = rusttype::point(0.0, v_metrics.ascent);
323
324        let glyphs: Vec<rusttype::PositionedGlyph> =
325            self.font.layout(text, scale, offset).collect();
326
327        let width = glyphs
328            .iter()
329            .rev()
330            .map(|g| g.position().x as f32 + g.unpositioned().h_metrics().advance_width)
331            .next()
332            .unwrap_or(0.0)
333            .ceil() as f64;
334
335        let size = Size::new(width as u32, self.font_size);
336
337        TextMetrics {
338            bounding_box: Rectangle::new(position, size),
339            next_position: position + size.x_axis(),
340        }
341    }
342
343    fn line_height(&self) -> u32 {
344        self.font_size
345    }
346}
347
348/// Text style builder for ttf and otf fonts.
349///
350/// Use this builder to create [`FontTextStyle`]s for [`Text`].
351pub struct FontTextStyleBuilder<C: PixelColor> {
352    style: FontTextStyle<C>,
353}
354
355impl<C: PixelColor> FontTextStyleBuilder<C> {
356    /// Create a new text style builder.
357    pub fn new(font: Font<'static>) -> Self {
358        Self {
359            style: FontTextStyle {
360                font,
361                background_color: None,
362                anti_aliasing: AntiAliasing::None,
363                font_size: 12,
364                text_color: None,
365                underline_color: DecorationColor::None,
366                strikethrough_color: DecorationColor::None,
367            },
368        }
369    }
370
371    /// Set the font size of the style in pixels.
372    pub fn font_size(mut self, font_size: u32) -> Self {
373        self.style.font_size = font_size;
374        self
375    }
376
377    /// Enable underline using the text color.
378    pub fn underline(mut self) -> Self {
379        self.style.underline_color = DecorationColor::TextColor;
380        self
381    }
382
383    /// Enable strikethrough using the text color.
384    pub fn strikethrough(mut self) -> Self {
385        self.style.strikethrough_color = DecorationColor::TextColor;
386        self
387    }
388
389    /// Set the text color.
390    pub fn text_color(mut self, text_color: C) -> Self {
391        self.style.text_color = Some(text_color);
392        self.style.anti_aliasing = AntiAliasing::BackgroundColor;
393        self
394    }
395
396    /// Set the background color.
397    pub fn background_color(mut self, background_color: C) -> Self {
398        self.style.background_color = Some(background_color);
399        self
400    }
401
402    /// Apply antialiasing over a known color.
403    pub fn anti_aliasing_color(mut self, background_color: C) -> Self {
404        self.style.anti_aliasing = AntiAliasing::SolidColor(background_color);
405        self
406    }
407
408    /// Enable underline with a custom color.
409    pub fn underline_with_color(mut self, underline_color: C) -> Self {
410        self.style.underline_color = DecorationColor::Custom(underline_color);
411        self
412    }
413
414    /// Enable strikethrough with a custom color.
415    pub fn strikethrough_with_color(mut self, strikethrough_color: C) -> Self {
416        self.style.strikethrough_color = DecorationColor::Custom(strikethrough_color);
417
418        self
419    }
420
421    /// Build the text style.
422    pub fn build(self) -> FontTextStyle<C> {
423        self.style
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use embedded_graphics::pixelcolor::Rgb888;
431
432    //TODO
433}