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#![cfg_attr(not(feature = "std"), no_std)]
28
29#[cfg(not(feature = "std"))]
30extern crate alloc;
31
32#[cfg(not(feature = "std"))]
33use num_traits::float::FloatCore;
34
35#[cfg(feature = "std")]
36use std as stdlib;
37
38#[cfg(not(feature = "std"))]
39mod stdlib {
40    pub use ::alloc::vec;
41    pub use core::*;
42}
43
44use stdlib::{f32, vec::Vec};
45
46use embedded_graphics::{
47    draw_target::DrawTarget,
48    pixelcolor::Rgb888,
49    prelude::*,
50    primitives::Rectangle,
51    text::{
52        renderer::{CharacterStyle, TextMetrics, TextRenderer},
53        Baseline, DecorationColor,
54    },
55};
56
57use rusttype::Font;
58
59/// Style properties for text using a ttf and otf font.
60///
61/// A `FontTextStyle` can be applied to a [`Text`] object to define how the text is drawn.
62///
63#[derive(Debug, Clone)]
64pub struct FontTextStyle<C: PixelColor> {
65    /// Text color.
66    pub text_color: Option<C>,
67
68    /// Background color.
69    pub background_color: Option<C>,
70
71    /// Replace anti-aliasing with solid color (based on transparency value, cleaner when background color is None)
72    pub aliasing_filter: Option<u8>,
73
74    /// Underline color.
75    pub underline_color: DecorationColor<C>,
76
77    /// Strikethrough color.
78    pub strikethrough_color: DecorationColor<C>,
79
80    /// Font size.
81    pub font_size: u32,
82
83    /// Font from rusttype.
84    font: Font<'static>,
85}
86
87impl<C: PixelColor> FontTextStyle<C> {
88    /// Creates a text style with a transparent background.
89    pub fn new(font: Font<'static>, text_color: C, font_size: u32) -> Self {
90        FontTextStyleBuilder::new(font)
91            .text_color(text_color)
92            .font_size(font_size)
93            .build()
94    }
95
96    /// Resolves a decoration color.
97    fn resolve_decoration_color(&self, color: DecorationColor<C>) -> Option<C> {
98        match color {
99            DecorationColor::None => None,
100            DecorationColor::TextColor => self.text_color,
101            DecorationColor::Custom(c) => Some(c),
102        }
103    }
104
105    fn draw_background<D>(
106        &self,
107        width: u32,
108        position: Point,
109        target: &mut D,
110    ) -> Result<(), D::Error>
111    where
112        D: DrawTarget<Color = C>,
113    {
114        if width == 0 {
115            return Ok(());
116        }
117
118        if let Some(background_color) = self.background_color {
119            target.fill_solid(
120                &Rectangle::new(position, Size::new(width, self.font_size)),
121                background_color,
122            )?;
123        }
124
125        Ok(())
126    }
127
128    fn draw_strikethrough<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 let Some(strikethrough_color) = self.resolve_decoration_color(self.strikethrough_color) {
138            let top_left = position + Point::new(0, self.font_size as i32 / 2);
139            // small strikethrough width
140            let size = Size::new(width, self.font_size / 30 + 1);
141
142            target.fill_solid(&Rectangle::new(top_left, size), strikethrough_color)?;
143        }
144
145        Ok(())
146    }
147
148    fn draw_underline<D>(&self, width: u32, position: Point, target: &mut D) -> Result<(), D::Error>
149    where
150        D: DrawTarget<Color = C>,
151    {
152        if let Some(underline_color) = self.resolve_decoration_color(self.underline_color) {
153            let top_left = position + Point::new(0, self.font_size as i32);
154            // small underline width
155            let size = Size::new(width, self.font_size / 30 + 1);
156
157            target.fill_solid(&Rectangle::new(top_left, size), underline_color)?;
158        }
159
160        Ok(())
161    }
162}
163
164impl<C: PixelColor> CharacterStyle for FontTextStyle<C> {
165    type Color = C;
166
167    fn set_text_color(&mut self, text_color: Option<Self::Color>) {
168        self.text_color = text_color;
169    }
170
171    fn set_background_color(&mut self, background_color: Option<Self::Color>) {
172        self.background_color = background_color;
173    }
174
175    fn set_underline_color(&mut self, underline_color: DecorationColor<Self::Color>) {
176        self.underline_color = underline_color;
177    }
178
179    fn set_strikethrough_color(&mut self, strikethrough_color: DecorationColor<Self::Color>) {
180        self.strikethrough_color = strikethrough_color;
181    }
182}
183
184impl<C> TextRenderer for FontTextStyle<C>
185where
186    C: PixelColor + Into<Rgb888> + From<Rgb888> + stdlib::fmt::Debug,
187{
188    type Color = C;
189
190    fn draw_string<D>(
191        &self,
192        text: &str,
193        position: Point,
194        _baseline: Baseline,
195        target: &mut D,
196    ) -> Result<Point, D::Error>
197    where
198        D: DrawTarget<Color = Self::Color>,
199    {
200        let scale = rusttype::Scale::uniform(self.font_size as f32);
201
202        let v_metrics = self.font.v_metrics(scale);
203        let offset = rusttype::point(0.0, v_metrics.ascent);
204
205        let glyphs: Vec<rusttype::PositionedGlyph> =
206            self.font.layout(text, scale, offset).collect();
207
208        let width = glyphs
209            .iter()
210            .rev()
211            .filter_map(|g| {
212                g.pixel_bounding_box()
213                    .map(|b| b.min.x as f32 + g.unpositioned().h_metrics().advance_width)
214            })
215            .next()
216            .unwrap_or(0.0)
217            .ceil() as i32;
218
219        let height = self.font_size as i32;
220
221        let mut pixels = Vec::new();
222
223        if let Some(text_color) = self.text_color {
224            for g in glyphs.iter() {
225                if let Some(bb) = g.pixel_bounding_box() {
226                    g.draw(|off_x, off_y, v| {
227                        let off_x = off_x as i32 + bb.min.x;
228                        let off_y = off_y as i32 + bb.min.y;
229                        // There's still a possibility that the glyph clips the boundaries of the bitmap
230                        if off_x >= 0 && off_x < width as i32 && off_y >= 0 && off_y < height as i32
231                        {
232                            let c = (v * 255.0) as u32;
233
234                            let (text_r, text_g, text_b, text_a) =
235                                u32_to_rgba(c << 24 | (pixel_color_to_u32(text_color) & 0xFFFFFF));
236
237                            let (new_r, new_g, new_b) = rgba_background_to_rgb(
238                                text_r,
239                                text_g,
240                                text_b,
241                                text_a,
242                                self.background_color,
243                            );
244
245                            if self.aliasing_filter.is_none() && text_a > 0 {
246                                pixels.push(Pixel(
247                                    Point::new(position.x + off_x, position.y + off_y),
248                                    Rgb888::new(new_r, new_g, new_b).into(),
249                                ));
250                            } else if let Some(filter) = self.aliasing_filter {
251                                if text_a >= filter {
252                                    pixels.push(Pixel(
253                                        Point::new(position.x + off_x, position.y + off_y),
254                                        Rgb888::new(text_r, text_g, text_b).into(),
255                                    ));
256                                }
257                            }
258                        }
259                    });
260                }
261            }
262        }
263
264        self.draw_background(width as u32, position, target)?;
265        target.draw_iter(pixels)?;
266        self.draw_strikethrough(width as u32, position, target)?;
267        self.draw_underline(width as u32, position, target)?;
268
269        Ok(position + Point::new(width, 0))
270    }
271
272    fn draw_whitespace<D>(
273        &self,
274        width: u32,
275        position: Point,
276        _baseline: Baseline,
277        target: &mut D,
278    ) -> Result<Point, D::Error>
279    where
280        D: DrawTarget<Color = Self::Color>,
281    {
282        self.draw_background(width, position, target)?;
283        self.draw_strikethrough(width, position, target)?;
284        self.draw_underline(width, position, target)?;
285
286        Ok(position + Size::new(width, 0))
287    }
288
289    fn measure_string(&self, text: &str, position: Point, _baseline: Baseline) -> TextMetrics {
290        let scale = rusttype::Scale::uniform(self.font_size as f32);
291        let v_metrics = self.font.v_metrics(scale);
292        let offset = rusttype::point(0.0, v_metrics.ascent);
293
294        let glyphs: Vec<rusttype::PositionedGlyph> =
295            self.font.layout(text, scale, offset).collect();
296
297        let width = glyphs
298            .iter()
299            .rev()
300            .map(|g| g.position().x as f32 + g.unpositioned().h_metrics().advance_width)
301            .next()
302            .unwrap_or(0.0)
303            .ceil() as f64;
304
305        let size = Size::new(width as u32, self.font_size);
306
307        TextMetrics {
308            bounding_box: Rectangle::new(position, size),
309            next_position: position + size.x_axis(),
310        }
311    }
312
313    fn line_height(&self) -> u32 {
314        self.font_size
315    }
316}
317
318/// Text style builder for ttf and otf fonts.
319///
320/// Use this builder to create [`FontTextStyle`]s for [`Text`].
321pub struct FontTextStyleBuilder<C: PixelColor> {
322    style: FontTextStyle<C>,
323}
324
325impl<C: PixelColor> FontTextStyleBuilder<C> {
326    /// Create a new text style builder.
327    pub fn new(font: Font<'static>) -> Self {
328        Self {
329            style: FontTextStyle {
330                font,
331                background_color: None,
332                aliasing_filter: None,
333                font_size: 12,
334                text_color: None,
335                underline_color: DecorationColor::None,
336                strikethrough_color: DecorationColor::None,
337            },
338        }
339    }
340
341    /// Set the font size of the style in pixels.
342    pub fn font_size(mut self, font_size: u32) -> Self {
343        self.style.font_size = font_size;
344        self
345    }
346
347    /// Enable underline using the text color.
348    pub fn underline(mut self) -> Self {
349        self.style.underline_color = DecorationColor::TextColor;
350        self
351    }
352
353    /// Enable strikethrough using the text color.
354    pub fn strikethrough(mut self) -> Self {
355        self.style.strikethrough_color = DecorationColor::TextColor;
356        self
357    }
358
359    /// Set the text color.
360    pub fn text_color(mut self, text_color: C) -> Self {
361        self.style.text_color = Some(text_color);
362        self
363    }
364
365    /// Set the background color.
366    pub fn background_color(mut self, background_color: C) -> Self {
367        self.style.background_color = Some(background_color);
368        self
369    }
370
371    /// Replace antialiasing by an alpha channel cutoff.
372    /// Do not use, this will be replaced
373    pub fn aliasing_filter(mut self, alpha_filter: u8) -> Self {
374        self.style.aliasing_filter = Some(alpha_filter);
375        self
376    }
377
378    /// Enable underline with a custom color.
379    pub fn underline_with_color(mut self, underline_color: C) -> Self {
380        self.style.underline_color = DecorationColor::Custom(underline_color);
381        self
382    }
383
384    /// Enable strikethrough with a custom color.
385    pub fn strikethrough_with_color(mut self, strikethrough_color: C) -> Self {
386        self.style.strikethrough_color = DecorationColor::Custom(strikethrough_color);
387
388        self
389    }
390
391    /// Build the text style.
392    pub fn build(self) -> FontTextStyle<C> {
393        self.style
394    }
395}
396
397fn pixel_color_to_u32<C: Into<Rgb888>>(color: C) -> u32 {
398    let color = color.into();
399
400    0xFF000000 | ((color.r() as u32) << 16) | ((color.g() as u32) << 8) | (color.b() as u32)
401}
402
403fn u32_to_rgba(color: u32) -> (u8, u8, u8, u8) {
404    (
405        ((color & 0x00FF0000) >> 16) as u8,
406        ((color & 0x0000FF00) >> 8) as u8,
407        (color & 0x000000FF) as u8,
408        ((color & 0xFF000000) >> 24) as u8,
409    )
410}
411
412fn rgba_to_rgb(r: u8, g: u8, b: u8, a: u8) -> (u8, u8, u8) {
413    let alpha = a as f32 / 255.;
414
415    (
416        (r as f32 * alpha).ceil() as u8,
417        (g as f32 * alpha).ceil() as u8,
418        (b as f32 * alpha).ceil() as u8,
419    )
420}
421
422fn rgba_background_to_rgb<C: Into<Rgb888>>(
423    r: u8,
424    g: u8,
425    b: u8,
426    a: u8,
427    background_color: Option<C>,
428) -> (u8, u8, u8) {
429    if let Some(background_color) = background_color {
430        let background_color_data = pixel_color_to_u32(background_color);
431        let (br, bg, bb, ba) = u32_to_rgba(background_color_data);
432        let (br, bg, bb) = rgba_to_rgb(br, bg, bb, ba);
433
434        let alpha = a as f32 / 255.;
435        let b_alpha = 1. - alpha;
436
437        // blend with background color
438        return (
439            ((r as f32 * alpha) + br as f32 * b_alpha).ceil() as u8,
440            ((g as f32 * alpha) + bg as f32 * b_alpha).ceil() as u8,
441            ((b as f32 * alpha) + bb as f32 * b_alpha).ceil() as u8,
442        );
443    }
444
445    // this is equivalent to blending with black
446    rgba_to_rgb(r, g, b, a)
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use embedded_graphics::pixelcolor::Rgb888;
453
454    #[test]
455    fn test_pixel_color_to_u32() {
456        assert_eq!(4294967295, pixel_color_to_u32(Rgb888::WHITE));
457        assert_eq!(4278190080, pixel_color_to_u32(Rgb888::BLACK));
458    }
459
460    #[test]
461    fn test_u32_to_rgba() {
462        assert_eq!((255, 255, 255, 255), u32_to_rgba(4294967295));
463        assert_eq!((0, 0, 0, 255), u32_to_rgba(4278190080));
464    }
465
466    #[test]
467    fn test_rgba_to_rgb() {
468        assert_eq!((255, 255, 255), rgba_to_rgb(255, 255, 255, 255));
469        assert_eq!((100, 100, 100), rgba_to_rgb(255, 255, 255, 100));
470    }
471
472    #[test]
473    fn test_rgba_background_to_rgb() {
474        assert_eq!(
475            (255, 255, 255),
476            rgba_background_to_rgb::<Rgb888>(255, 255, 255, 255, None)
477        );
478        assert_eq!(
479            (100, 100, 100),
480            rgba_background_to_rgb(255, 255, 255, 100, Some(Rgb888::BLACK))
481        );
482    }
483}