1use 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#[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 pub fn new() -> Self {
31 Self {
32 font: &font::BUILT_IN,
33 scratch: Vec::new(),
34 }
35 }
36
37 #[inline]
44 fn scale(size_px: u16) -> i32 {
45 (i32::from(size_px) / font::CELL_HEIGHT).max(1)
46 }
47
48 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 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 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 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 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 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}