Skip to main content

denise_text/
bitmap.rs

1//! The built-in bitmap font, as a glyph source.
2//!
3//! Always available, needs no files, no allocator beyond a scratch buffer and no
4//! feature flags. This is the font a panel gets if nobody chooses one, and the
5//! font it falls back to if the chosen one fails to load — which on a device that
6//! boots from flash and mounts a read-only root is not a hypothetical.
7
8use alloc::vec::Vec;
9
10use denise::Size;
11use denise_render::font::{self, BitmapFont, Glyph};
12
13use crate::source::{FontMetrics, GlyphId, GlyphMetrics, GlyphSource, Rasterised};
14
15/// The built-in five-by-seven font at whole-number scales.
16#[derive(Debug)]
17pub struct BitmapSource {
18    font: &'static BitmapFont,
19    scratch: Vec<u8>,
20}
21
22impl Default for BitmapSource {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl BitmapSource {
29    /// Wraps the built-in font.
30    pub fn new() -> Self {
31        Self {
32            font: &font::BUILT_IN,
33            scratch: Vec::new(),
34        }
35    }
36
37    /// Whole-number scale for a requested pixel size, at least 1.
38    ///
39    /// A bitmap font cannot be scaled continuously. Asking for 13 px and silently
40    /// getting 16 is how a layout ends up three pixels wrong for reasons nobody
41    /// can find, so [`snap_size`](GlyphSource::snap_size) reports what will
42    /// actually happen.
43    #[inline]
44    fn scale(size_px: u16) -> i32 {
45        (i32::from(size_px) / font::CELL_HEIGHT).max(1)
46    }
47
48    /// Bounding box of a glyph's ink, in unscaled cell coordinates.
49    ///
50    /// Trimming matters: a full stop is one pixel of ink in a five-by-eight cell,
51    /// and caching it untrimmed would spend forty bytes of a bounded atlas on
52    /// thirty-nine blank ones.
53    fn ink(glyph: &Glyph) -> Option<(i32, i32, i32, i32)> {
54        let mut left = font::CELL_WIDTH;
55        let mut right = 0;
56        let mut top = font::CELL_HEIGHT;
57        let mut bottom = 0;
58        for (row, bits) in glyph.iter().enumerate() {
59            if *bits == 0 {
60                continue;
61            }
62            let row = row as i32;
63            top = top.min(row);
64            bottom = bottom.max(row + 1);
65            for x in 0..font::CELL_WIDTH {
66                if bits & (0x80 >> x) != 0 {
67                    left = left.min(x);
68                    right = right.max(x + 1);
69                }
70            }
71        }
72        (right > left && bottom > top).then_some((left, top, right, bottom))
73    }
74
75    fn metrics_for(&self, ch: char, size_px: u16) -> GlyphMetrics {
76        let scale = Self::scale(size_px);
77        let glyph = self.font.glyph(ch);
78        // The baseline sits at the bottom of the seven-row body, one row above the
79        // bottom of the cell — which is the row descenders use.
80        let baseline_row = font::CELL_HEIGHT - 1;
81        match Self::ink(glyph) {
82            None => GlyphMetrics {
83                advance: font::ADVANCE * scale,
84                ..GlyphMetrics::default()
85            },
86            Some((left, top, right, bottom)) => GlyphMetrics {
87                advance: font::ADVANCE * scale,
88                bearing_x: left * scale,
89                bearing_y: (baseline_row - top) * scale,
90                size: Size::new(
91                    ((right - left) * scale) as u32,
92                    ((bottom - top) * scale) as u32,
93                ),
94            },
95        }
96    }
97}
98
99impl GlyphSource for BitmapSource {
100    fn name(&self) -> &str {
101        "built-in 5x7"
102    }
103
104    fn metrics(&self, size_px: u16) -> FontMetrics {
105        let scale = Self::scale(size_px);
106        FontMetrics {
107            ascent: (font::CELL_HEIGHT - 1) * scale,
108            descent: scale,
109            line_gap: (font::LINE_HEIGHT - font::CELL_HEIGHT) * scale,
110        }
111    }
112
113    fn glyph_id(&self, ch: char) -> Option<GlyphId> {
114        // Every character maps to something: an unmapped one gets the missing-
115        // character box, which is a visible defect rather than a silent gap.
116        Some(GlyphId::from_char(ch))
117    }
118
119    fn glyph_metrics(&mut self, glyph: GlyphId, size_px: u16) -> Option<GlyphMetrics> {
120        Some(self.metrics_for(glyph.as_char()?, size_px))
121    }
122
123    fn rasterise(&mut self, glyph: GlyphId, size_px: u16) -> Option<Rasterised<'_>> {
124        let ch = glyph.as_char()?;
125        let scale = Self::scale(size_px);
126        let metrics = self.metrics_for(ch, size_px);
127        if metrics.is_blank() {
128            self.scratch.clear();
129            return Some(Rasterised {
130                metrics,
131                coverage: &self.scratch,
132                stride: 0,
133            });
134        }
135
136        let glyph = *self.font.glyph(ch);
137        let (left, top, _, _) = Self::ink(&glyph).expect("non-blank glyph has ink");
138        let width = metrics.size.width as usize;
139        let height = metrics.size.height as usize;
140        self.scratch.clear();
141        self.scratch.resize(width * height, 0);
142
143        // A bitmap font has no partial coverage: every pixel is on or off, which
144        // is exactly what makes it cheap to blit and exactly why it looks like
145        // what it is at large sizes.
146        for y in 0..height {
147            let source_row = top + (y as i32 / scale);
148            let bits = glyph[source_row as usize];
149            if bits == 0 {
150                continue;
151            }
152            let row = &mut self.scratch[y * width..(y + 1) * width];
153            for (x, out) in row.iter_mut().enumerate() {
154                let source_x = left + (x as i32 / scale);
155                if bits & (0x80 >> source_x) != 0 {
156                    *out = 255;
157                }
158            }
159        }
160
161        Some(Rasterised {
162            metrics,
163            coverage: &self.scratch,
164            stride: width,
165        })
166    }
167
168    fn contains(&self, ch: char) -> bool {
169        self.font.contains(ch)
170    }
171
172    fn fallback_id(&self, ch: char) -> Option<GlyphId> {
173        Some(GlyphId::from_char(ch))
174    }
175
176    fn snap_size(&self, size_px: u16) -> u16 {
177        (Self::scale(size_px) * font::CELL_HEIGHT) as u16
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn sizes_snap_to_whole_scales() {
187        let source = BitmapSource::new();
188        assert_eq!(source.snap_size(8), 8);
189        assert_eq!(
190            source.snap_size(13),
191            8,
192            "13 px cannot be drawn; 8 is honest"
193        );
194        assert_eq!(source.snap_size(16), 16);
195        assert_eq!(source.snap_size(0), 8, "never smaller than one whole scale");
196    }
197
198    #[test]
199    fn a_space_has_advance_and_no_ink() {
200        let mut source = BitmapSource::new();
201        let metrics = source
202            .glyph_metrics(GlyphId::from_char(' '), 16)
203            .expect("space");
204        assert!(metrics.is_blank());
205        assert_eq!(metrics.advance, font::ADVANCE * 2);
206    }
207
208    #[test]
209    fn ink_is_trimmed_to_the_glyph() {
210        let mut source = BitmapSource::new();
211        // A full stop is one blob in the bottom left of the cell, nothing else.
212        let dot = source
213            .glyph_metrics(GlyphId::from_char('.'), 8)
214            .expect("full stop");
215        let m = source.glyph_metrics(GlyphId::from_char('M'), 8).expect("M");
216        assert!(
217            dot.size.width < m.size.width && dot.size.height < m.size.height,
218            "a full stop should not occupy an M-sized cell: {dot:?} vs {m:?}"
219        );
220        assert!(dot.advance == m.advance, "the font is monospace");
221    }
222
223    #[test]
224    fn a_descender_reaches_below_the_baseline() {
225        let mut source = BitmapSource::new();
226        let g = source.glyph_metrics(GlyphId::from_char('g'), 8).expect("g");
227        let o = source.glyph_metrics(GlyphId::from_char('o'), 8).expect("o");
228        // `bearing_y` is measured up from the baseline, so a descender's mask is
229        // taller than its bearing and an x-height letter's is not.
230        assert!(
231            g.size.height as i32 > g.bearing_y,
232            "g should hang below the baseline: {g:?}"
233        );
234        assert!(
235            o.size.height as i32 <= o.bearing_y,
236            "o should sit on the baseline: {o:?}"
237        );
238    }
239
240    #[test]
241    fn rasterising_fills_exactly_the_declared_extent() {
242        let mut source = BitmapSource::new();
243        for scale in [1u16, 2, 3] {
244            let size = scale * 8;
245            let glyph = source.rasterise(GlyphId::from_char('M'), size).expect("M");
246            let m = glyph.metrics;
247            assert_eq!(glyph.stride, m.size.width as usize);
248            assert_eq!(
249                glyph.coverage.len(),
250                (m.size.width * m.size.height) as usize,
251                "at {size} px"
252            );
253            assert!(glyph.coverage.contains(&255), "M has ink");
254            assert!(
255                glyph.coverage.iter().all(|&c| c == 0 || c == 255),
256                "a bitmap font has no partial coverage"
257            );
258        }
259    }
260
261    #[test]
262    fn scaling_multiplies_every_dimension() {
263        let mut source = BitmapSource::new();
264        let one = source.glyph_metrics(GlyphId::from_char('M'), 8).expect("M");
265        let three = source
266            .glyph_metrics(GlyphId::from_char('M'), 24)
267            .expect("M");
268        assert_eq!(three.size.width, one.size.width * 3);
269        assert_eq!(three.size.height, one.size.height * 3);
270        assert_eq!(three.advance, one.advance * 3);
271        assert_eq!(three.bearing_y, one.bearing_y * 3);
272    }
273
274    #[test]
275    fn line_height_matches_the_font_module() {
276        let source = BitmapSource::new();
277        assert_eq!(source.metrics(8).line_height(), font::LINE_HEIGHT);
278        assert_eq!(source.metrics(24).line_height(), font::LINE_HEIGHT * 3);
279    }
280
281    #[test]
282    fn an_unmapped_character_still_rasterises_as_the_missing_box() {
283        let mut source = BitmapSource::new();
284        assert!(!source.contains('\u{4e2d}'));
285        let glyph = source
286            .rasterise(GlyphId::from_char('\u{4e2d}'), 16)
287            .expect("fallback box");
288        assert!(!glyph.metrics.is_blank(), "a missing glyph must be visible");
289    }
290}