use alloc::vec::Vec;
use denise::Size;
use denise_render::font::{self, BitmapFont, Glyph};
use crate::source::{FontMetrics, GlyphId, GlyphMetrics, GlyphSource, Rasterised};
#[derive(Debug)]
pub struct BitmapSource {
font: &'static BitmapFont,
scratch: Vec<u8>,
}
impl Default for BitmapSource {
fn default() -> Self {
Self::new()
}
}
impl BitmapSource {
pub fn new() -> Self {
Self {
font: &font::BUILT_IN,
scratch: Vec::new(),
}
}
#[inline]
fn scale(size_px: u16) -> i32 {
(i32::from(size_px) / font::CELL_HEIGHT).max(1)
}
fn ink(glyph: &Glyph) -> Option<(i32, i32, i32, i32)> {
let mut left = font::CELL_WIDTH;
let mut right = 0;
let mut top = font::CELL_HEIGHT;
let mut bottom = 0;
for (row, bits) in glyph.iter().enumerate() {
if *bits == 0 {
continue;
}
let row = row as i32;
top = top.min(row);
bottom = bottom.max(row + 1);
for x in 0..font::CELL_WIDTH {
if bits & (0x80 >> x) != 0 {
left = left.min(x);
right = right.max(x + 1);
}
}
}
(right > left && bottom > top).then_some((left, top, right, bottom))
}
fn metrics_for(&self, ch: char, size_px: u16) -> GlyphMetrics {
let scale = Self::scale(size_px);
let glyph = self.font.glyph(ch);
let baseline_row = font::CELL_HEIGHT - 1;
match Self::ink(glyph) {
None => GlyphMetrics {
advance: font::ADVANCE * scale,
..GlyphMetrics::default()
},
Some((left, top, right, bottom)) => GlyphMetrics {
advance: font::ADVANCE * scale,
bearing_x: left * scale,
bearing_y: (baseline_row - top) * scale,
size: Size::new(
((right - left) * scale) as u32,
((bottom - top) * scale) as u32,
),
},
}
}
}
impl GlyphSource for BitmapSource {
fn name(&self) -> &str {
"built-in 5x7"
}
fn metrics(&self, size_px: u16) -> FontMetrics {
let scale = Self::scale(size_px);
FontMetrics {
ascent: (font::CELL_HEIGHT - 1) * scale,
descent: scale,
line_gap: (font::LINE_HEIGHT - font::CELL_HEIGHT) * scale,
}
}
fn glyph_id(&self, ch: char) -> Option<GlyphId> {
Some(GlyphId::from_char(ch))
}
fn glyph_metrics(&mut self, glyph: GlyphId, size_px: u16) -> Option<GlyphMetrics> {
Some(self.metrics_for(glyph.as_char()?, size_px))
}
fn rasterise(&mut self, glyph: GlyphId, size_px: u16) -> Option<Rasterised<'_>> {
let ch = glyph.as_char()?;
let scale = Self::scale(size_px);
let metrics = self.metrics_for(ch, size_px);
if metrics.is_blank() {
self.scratch.clear();
return Some(Rasterised {
metrics,
coverage: &self.scratch,
stride: 0,
});
}
let glyph = *self.font.glyph(ch);
let (left, top, _, _) = Self::ink(&glyph).expect("non-blank glyph has ink");
let width = metrics.size.width as usize;
let height = metrics.size.height as usize;
self.scratch.clear();
self.scratch.resize(width * height, 0);
for y in 0..height {
let source_row = top + (y as i32 / scale);
let bits = glyph[source_row as usize];
if bits == 0 {
continue;
}
let row = &mut self.scratch[y * width..(y + 1) * width];
for (x, out) in row.iter_mut().enumerate() {
let source_x = left + (x as i32 / scale);
if bits & (0x80 >> source_x) != 0 {
*out = 255;
}
}
}
Some(Rasterised {
metrics,
coverage: &self.scratch,
stride: width,
})
}
fn contains(&self, ch: char) -> bool {
self.font.contains(ch)
}
fn fallback_id(&self, ch: char) -> Option<GlyphId> {
Some(GlyphId::from_char(ch))
}
fn snap_size(&self, size_px: u16) -> u16 {
(Self::scale(size_px) * font::CELL_HEIGHT) as u16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sizes_snap_to_whole_scales() {
let source = BitmapSource::new();
assert_eq!(source.snap_size(8), 8);
assert_eq!(
source.snap_size(13),
8,
"13 px cannot be drawn; 8 is honest"
);
assert_eq!(source.snap_size(16), 16);
assert_eq!(source.snap_size(0), 8, "never smaller than one whole scale");
}
#[test]
fn a_space_has_advance_and_no_ink() {
let mut source = BitmapSource::new();
let metrics = source
.glyph_metrics(GlyphId::from_char(' '), 16)
.expect("space");
assert!(metrics.is_blank());
assert_eq!(metrics.advance, font::ADVANCE * 2);
}
#[test]
fn ink_is_trimmed_to_the_glyph() {
let mut source = BitmapSource::new();
let dot = source
.glyph_metrics(GlyphId::from_char('.'), 8)
.expect("full stop");
let m = source.glyph_metrics(GlyphId::from_char('M'), 8).expect("M");
assert!(
dot.size.width < m.size.width && dot.size.height < m.size.height,
"a full stop should not occupy an M-sized cell: {dot:?} vs {m:?}"
);
assert!(dot.advance == m.advance, "the font is monospace");
}
#[test]
fn a_descender_reaches_below_the_baseline() {
let mut source = BitmapSource::new();
let g = source.glyph_metrics(GlyphId::from_char('g'), 8).expect("g");
let o = source.glyph_metrics(GlyphId::from_char('o'), 8).expect("o");
assert!(
g.size.height as i32 > g.bearing_y,
"g should hang below the baseline: {g:?}"
);
assert!(
o.size.height as i32 <= o.bearing_y,
"o should sit on the baseline: {o:?}"
);
}
#[test]
fn rasterising_fills_exactly_the_declared_extent() {
let mut source = BitmapSource::new();
for scale in [1u16, 2, 3] {
let size = scale * 8;
let glyph = source.rasterise(GlyphId::from_char('M'), size).expect("M");
let m = glyph.metrics;
assert_eq!(glyph.stride, m.size.width as usize);
assert_eq!(
glyph.coverage.len(),
(m.size.width * m.size.height) as usize,
"at {size} px"
);
assert!(glyph.coverage.contains(&255), "M has ink");
assert!(
glyph.coverage.iter().all(|&c| c == 0 || c == 255),
"a bitmap font has no partial coverage"
);
}
}
#[test]
fn scaling_multiplies_every_dimension() {
let mut source = BitmapSource::new();
let one = source.glyph_metrics(GlyphId::from_char('M'), 8).expect("M");
let three = source
.glyph_metrics(GlyphId::from_char('M'), 24)
.expect("M");
assert_eq!(three.size.width, one.size.width * 3);
assert_eq!(three.size.height, one.size.height * 3);
assert_eq!(three.advance, one.advance * 3);
assert_eq!(three.bearing_y, one.bearing_y * 3);
}
#[test]
fn line_height_matches_the_font_module() {
let source = BitmapSource::new();
assert_eq!(source.metrics(8).line_height(), font::LINE_HEIGHT);
assert_eq!(source.metrics(24).line_height(), font::LINE_HEIGHT * 3);
}
#[test]
fn an_unmapped_character_still_rasterises_as_the_missing_box() {
let mut source = BitmapSource::new();
assert!(!source.contains('\u{4e2d}'));
let glyph = source
.rasterise(GlyphId::from_char('\u{4e2d}'), 16)
.expect("fallback box");
assert!(!glyph.metrics.is_blank(), "a missing glyph must be visible");
}
}