extern crate std;
use crate::font::{FontLineMetrics, FontMetrics, GlyphInfo};
use crate::widget::Color;
use alloc::vec::Vec;
use blake3;
use fontdue::{Font, FontResult, FontSettings};
pub use fontdue::{LineMetrics, Metrics};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Mutex;
static FONT_CACHE: OnceCell<Mutex<HashMap<u64, Font>>> = OnceCell::new();
fn hash_font_data(font_data: &[u8]) -> u64 {
let key = blake3::hash(font_data);
u64::from_le_bytes(key.as_bytes()[..8].try_into().unwrap())
}
fn get_cached_font(font_data: &[u8]) -> Font {
let key = hash_font_data(font_data);
let cache = FONT_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut map = cache.lock().unwrap();
map.entry(key)
.or_insert_with(|| {
Font::from_bytes(font_data, FontSettings::default()).expect("valid font")
})
.clone()
}
fn round_to_i32(value: f32) -> i32 {
if value.is_nan() {
0
} else if value >= 0.0 {
(value + 0.5) as i32
} else {
(value - 0.5) as i32
}
}
#[derive(Debug, Clone, Copy)]
pub struct FontdueFont<'a> {
pub font_data: &'a [u8],
pub px: f32,
}
impl<'a> FontdueFont<'a> {
pub const fn new(font_data: &'a [u8], px: f32) -> Self {
Self { font_data, px }
}
}
pub fn rasterize_glyph(font_data: &[u8], ch: char, px: f32) -> FontResult<(Metrics, Vec<u8>)> {
let font = get_cached_font(font_data);
Ok(font.rasterize(ch, px))
}
pub fn line_metrics(font_data: &[u8], px: f32) -> FontResult<LineMetrics> {
let font = Font::from_bytes(font_data, FontSettings::default())?;
font.horizontal_line_metrics(px)
.ok_or("missing horizontal metrics")
}
pub trait FontdueRenderTarget {
fn dimensions(&self) -> (usize, usize);
fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8);
}
pub fn render_text<R: FontdueRenderTarget>(
target: &mut R,
font_data: &[u8],
position: (i32, i32),
text: &str,
color: Color,
px: f32,
) -> FontResult<()> {
let vm = line_metrics(font_data, px)?;
let ascent = round_to_i32(vm.ascent);
let baseline = position.1 + ascent;
let (width, height) = target.dimensions();
let mut x_cursor = position.0;
for ch in text.chars() {
if let Ok((metrics, bitmap)) = rasterize_glyph(font_data, ch, px) {
let w = metrics.width as i32;
let h = metrics.height as i32;
let draw_y = baseline - ascent - metrics.ymin;
for y in 0..h {
let py = draw_y - y;
if py < 0 || (py as usize) >= height {
continue;
}
for x in 0..w {
let px_coord = x_cursor + metrics.xmin + x;
if px_coord < 0 || (px_coord as usize) >= width {
continue;
}
let alpha = bitmap[(h - 1 - y) as usize * metrics.width + x as usize];
if alpha > 0 {
target.blend_pixel(px_coord, py, color, alpha);
}
}
}
x_cursor += round_to_i32(metrics.advance_width);
}
}
Ok(())
}
impl FontMetrics for FontdueFont<'_> {
fn glyph_metrics(&self, ch: char) -> Option<GlyphInfo> {
let font = get_cached_font(self.font_data);
let metrics = font.metrics(ch, self.px);
let advance_fp16 = round_to_i32(metrics.advance_width * 16.0).clamp(0, u16::MAX as i32);
let bearing_y = metrics.ymin + metrics.height as i32;
Some(GlyphInfo {
advance_fp16: advance_fp16 as u16,
bearing_x: metrics.xmin.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
bearing_y: bearing_y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
width: metrics.width.min(u16::MAX as usize) as u16,
height: metrics.height.min(u16::MAX as usize) as u16,
})
}
fn line_metrics(&self) -> FontLineMetrics {
let Ok(metrics) = line_metrics(self.font_data, self.px) else {
let px = round_to_i32(self.px).max(0).min(u16::MAX as i32) as u16;
return FontLineMetrics {
line_height: px,
ascent: px.min(i16::MAX as u16) as i16,
descent: 0,
};
};
let line_height = round_to_i32(metrics.new_line_size)
.max(0)
.min(u16::MAX as i32) as u16;
let ascent = round_to_i32(metrics.ascent).clamp(i16::MIN as i32, i16::MAX as i32) as i16;
let descent = round_to_i32(-metrics.descent).max(0).min(i16::MAX as i32) as i16;
FontLineMetrics {
line_height,
ascent,
descent,
}
}
fn glyph_coverage_row(&self, ch: char, row: u16, x_offset: u16, coverage: &mut [u8]) -> bool {
let Ok((metrics, bitmap)) = rasterize_glyph(self.font_data, ch, self.px) else {
return false;
};
if row as usize >= metrics.height {
coverage.fill(0);
return true;
}
let row_start = row as usize * metrics.width;
for (offset, alpha) in coverage.iter_mut().enumerate() {
let col = x_offset as usize + offset;
*alpha = if col < metrics.width {
bitmap.get(row_start + col).copied().unwrap_or(0)
} else {
0
};
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::font::FontMetrics;
const FONT_DATA: &[u8] = include_bytes!("../../../assets/fonts/DejaVuSans.ttf");
#[test]
fn rasterize_a() {
let (metrics, bitmap) = rasterize_glyph(FONT_DATA, 'A', 16.0).unwrap();
assert_eq!(bitmap.len(), metrics.width * metrics.height);
assert!(metrics.width > 0 && metrics.height > 0);
}
#[test]
fn line_metrics_present() {
let vm = line_metrics(FONT_DATA, 16.0).unwrap();
assert!(vm.ascent > 0.0 && vm.descent < 0.0);
}
#[test]
fn fontdue_font_implements_font_metrics() {
let font = FontdueFont::new(FONT_DATA, 16.0);
let info = font.glyph_metrics('A').unwrap();
assert!(info.advance_fp16 > 0);
assert!(info.width > 0 && info.height > 0);
let line = font.line_metrics();
assert!(line.line_height > 0);
assert!(line.ascent > 0);
let shaped = font.shape("A", (0, line.ascent as i32));
assert_eq!(shaped.glyphs.len(), 1);
assert_eq!(shaped.bidi_level, 0);
}
struct Surface {
buf: [u8; 32 * 32 * 4],
}
impl Surface {
fn new() -> Self {
Self {
buf: [0; 32 * 32 * 4],
}
}
}
impl FontdueRenderTarget for Surface {
fn dimensions(&self) -> (usize, usize) {
(32, 32)
}
fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8) {
if x >= 0 && y >= 0 && x < 32 && y < 32 {
let idx = ((y as usize) * 32 + x as usize) * 4;
let r = (color.0 as u16 * alpha as u16 / 255) as u8;
let g = (color.1 as u16 * alpha as u16 / 255) as u8;
let b = (color.2 as u16 * alpha as u16 / 255) as u8;
self.buf[idx] = r;
self.buf[idx + 1] = g;
self.buf[idx + 2] = b;
self.buf[idx + 3] = 0xff;
}
}
}
#[test]
fn render_text_draws_pixels() {
let mut surf = Surface::new();
render_text(
&mut surf,
FONT_DATA,
(0, 0),
"A",
Color(255, 255, 255, 255),
16.0,
)
.unwrap();
assert!(surf.buf.iter().any(|&p| p != 0));
}
}