use std::collections::HashMap;
use skrifa::GlyphId;
use skrifa::MetadataProvider;
use skrifa::instance::{LocationRef, Size};
use skrifa::outline::pen::PathElement;
use skrifa::outline::{DrawSettings, OutlinePen};
use smudge_core::paint::FontBlob;
use tiny_skia::{FillRule, Mask, PathBuilder, Transform};
pub(crate) const MAX_GLYPH_SIZE: f32 = 1024.0;
const MAX_MASK_DIMENSION: u32 = 4096;
const DEFAULT_BUDGET: usize = 16 * 1024 * 1024;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct MaskKey {
pub(crate) font_id: u64,
pub(crate) font_index: u32,
pub(crate) glyph_id: u32,
pub(crate) size: i32,
pub(crate) subpixel: u32,
}
pub(crate) struct GlyphMask {
data: Vec<u8>,
width: u32,
height: u32,
left: i32,
top: i32,
}
impl GlyphMask {
pub(crate) fn width(&self) -> u32 {
self.width
}
pub(crate) fn height(&self) -> u32 {
self.height
}
pub(crate) fn left(&self) -> i32 {
self.left
}
pub(crate) fn top(&self) -> i32 {
self.top
}
pub(crate) fn data(&self) -> &[u8] {
&self.data
}
fn size_in_bytes(&self) -> usize {
self.data.len()
}
fn empty() -> GlyphMask {
GlyphMask {
data: Vec::new(),
width: 0,
height: 0,
left: 0,
top: 0,
}
}
}
pub(crate) struct GlyphCache {
current: HashMap<MaskKey, GlyphMask>,
previous: HashMap<MaskKey, GlyphMask>,
bytes: usize,
budget: usize,
hits: u64,
misses: u64,
}
impl GlyphCache {
pub(crate) fn new() -> GlyphCache {
GlyphCache::with_budget(DEFAULT_BUDGET)
}
pub(crate) fn with_budget(budget: usize) -> GlyphCache {
GlyphCache {
current: HashMap::new(),
previous: HashMap::new(),
bytes: 0,
budget,
hits: 0,
misses: 0,
}
}
pub(crate) fn entry(&mut self, key: MaskKey, font: &FontBlob) -> Option<&GlyphMask> {
if !self.current.contains_key(&key) {
if let Some(mask) = self.previous.remove(&key) {
self.bytes += mask.size_in_bytes();
self.current.insert(key, mask);
self.hits += 1;
} else {
self.misses += 1;
let mask = rasterize(key, font)?;
if self.bytes + mask.size_in_bytes() > self.budget {
self.previous = std::mem::take(&mut self.current);
self.bytes = 0;
}
self.bytes += mask.size_in_bytes();
self.current.insert(key, mask);
}
} else {
self.hits += 1;
}
self.current.get(&key)
}
#[cfg(test)]
pub(crate) fn bytes(&self) -> usize {
self.bytes
}
#[cfg(test)]
pub(crate) fn hits(&self) -> u64 {
self.hits
}
#[cfg(test)]
pub(crate) fn misses(&self) -> u64 {
self.misses
}
}
fn rasterize(key: MaskKey, font: &FontBlob) -> Option<GlyphMask> {
if key.size <= 0 || key.size as f32 / 64.0 > MAX_GLYPH_SIZE {
return None;
}
let data = font.data.as_ref().as_ref();
let font = skrifa::FontRef::from_index(data, key.font_index).ok()?;
let outline = font.outline_glyphs().get(GlyphId::new(key.glyph_id))?;
let mut collector = OutlineCollector::default();
let settings =
DrawSettings::unhinted(Size::new(key.size as f32 / 64.0), LocationRef::default());
outline.draw(settings, &mut collector).ok()?;
let Some((x_min, y_min, x_max, y_max)) = collector.bounds() else {
return Some(GlyphMask::empty());
};
let shift = key.subpixel as f32 / 4.0;
let x0 = (x_min + shift).floor() - 1.0;
let y0 = y_min.floor() - 1.0;
let width = ((x_max + shift).ceil() + 1.0 - x0) as u32;
let height = (y_max.ceil() + 1.0 - y0) as u32;
if width == 0 || height == 0 || width > MAX_MASK_DIMENSION || height > MAX_MASK_DIMENSION {
return None;
}
let mut builder = PathBuilder::new();
for element in &collector.elements {
match *element {
PathElement::MoveTo { x, y } => builder.move_to(x + shift - x0, y - y0),
PathElement::LineTo { x, y } => builder.line_to(x + shift - x0, y - y0),
PathElement::QuadTo { cx0, cy0, x, y } => {
builder.quad_to(cx0 + shift - x0, cy0 - y0, x + shift - x0, y - y0);
}
PathElement::CurveTo {
cx0,
cy0,
cx1,
cy1,
x,
y,
} => builder.cubic_to(
cx0 + shift - x0,
cy0 - y0,
cx1 + shift - x0,
cy1 - y0,
x + shift - x0,
y - y0,
),
PathElement::Close => builder.close(),
}
}
let Some(path) = builder.finish() else {
return Some(GlyphMask::empty());
};
let mut mask = Mask::new(width, height)?;
mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
Some(GlyphMask {
data: mask.data().to_vec(),
width,
height,
left: x0 as i32,
top: y0 as i32,
})
}
#[derive(Default)]
struct OutlineCollector {
elements: Vec<PathElement>,
}
impl OutlineCollector {
fn bounds(&self) -> Option<(f32, f32, f32, f32)> {
let mut bounds: Option<(f32, f32, f32, f32)> = None;
let fold = |bounds: &mut Option<(f32, f32, f32, f32)>, x: f32, y: f32| {
*bounds = Some(match *bounds {
None => (x, y, x, y),
Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x), y1.max(y)),
});
};
for element in &self.elements {
match *element {
PathElement::MoveTo { x, y } | PathElement::LineTo { x, y } => {
fold(&mut bounds, x, y);
}
PathElement::QuadTo { cx0, cy0, x, y, .. } => {
fold(&mut bounds, cx0, cy0);
fold(&mut bounds, x, y);
}
PathElement::CurveTo {
cx0,
cy0,
cx1,
cy1,
x,
y,
} => {
fold(&mut bounds, cx0, cy0);
fold(&mut bounds, cx1, cy1);
fold(&mut bounds, x, y);
}
PathElement::Close => {}
}
}
bounds
}
}
impl OutlinePen for OutlineCollector {
fn move_to(&mut self, x: f32, y: f32) {
self.elements.push(PathElement::MoveTo { x, y: -y });
}
fn line_to(&mut self, x: f32, y: f32) {
self.elements.push(PathElement::LineTo { x, y: -y });
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.elements.push(PathElement::QuadTo {
cx0,
cy0: -cy0,
x,
y: -y,
});
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.elements.push(PathElement::CurveTo {
cx0,
cy0: -cy0,
cx1,
cy1: -cy1,
x,
y: -y,
});
}
fn close(&mut self) {
self.elements.push(PathElement::Close);
}
}
#[cfg(test)]
#[path = "glyph_cache_tests.rs"]
mod tests;