Skip to main content

goud_engine/rendering/text/
rasterizer.rs

1//! Glyph rasterization wrapper around `fontdue`.
2//!
3//! Converts font glyphs into CPU-side bitmaps with associated metrics,
4//! ready for atlas packing.
5
6/// Per-glyph metric data extracted during rasterization.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct GlyphMetrics {
9    /// Horizontal advance after rendering this glyph (in pixels).
10    pub advance_width: f32,
11    /// Horizontal bearing from the origin to the left edge of the glyph.
12    pub bearing_x: f32,
13    /// Y-offset of the glyph bounding box minimum (fontdue ymin).
14    pub bearing_y: f32,
15    /// Rasterized glyph width in pixels.
16    pub width: f32,
17    /// Rasterized glyph height in pixels.
18    pub height: f32,
19}
20
21/// A single rasterized glyph with its grayscale bitmap and metrics.
22///
23/// Note: The `width` and `height` fields store the bitmap dimensions as `u32` for
24/// array indexing and bitmap operations. The same values are duplicated in
25/// `metrics.width` and `metrics.height` as `f32` for layout calculations.
26#[derive(Debug, Clone)]
27pub struct RasterizedGlyph {
28    /// Grayscale coverage bitmap (one byte per pixel, 0..=255).
29    pub bitmap: Vec<u8>,
30    /// Bitmap width in pixels.
31    pub width: u32,
32    /// Bitmap height in pixels.
33    pub height: u32,
34    /// Metrics for this glyph.
35    pub metrics: GlyphMetrics,
36}
37
38/// Rasterizes a set of characters at the given pixel size.
39///
40/// Returns a `Vec` of `(char, RasterizedGlyph)` pairs in the same
41/// order as the input `chars` slice.
42///
43/// # Arguments
44///
45/// * `font`    - A parsed `fontdue::Font` reference.
46/// * `size_px` - The pixel height to rasterize at.
47/// * `chars`   - The characters to rasterize.
48pub fn rasterize_glyphs(
49    font: &fontdue::Font,
50    size_px: f32,
51    chars: &[char],
52) -> Vec<(char, RasterizedGlyph)> {
53    chars
54        .iter()
55        .map(|&ch| {
56            let (metrics, bitmap) = font.rasterize(ch, size_px);
57            let glyph = RasterizedGlyph {
58                bitmap,
59                width: metrics.width as u32,
60                height: metrics.height as u32,
61                metrics: GlyphMetrics {
62                    advance_width: metrics.advance_width,
63                    bearing_x: metrics.xmin as f32,
64                    bearing_y: metrics.ymin as f32,
65                    width: metrics.width as f32,
66                    height: metrics.height as f32,
67                },
68            };
69            (ch, glyph)
70        })
71        .collect()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    /// Helper: parse the test TTF fixture into a `fontdue::Font`.
79    fn test_font() -> fontdue::Font {
80        let bytes = include_bytes!("../../../test_assets/fonts/test_font.ttf");
81        fontdue::Font::from_bytes(bytes as &[u8], fontdue::FontSettings::default())
82            .expect("test_font.ttf should parse")
83    }
84
85    #[test]
86    fn test_rasterize_glyph_a_has_nonzero_dimensions() {
87        let font = test_font();
88        let glyphs = rasterize_glyphs(&font, 32.0, &['A']);
89
90        assert_eq!(glyphs.len(), 1);
91        let (ch, glyph) = &glyphs[0];
92        assert_eq!(*ch, 'A');
93        assert!(glyph.width > 0, "glyph width should be > 0");
94        assert!(glyph.height > 0, "glyph height should be > 0");
95    }
96
97    #[test]
98    fn test_rasterize_glyph_a_bitmap_has_nonzero_pixels() {
99        let font = test_font();
100        let glyphs = rasterize_glyphs(&font, 32.0, &['A']);
101        let (_, glyph) = &glyphs[0];
102
103        let nonzero_count = glyph.bitmap.iter().filter(|&&b| b > 0).count();
104        assert!(nonzero_count > 0, "bitmap should have non-zero pixels");
105    }
106
107    #[test]
108    fn test_rasterize_glyph_bitmap_length_matches_dimensions() {
109        let font = test_font();
110        let glyphs = rasterize_glyphs(&font, 24.0, &['B']);
111        let (_, glyph) = &glyphs[0];
112
113        let expected_len = (glyph.width * glyph.height) as usize;
114        assert_eq!(glyph.bitmap.len(), expected_len);
115    }
116
117    #[test]
118    fn test_rasterize_multiple_chars_preserves_order() {
119        let font = test_font();
120        let chars = vec!['X', 'Y', 'Z'];
121        let glyphs = rasterize_glyphs(&font, 16.0, &chars);
122
123        assert_eq!(glyphs.len(), 3);
124        assert_eq!(glyphs[0].0, 'X');
125        assert_eq!(glyphs[1].0, 'Y');
126        assert_eq!(glyphs[2].0, 'Z');
127    }
128
129    #[test]
130    fn test_rasterize_space_glyph_has_zero_dimensions() {
131        let font = test_font();
132        let glyphs = rasterize_glyphs(&font, 32.0, &[' ']);
133        let (_, glyph) = &glyphs[0];
134
135        // Space typically has zero width/height bitmap but nonzero advance.
136        assert_eq!(glyph.width, 0);
137        assert_eq!(glyph.height, 0);
138        assert!(
139            glyph.metrics.advance_width > 0.0,
140            "space should have positive advance"
141        );
142    }
143
144    #[test]
145    fn test_rasterize_metrics_advance_width_positive_for_visible_glyph() {
146        let font = test_font();
147        let glyphs = rasterize_glyphs(&font, 20.0, &['M']);
148        let (_, glyph) = &glyphs[0];
149
150        assert!(
151            glyph.metrics.advance_width > 0.0,
152            "advance_width should be positive for 'M'"
153        );
154    }
155}