embedded_ttf/
lib.rs

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