Skip to main content

eg_fontdue/
lib.rs

1//! # eg-fontdue - A TTF/OTF renderer for `embedded_graphics`
2//!
3//! `eg-fontdue` implements `embedded_graphics`'s [`TextRenderer`](https://docs.rs/embedded-graphics/latest/embedded_graphics/text/renderer/trait.TextRenderer.html) and [`CharacterStyle`](https://docs.rs/embedded-graphics/latest/embedded_graphics/text/renderer/trait.CharacterStyle.html) traits over the [`fontdue`](https://github.com/mooman222/fontdue) crate. Allowing for the rendering of arbitrary TTF/OTF fonts at any size.
4//!
5//! Basic anti-aliasing is implemented, the anti-aliasing engine automatically chooses the inverse of the text color as the background color, if you do not want this, specify an anti-aliasing color with `FontdueTextStyle::with_aa_color`.
6//!
7//! Since glyphs have to be manually rasterized, rendering times may vary, `alloc` is also required
8//!
9//! ```rust
10//! use embedded_graphics::{pixelcolor::BinaryColor, text::Text};
11//!
12//! // Load a font using `fontdue`
13//! let ttf_font_data = include_bytes!("assets/font.ttf");
14//! let font = fontdue::Font::from_bytes(ttf_font_data, fontdue::FontSettings::default())?;
15//!
16//! // Specify color and location
17//! let style = eg_fontdue::FontdueTextStyle::new(&font, BinaryColor::Off, 40);
18//! let rendered_text = Text::new("Hello!", Point::new(101, 100), style);
19//!
20//! // Render
21//! rendered_text.draw(display)?;
22//! ```
23#![no_std]
24#![warn(missing_docs)]
25#![warn(missing_debug_implementations)]
26#![warn(missing_copy_implementations)]
27#![warn(trivial_casts)]
28#![warn(trivial_numeric_casts)]
29#![deny(unsafe_code)]
30#![deny(unstable_features)]
31#![warn(unused_import_braces)]
32#![warn(unused_qualifications)]
33#![deny(rustdoc::broken_intra_doc_links)]
34#![deny(rustdoc::private_intra_doc_links)]
35
36use embedded_graphics::{
37    pixelcolor::{Gray8, Rgb888},
38    prelude::*,
39    primitives::Rectangle,
40    text::{
41        renderer::{CharacterStyle, TextMetrics, TextRenderer},
42        Alignment, Baseline,
43    },
44};
45use fontdue::layout::{Layout, TextStyle, WrapStyle};
46
47/// Text vertical alignment
48#[derive(Debug, Clone, Copy, Default)]
49pub enum VerticalAlign {
50    #[default]
51    /// Aligns to top of max height
52    Top,
53    /// Aligns to bottom of max height
54    Bottom,
55    /// Aligns to middle of max height
56    Middle,
57}
58
59fn alpha_composite(background: Rgb888, foreground: Rgb888, alpha: u8) -> Rgb888 {
60    let (r1, g1, b1) = (
61        foreground.r() as u16,
62        foreground.g() as u16,
63        foreground.b() as u16,
64    );
65    let (r2, g2, b2) = (
66        background.r() as u16,
67        background.g() as u16,
68        background.b() as u16,
69    );
70
71    let alpha = alpha as u16;
72    let p = 255 - alpha;
73
74    Rgb888::new(
75        ((r1 * alpha + r2 * p) / 255) as u8,
76        ((g1 * alpha + g2 * p) / 255) as u8,
77        ((b1 * alpha + b2 * p) / 255) as u8,
78    )
79}
80
81fn inverse(col: Rgb888) -> Rgb888 {
82    let (r, g, b) = (col.r(), col.g(), col.b());
83    Rgb888::new(Rgb888::MAX_R - r, Rgb888::MAX_G - g, Rgb888::MAX_B - b)
84}
85
86/// A text renderer for TTF and OTF fonts
87#[derive(Debug, Clone, Copy)]
88pub struct FontdueTextStyle<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> {
89    /// A SFNT font
90    pub font: &'a fontdue::Font,
91    /// The color the text will be rendered in
92    pub color: C,
93    /// The color the font anti-aliases towards
94    pub antialias_color: C,
95    /// Size in pixels
96    pub size: u16,
97    /// Maximum Width
98    pub max_width: Option<f32>,
99    /// Maximum Height
100    pub max_height: Option<f32>,
101    /// Horizontal Alignment
102    pub horiz_align: Alignment,
103    /// Vertical Alignment
104    pub vert_align_not_center: VerticalAlign,
105    /// Line Height
106    pub line_height: f32,
107    /// Wraps words (if false, wraps letters)
108    pub word_wrap: bool,
109    /// Wrap hard breaks
110    pub wrap_hard_breaks: bool,
111}
112
113impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> FontdueTextStyle<'a, C> {
114    fn ascent(&self) -> u16 {
115        self.font
116            .horizontal_line_metrics(self.size as f32)
117            .unwrap()
118            .ascent as u16
119    }
120
121    fn descent(&self) -> u16 {
122        self.font
123            .horizontal_line_metrics(self.size as f32)
124            .unwrap()
125            .descent as u16
126    }
127
128    fn baseline_offset(&self, baseline: Baseline) -> i32 {
129        match baseline {
130            Baseline::Top => self.ascent().saturating_sub(1) as i32,
131            Baseline::Bottom => -(self.descent() as i32),
132            Baseline::Middle => (self.ascent() as i32 - self.descent() as i32) / 2,
133            Baseline::Alphabetic => 0,
134        }
135    }
136}
137
138impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> FontdueTextStyle<'a, C>
139where
140    Rgb888: From<C>,
141{
142    /// Constructs a new text style
143    pub fn new(font: &'a fontdue::Font, color: C, size: u16) -> Self {
144        Self {
145            font,
146            color,
147            antialias_color: inverse(Rgb888::from(color)).into(),
148            size,
149            max_width: None,
150            max_height: None,
151            horiz_align: Alignment::Left,
152            vert_align_not_center: VerticalAlign::Top,
153            line_height: 1.0 * size as f32,
154            word_wrap: true,
155            wrap_hard_breaks: true,
156        }
157    }
158
159    /// Constructs a new text style with an antialiasing color
160    pub fn with_aa_color(font: &'a fontdue::Font, color: C, aa_color: C, size: u16) -> Self {
161        Self {
162            font,
163            color,
164            antialias_color: aa_color,
165            size,
166            max_width: None,
167            max_height: None,
168            horiz_align: Alignment::Left,
169            vert_align_not_center: VerticalAlign::Top,
170            line_height: 1.0 * size as f32,
171            word_wrap: true,
172            wrap_hard_breaks: true,
173        }
174    }
175
176    /// Renders a glyph at a certain location
177    pub fn render_glyph_at<D: DrawTarget<Color = C>>(
178        &self,
179        idx: u16,
180        x: f32,
181        y: f32,
182        target: &mut D,
183    ) -> Result<Point, D::Error> {
184        let (m, d) = self.font.rasterize_indexed(idx, self.size as f32);
185
186        let bbx = Rectangle::new(
187            Point {
188                x: x as i32,
189                y: y as i32,
190            },
191            Size {
192                width: m.width as u32,
193                height: m.height as u32,
194            },
195        );
196
197        let mut data_iter = d.iter();
198
199        let c8: Rgb888 = self.color.into();
200        let bc8: Rgb888 = self.antialias_color.into();
201
202        bbx.points()
203            .filter_map(|p| {
204                let l = *(data_iter.next()?);
205                if l != 0 {
206                    Some(Pixel(p, alpha_composite(bc8, c8, l).into()))
207                } else {
208                    None
209                }
210            })
211            .draw(target)?;
212
213        Ok(Point::new(m.advance_width as i32, m.advance_height as i32))
214    }
215
216    /// Generates a font layout from the text style
217    pub fn generate_layout(&self, text: &str, position: Point) -> Layout {
218        let mut layout = Layout::new(fontdue::layout::CoordinateSystem::PositiveYDown);
219        let settings = fontdue::layout::LayoutSettings {
220            x: position.x as f32,
221            y: position.y as f32,
222            line_height: self.line_height,
223            max_height: self.max_height,
224            max_width: self.max_width,
225            wrap_style: match self.word_wrap {
226                true => WrapStyle::Word,
227                false => WrapStyle::Letter,
228            },
229            wrap_hard_breaks: self.wrap_hard_breaks,
230            horizontal_align: match self.horiz_align {
231                Alignment::Center => fontdue::layout::HorizontalAlign::Center,
232                Alignment::Left => fontdue::layout::HorizontalAlign::Left,
233                Alignment::Right => fontdue::layout::HorizontalAlign::Right,
234            },
235            vertical_align: match self.vert_align_not_center {
236                VerticalAlign::Middle => fontdue::layout::VerticalAlign::Middle,
237                VerticalAlign::Top => fontdue::layout::VerticalAlign::Top,
238                VerticalAlign::Bottom => fontdue::layout::VerticalAlign::Bottom,
239            },
240        };
241
242        layout.reset(&settings);
243
244        layout.append(&[self.font], &TextStyle::new(text, self.size as f32, 0));
245
246        layout
247    }
248}
249
250impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> CharacterStyle
251    for FontdueTextStyle<'a, C>
252{
253    type Color = C;
254
255    fn set_text_color(&mut self, text_color: Option<C>) {
256        // TODO: support transparent text
257        if let Some(color) = text_color {
258            self.color = color;
259        }
260    }
261
262    // TODO: implement additional methods
263}
264
265impl<'a, C: PixelColor + From<Gray8> + From<Rgb888> + Into<Rgb888>> TextRenderer
266    for FontdueTextStyle<'a, C>
267where
268    Rgb888: From<C>,
269{
270    type Color = C;
271
272    fn draw_string<D>(
273        &self,
274        text: &str,
275        position: Point,
276        baseline: Baseline,
277        target: &mut D,
278    ) -> Result<Point, D::Error>
279    where
280        Rgb888: From<C>,
281        D: DrawTarget<Color = Self::Color>,
282    {
283        let mut position = position + Point::new(0, self.baseline_offset(baseline));
284        let layout = self.generate_layout(text, position);
285
286        for glyph in layout.glyphs() {
287            position += self.render_glyph_at(
288                glyph.key.glyph_index,
289                glyph.x,
290                glyph.y - (self.baseline_offset(Baseline::Middle) as f32 * 2.0),
291                target,
292            )?;
293        }
294
295        Ok(position)
296    }
297
298    fn draw_whitespace<D>(
299        &self,
300        width: u32,
301        position: Point,
302        baseline: Baseline,
303        _: &mut D,
304    ) -> Result<Point, D::Error>
305    where
306        Rgb888: From<C>,
307        D: DrawTarget<Color = Self::Color>,
308    {
309        let position = position + Point::new(0, self.baseline_offset(baseline));
310
311        Ok(position + Size::new(width, 0))
312    }
313
314    fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics {
315        let position = position + Point::new(0, self.baseline_offset(baseline));
316        let layout = self.generate_layout(text, position);
317
318        let mut dx = 0.0;
319        let mut dy = 0.0;
320        for met in layout.glyphs().iter().map(|g| {
321            self.font
322                .metrics_indexed(g.key.glyph_index, self.size as f32)
323        }) {
324            dy += met.advance_height;
325            dx += met.advance_width;
326        }
327
328        let bounding_box = Rectangle::new(
329            position - Size::new(0, self.ascent().saturating_sub(1) as u32 + (dy as u32)),
330            Size::new(dx as u32, self.line_height()),
331        );
332
333        TextMetrics {
334            bounding_box,
335            next_position: position + Size::new(dx as u32, 0),
336        }
337    }
338
339    fn line_height(&self) -> u32 {
340        self.line_height as u32
341    }
342}