use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use denise::Size;
use fontdue::{Font, FontSettings};
use crate::source::{FontMetrics, GlyphId, GlyphMetrics, GlyphSource, Rasterised};
pub struct TrueTypeSource {
name: String,
font: Font,
scratch: Vec<u8>,
}
impl TrueTypeSource {
pub fn from_bytes(name: &str, data: &[u8]) -> Result<Self, String> {
let font = Font::from_bytes(data, FontSettings::default()).map_err(|e| e.to_owned())?;
Ok(Self {
name: name.to_owned(),
font,
scratch: Vec::new(),
})
}
#[inline]
pub const fn font(&self) -> &Font {
&self.font
}
fn convert(metrics: &fontdue::Metrics) -> GlyphMetrics {
GlyphMetrics {
advance: metrics.advance_width.round() as i32,
bearing_x: metrics.xmin,
bearing_y: metrics.ymin + metrics.height as i32,
size: Size::new(metrics.width as u32, metrics.height as u32),
}
}
}
impl GlyphSource for TrueTypeSource {
fn name(&self) -> &str {
&self.name
}
fn metrics(&self, size_px: u16) -> FontMetrics {
match self.font.horizontal_line_metrics(f32::from(size_px)) {
Some(line) => FontMetrics {
ascent: line.ascent.round() as i32,
descent: (-line.descent).round() as i32,
line_gap: line.line_gap.round() as i32,
},
None => FontMetrics {
ascent: i32::from(size_px) * 4 / 5,
descent: i32::from(size_px) / 5,
line_gap: i32::from(size_px) / 8,
},
}
}
fn glyph_id(&self, ch: char) -> Option<GlyphId> {
Some(GlyphId(u32::from(self.font.lookup_glyph_index(ch))))
}
fn glyph_metrics(&mut self, glyph: GlyphId, size_px: u16) -> Option<GlyphMetrics> {
Some(Self::convert(
&self
.font
.metrics_indexed(glyph.0 as u16, f32::from(size_px)),
))
}
fn rasterise(&mut self, glyph: GlyphId, size_px: u16) -> Option<Rasterised<'_>> {
let (metrics, coverage) = self
.font
.rasterize_indexed(glyph.0 as u16, f32::from(size_px));
let converted = Self::convert(&metrics);
self.scratch.clear();
self.scratch.extend_from_slice(&coverage);
Some(Rasterised {
metrics: converted,
coverage: &self.scratch,
stride: metrics.width,
})
}
fn contains(&self, ch: char) -> bool {
self.font.lookup_glyph_index(ch) != 0
}
fn fallback_id(&self, _ch: char) -> Option<GlyphId> {
Some(GlyphId(0))
}
}
impl core::fmt::Debug for TrueTypeSource {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TrueTypeSource")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_file_that_is_not_a_font_fails_with_a_reason() {
let error = TrueTypeSource::from_bytes("junk", b"not a font at all")
.expect_err("that is not a font");
assert!(!error.is_empty(), "the failure has to say something");
}
#[test]
fn fontdues_ymin_is_converted_to_a_top_bearing() {
let metrics = fontdue::Metrics {
xmin: 1,
ymin: -3,
width: 5,
height: 10,
advance_width: 6.4,
advance_height: 0.0,
bounds: fontdue::OutlineBounds::default(),
};
let converted = TrueTypeSource::convert(&metrics);
assert_eq!(converted.bearing_y, 7);
assert_eq!(converted.bearing_x, 1);
assert_eq!(converted.advance, 6, "6.4 rounds down");
assert_eq!(converted.size, Size::new(5, 10));
}
}