rlvgl_core/plugins/
fontdue.rs

1//! Glyph rasterization using `fontdue`.
2use crate::widget::Color;
3use alloc::vec::Vec;
4use blake3;
5use fontdue::{Font, FontResult, FontSettings};
6pub use fontdue::{LineMetrics, Metrics};
7use once_cell::sync::OnceCell;
8use std::collections::HashMap;
9use std::sync::Mutex;
10
11/// Global font cache: hashed by blake3(font_data)
12static FONT_CACHE: OnceCell<Mutex<HashMap<u64, Font>>> = OnceCell::new();
13
14/// Hash the font data into a u64 using blake3
15fn hash_font_data(font_data: &[u8]) -> u64 {
16    let key = blake3::hash(font_data);
17    u64::from_le_bytes(key.as_bytes()[..8].try_into().unwrap())
18}
19
20/// Retrieve or insert a font into the cache
21fn get_cached_font(font_data: &[u8]) -> Font {
22    let key = hash_font_data(font_data);
23    let cache = FONT_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
24    let mut map = cache.lock().unwrap();
25
26    map.entry(key)
27        .or_insert_with(|| {
28            Font::from_bytes(font_data, FontSettings::default()).expect("valid font")
29        })
30        .clone()
31}
32
33/// Rasterize `ch` from the provided font data at the given pixel height.
34///
35/// Returns a grayscale bitmap along with its associated [`Metrics`]
36/// describing placement and advance information.
37///
38/// The bitmap contains alpha values in row-major order which callers may use
39/// to blend the glyph with an arbitrary text color.
40pub fn rasterize_glyph(font_data: &[u8], ch: char, px: f32) -> FontResult<(Metrics, Vec<u8>)> {
41    let font = get_cached_font(font_data);
42    Ok(font.rasterize(ch, px))
43}
44
45/// Retrieve horizontal line metrics for `font_data` at `px` height.
46///
47/// The returned [`LineMetrics`] structure provides ascent and descent values
48/// used to align glyph baselines.
49pub fn line_metrics(font_data: &[u8], px: f32) -> FontResult<LineMetrics> {
50    let font = Font::from_bytes(font_data, FontSettings::default())?;
51    font.horizontal_line_metrics(px)
52        .ok_or("missing horizontal metrics")
53}
54
55/// Surface that can blend individual pixels for text rendering.
56pub trait FontdueRenderTarget {
57    /// Return the width and height of the render surface in pixels.
58    fn dimensions(&self) -> (usize, usize);
59
60    /// Blend `color` at `(x, y)` using the provided alpha value.
61    fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8);
62}
63
64/// Render UTF‑8 text onto the provided [`FontdueRenderTarget`].
65pub fn render_text<R: FontdueRenderTarget>(
66    target: &mut R,
67    font_data: &[u8],
68    position: (i32, i32),
69    text: &str,
70    color: Color,
71    px: f32,
72) -> FontResult<()> {
73    let vm = line_metrics(font_data, px)?;
74    let ascent = vm.ascent.round() as i32;
75    let baseline = position.1 + ascent;
76    let (width, height) = target.dimensions();
77    let mut x_cursor = position.0;
78    for ch in text.chars() {
79        if let Ok((metrics, bitmap)) = rasterize_glyph(font_data, ch, px) {
80            let w = metrics.width as i32;
81            let h = metrics.height as i32;
82            let draw_y = baseline - ascent - metrics.ymin;
83            for y in 0..h {
84                let py = draw_y - y;
85                if py < 0 || (py as usize) >= height {
86                    continue;
87                }
88                for x in 0..w {
89                    let px_coord = x_cursor + metrics.xmin + x;
90                    if px_coord < 0 || (px_coord as usize) >= width {
91                        continue;
92                    }
93                    let alpha = bitmap[(h - 1 - y) as usize * metrics.width + x as usize];
94                    if alpha > 0 {
95                        target.blend_pixel(px_coord, py, color, alpha);
96                    }
97                }
98            }
99            x_cursor += metrics.advance_width.round() as i32;
100        }
101    }
102    Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    const FONT_DATA: &[u8] = include_bytes!("../../../assets/fonts/DejaVuSans.ttf");
110
111    #[test]
112    fn rasterize_a() {
113        let (metrics, bitmap) = rasterize_glyph(FONT_DATA, 'A', 16.0).unwrap();
114        assert_eq!(bitmap.len(), metrics.width * metrics.height);
115        assert!(metrics.width > 0 && metrics.height > 0);
116    }
117
118    #[test]
119    fn line_metrics_present() {
120        let vm = line_metrics(FONT_DATA, 16.0).unwrap();
121        assert!(vm.ascent > 0.0 && vm.descent < 0.0);
122    }
123
124    struct Surface {
125        buf: [u8; 32 * 32 * 4],
126    }
127
128    impl Surface {
129        fn new() -> Self {
130            Self {
131                buf: [0; 32 * 32 * 4],
132            }
133        }
134    }
135
136    impl FontdueRenderTarget for Surface {
137        fn dimensions(&self) -> (usize, usize) {
138            (32, 32)
139        }
140
141        fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8) {
142            if x >= 0 && y >= 0 && x < 32 && y < 32 {
143                let idx = ((y as usize) * 32 + x as usize) * 4;
144                let r = (color.0 as u16 * alpha as u16 / 255) as u8;
145                let g = (color.1 as u16 * alpha as u16 / 255) as u8;
146                let b = (color.2 as u16 * alpha as u16 / 255) as u8;
147                self.buf[idx] = r;
148                self.buf[idx + 1] = g;
149                self.buf[idx + 2] = b;
150                self.buf[idx + 3] = 0xff;
151            }
152        }
153    }
154
155    #[test]
156    fn render_text_draws_pixels() {
157        let mut surf = Surface::new();
158        render_text(
159            &mut surf,
160            FONT_DATA,
161            (0, 0),
162            "A",
163            Color(255, 255, 255, 255),
164            16.0,
165        )
166        .unwrap();
167        assert!(surf.buf.iter().any(|&p| p != 0));
168    }
169}