use std::collections::HashMap;
use std::sync::Arc;
use super::{Encoding, FontEmbolden, StreamOffsets};
use peniko::{
FontData, Style,
kurbo::{BezPath, Join, Point},
};
use skrifa::instance::{NormalizedCoord, Size};
use skrifa::outline::{HintingInstance, HintingOptions, OutlineGlyphFormat, OutlinePen};
use skrifa::{GlyphId, MetadataProvider, OutlineGlyphCollection};
#[derive(Default)]
pub(crate) struct GlyphCache {
free_list: Vec<Arc<Encoding>>,
map: GlyphMap,
var_map: HashMap<VarKey, GlyphMap>,
cached_count: usize,
hinting: HintCache,
serial: u64,
last_prune_serial: u64,
}
impl GlyphCache {
pub(crate) fn session<'a>(
&'a mut self,
font: &'a FontData,
coords: &'a [NormalizedCoord],
size: f32,
embolden: FontEmbolden,
hint: bool,
style: &'a Style,
) -> Option<GlyphCacheSession<'a>> {
let font_id = font.data.id();
let font_index = font.index;
let font = skrifa::FontRef::from_index(font.data.as_ref(), font.index).ok()?;
let map = if !coords.is_empty() {
if self.var_map.contains_key(coords) {
self.var_map.get_mut(coords).unwrap()
} else {
self.var_map.entry(coords.into()).or_default()
}
} else {
&mut self.map
};
let outlines = font.outline_glyphs();
let size = Size::new(size);
let hinter = if hint {
let key = HintKey {
font_id,
font_index,
outlines: &outlines,
size,
coords,
};
self.hinting.get(&key)
} else {
None
};
let style_bits = match style {
Style::Fill(fill) => super::path::Style::from_fill(*fill),
Style::Stroke(stroke) => super::path::Style::from_stroke(stroke)?,
};
let style_bits: [u32; 2] = bytemuck::cast(style_bits);
Some(GlyphCacheSession {
free_list: &mut self.free_list,
map,
font_id,
font_index,
coords,
size,
size_bits: size.ppem().unwrap().to_bits(),
embolden,
style,
style_bits,
outlines,
hinter,
outline_buf: BezPath::new(),
serial: self.serial,
cached_count: &mut self.cached_count,
})
}
pub(crate) fn maintain(&mut self) {
const MAX_ENTRY_AGE: u64 = 64;
const PRUNE_FREQUENCY: u64 = 64;
const CACHED_COUNT_THRESHOLD: usize = 256;
const MAX_FREE_LIST_SIZE: usize = 32;
let free_list = &mut self.free_list;
let serial = self.serial;
self.serial += 1;
if serial - self.last_prune_serial < PRUNE_FREQUENCY
&& self.cached_count < CACHED_COUNT_THRESHOLD
{
return;
}
self.last_prune_serial = serial;
self.map.retain(|_, entry| {
if serial - entry.serial > MAX_ENTRY_AGE {
if free_list.len() < MAX_FREE_LIST_SIZE {
free_list.push(entry.encoding.clone());
}
self.cached_count -= 1;
false
} else {
true
}
});
self.var_map.retain(|_, map| {
map.retain(|_, entry| {
if serial - entry.serial > MAX_ENTRY_AGE {
if free_list.len() < MAX_FREE_LIST_SIZE {
free_list.push(entry.encoding.clone());
}
self.cached_count -= 1;
false
} else {
true
}
});
!map.is_empty()
});
}
}
pub(crate) struct GlyphCacheSession<'a> {
free_list: &'a mut Vec<Arc<Encoding>>,
map: &'a mut GlyphMap,
font_id: u64,
font_index: u32,
coords: &'a [NormalizedCoord],
size: Size,
size_bits: u32,
embolden: FontEmbolden,
style: &'a Style,
style_bits: [u32; 2],
outlines: OutlineGlyphCollection<'a>,
hinter: Option<&'a HintingInstance>,
outline_buf: BezPath,
serial: u64,
cached_count: &'a mut usize,
}
impl GlyphCacheSession<'_> {
pub(crate) fn get_or_insert(
&mut self,
glyph_id: u32,
) -> Option<(Arc<Encoding>, StreamOffsets)> {
let key = GlyphKey {
font_id: self.font_id,
font_index: self.font_index,
glyph_id,
font_size_bits: self.size_bits,
embolden_x_bits: f32_bits(self.embolden.amount.xx),
embolden_y_bits: f32_bits(self.embolden.amount.yy),
embolden_join_bits: join_bits(self.embolden.join),
embolden_miter_limit_bits: f32_bits(self.embolden.miter_limit),
embolden_tolerance_bits: f32_bits(self.embolden.tolerance),
style_bits: self.style_bits,
hint: self.hinter.is_some(),
};
if let Some(entry) = self.map.get_mut(&key) {
entry.serial = self.serial;
return Some((entry.encoding.clone(), entry.stream_sizes));
}
let outline = self.outlines.get(GlyphId::new(key.glyph_id))?;
let mut encoding = self.free_list.pop().unwrap_or_default();
let encoding_ptr = Arc::make_mut(&mut encoding);
encoding_ptr.reset();
let is_fill = match &self.style {
Style::Fill(fill) => {
encoding_ptr.encode_fill_style(*fill);
true
}
Style::Stroke(stroke) => {
let encoded_stroke = encoding_ptr.encode_stroke_style(stroke);
debug_assert!(encoded_stroke, "Stroke width is non-zero");
false
}
};
use skrifa::outline::DrawSettings;
let draw_settings = if key.hint {
if let Some(hinter) = self.hinter {
DrawSettings::hinted(hinter, false)
} else {
DrawSettings::unhinted(self.size, self.coords)
}
} else {
DrawSettings::unhinted(self.size, self.coords)
};
let n_path_segments = if self.embolden.amount != peniko::kurbo::Diagonal2::new(0.0, 0.0) {
self.outline_buf.truncate(0);
let mut path = BezPathOutline(&mut self.outline_buf);
outline.draw(draw_settings, &mut path).ok()?;
let path = peniko::kurbo::expand_path(
&self.outline_buf,
self.embolden.amount,
self.embolden.join,
self.embolden.miter_limit,
self.embolden.tolerance,
);
let mut encoder = encoding_ptr.encode_path(is_fill);
encoder.path_elements(path.elements().iter().copied());
encoder.finish(false)
} else {
let mut path = encoding_ptr.encode_path(is_fill);
outline.draw(draw_settings, &mut path).ok()?;
path.finish(false)
};
if n_path_segments == 0 {
encoding_ptr.reset();
}
let stream_sizes = encoding_ptr.stream_offsets();
self.map.insert(
key,
GlyphEntry {
encoding: encoding.clone(),
stream_sizes,
serial: self.serial,
},
);
*self.cached_count += 1;
Some((encoding, stream_sizes))
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
struct GlyphKey {
font_id: u64,
font_index: u32,
glyph_id: u32,
font_size_bits: u32,
embolden_x_bits: u32,
embolden_y_bits: u32,
embolden_join_bits: u8,
embolden_miter_limit_bits: u32,
embolden_tolerance_bits: u32,
style_bits: [u32; 2],
hint: bool,
}
#[inline(always)]
fn join_bits(join: Join) -> u8 {
match join {
Join::Bevel => 0,
Join::Miter => 1,
Join::Round => 2,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "Cache keys intentionally store embolden parameters at f32 precision."
)]
#[inline(always)]
fn f32_bits(value: f64) -> u32 {
(value as f32).to_bits()
}
struct BezPathOutline<'a>(&'a mut BezPath);
impl OutlinePen for BezPathOutline<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.0.move_to(Point::new(x.into(), y.into()));
}
fn line_to(&mut self, x: f32, y: f32) {
self.0.line_to(Point::new(x.into(), y.into()));
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.0.quad_to(
Point::new(cx0.into(), cy0.into()),
Point::new(x.into(), y.into()),
);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.0.curve_to(
Point::new(cx0.into(), cy0.into()),
Point::new(cx1.into(), cy1.into()),
Point::new(x.into(), y.into()),
);
}
fn close(&mut self) {
self.0.close_path();
}
}
type VarKey = smallvec::SmallVec<[NormalizedCoord; 8]>;
type GlyphMap = HashMap<GlyphKey, GlyphEntry>;
#[derive(Clone, Default)]
struct GlyphEntry {
encoding: Arc<Encoding>,
stream_sizes: StreamOffsets,
serial: u64,
}
const MAX_CACHED_HINT_INSTANCES: usize = 8;
pub(crate) struct HintKey<'a> {
font_id: u64,
font_index: u32,
outlines: &'a OutlineGlyphCollection<'a>,
size: Size,
coords: &'a [NormalizedCoord],
}
impl HintKey<'_> {
fn instance(&self) -> Option<HintingInstance> {
HintingInstance::new(self.outlines, self.size, self.coords, HINTING_OPTIONS).ok()
}
}
const HINTING_OPTIONS: HintingOptions = HintingOptions {
engine: skrifa::outline::Engine::AutoFallback,
target: skrifa::outline::Target::Smooth {
mode: skrifa::outline::SmoothMode::Lcd,
symmetric_rendering: false,
preserve_linear_metrics: true,
},
};
#[derive(Default)]
struct HintCache {
glyf_entries: Vec<HintEntry>,
cff_entries: Vec<HintEntry>,
varc_entries: Vec<HintEntry>,
serial: u64,
}
impl HintCache {
fn get(&mut self, key: &HintKey<'_>) -> Option<&HintingInstance> {
let entries = match key.outlines.format()? {
OutlineGlyphFormat::Glyf => &mut self.glyf_entries,
OutlineGlyphFormat::Cff | OutlineGlyphFormat::Cff2 => &mut self.cff_entries,
OutlineGlyphFormat::Varc => &mut self.varc_entries,
};
let (entry_ix, is_current) = find_hint_entry(entries, key)?;
let entry = entries.get_mut(entry_ix)?;
self.serial += 1;
entry.serial = self.serial;
if !is_current {
entry.font_id = key.font_id;
entry.font_index = key.font_index;
entry
.instance
.reconfigure(key.outlines, key.size, key.coords, HINTING_OPTIONS)
.ok()?;
}
Some(&entry.instance)
}
}
struct HintEntry {
font_id: u64,
font_index: u32,
instance: HintingInstance,
serial: u64,
}
fn find_hint_entry(entries: &mut Vec<HintEntry>, key: &HintKey<'_>) -> Option<(usize, bool)> {
let mut found_serial = u64::MAX;
let mut found_index = 0;
for (ix, entry) in entries.iter().enumerate() {
if entry.font_id == key.font_id
&& entry.font_index == key.font_index
&& entry.instance.size() == key.size
&& entry.instance.location().coords() == key.coords
{
return Some((ix, true));
}
if entry.serial < found_serial {
found_serial = entry.serial;
found_index = ix;
}
}
if entries.len() < MAX_CACHED_HINT_INSTANCES {
let instance = key.instance()?;
let ix = entries.len();
entries.push(HintEntry {
font_id: key.font_id,
font_index: key.font_index,
instance,
serial: 0,
});
Some((ix, true))
} else {
Some((found_index, false))
}
}