use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
use lru::LruCache;
use tiny_skia::{Color, FillRule, Paint, Pixmap, Transform};
use super::font::Font;
use super::sdf::{SdfGlyph, SDF_BORDER, SDF_EM_PX, SDF_RADIUS_PX};
type GlyphKey = (u64, u16);
const ENTRY_OVERHEAD_BYTES: usize = 96;
const GLYPH_SDF_CAP_BYTES: usize = 8 * 1024 * 1024;
struct GlyphSdfStore {
cache: LruCache<GlyphKey, Option<Arc<SdfGlyph>>>,
bytes: usize,
}
impl GlyphSdfStore {
fn entry_bytes(v: &Option<Arc<SdfGlyph>>) -> usize {
ENTRY_OVERHEAD_BYTES + v.as_ref().map_or(0, |g| g.bitmap.len())
}
fn insert(&mut self, key: GlyphKey, val: Option<Arc<SdfGlyph>>) {
let add = Self::entry_bytes(&val);
if let Some(old) = self.cache.put(key, val) {
self.bytes = self.bytes.saturating_sub(Self::entry_bytes(&old));
}
self.bytes += add;
while self.bytes > GLYPH_SDF_CAP_BYTES && self.cache.len() > 1 {
match self.cache.pop_lru() {
Some((_, old)) => self.bytes = self.bytes.saturating_sub(Self::entry_bytes(&old)),
None => break,
}
}
}
}
static GLYPH_SDF: LazyLock<Mutex<GlyphSdfStore>> = LazyLock::new(|| {
Mutex::new(GlyphSdfStore {
cache: LruCache::unbounded(),
bytes: 0,
})
});
#[derive(Default)]
pub struct OutlineSdfCache {
hits: AtomicU64,
misses: AtomicU64,
built_bytes: AtomicUsize,
}
#[derive(Debug, Clone, Copy)]
pub struct OutlineSdfStats {
pub built: u64,
pub hits: u64,
pub bitmap_bytes: usize,
}
impl OutlineSdfCache {
pub fn new() -> Self {
Self::default()
}
pub fn get(
&self,
font: &Font,
face: &rustybuzz::Face<'_>,
glyph_id: u16,
) -> Option<Arc<SdfGlyph>> {
let key: GlyphKey = (font.content_hash(), glyph_id);
if let Some(hit) = GLYPH_SDF
.lock()
.expect("sdf cache poisoned")
.cache
.get(&key)
{
self.hits.fetch_add(1, Ordering::Relaxed);
return hit.clone();
}
let built = build(font, face, glyph_id).map(Arc::new);
self.misses.fetch_add(1, Ordering::Relaxed);
self.built_bytes.fetch_add(
built.as_ref().map_or(0, |g| g.bitmap.len()),
Ordering::Relaxed,
);
GLYPH_SDF
.lock()
.expect("sdf cache poisoned")
.insert(key, built.clone());
built
}
pub fn stats(&self) -> OutlineSdfStats {
OutlineSdfStats {
built: self.misses.load(Ordering::Relaxed),
hits: self.hits.load(Ordering::Relaxed),
bitmap_bytes: self.built_bytes.load(Ordering::Relaxed),
}
}
}
const CUTOFF: f32 = 0.25;
const INF: f32 = 1e20;
pub fn build(font: &Font, face: &rustybuzz::Face<'_>, glyph_id: u16) -> Option<SdfGlyph> {
let path = font.glyph_path(face, glyph_id)?;
let upm = font.units_per_em();
let scale = SDF_EM_PX / upm;
let b = path.bounds();
let x_lo = b.left() * scale;
let x_hi = b.right() * scale;
let y_lo = -b.bottom() * scale;
let y_hi = -b.top() * scale;
let ix0 = x_lo.floor() as i32;
let iy0 = y_lo.floor() as i32;
let ix1 = x_hi.ceil() as i32;
let iy1 = y_hi.ceil() as i32;
let gw = (ix1 - ix0).max(0) as u32;
let gh = (iy1 - iy0).max(0) as u32;
if gw == 0 || gh == 0 {
return None;
}
let border = SDF_BORDER as i32;
let bw = gw + 2 * SDF_BORDER;
let bh = gh + 2 * SDF_BORDER;
let mut mask = Pixmap::new(bw, bh)?;
let mut paint = Paint::default();
paint.set_color(Color::WHITE);
paint.anti_alias = true;
let to_bitmap = Transform::from_row(
scale,
0.0,
0.0,
-scale,
border as f32 - ix0 as f32,
border as f32 - iy0 as f32,
);
mask.fill_path(&path, &paint, FillRule::Winding, to_bitmap, None);
let len = (bw * bh) as usize;
let mut outer = vec![0f32; len];
let mut inner = vec![0f32; len];
for (i, px) in mask.pixels().iter().enumerate() {
let a = px.alpha() as f32 / 255.0;
if a >= 1.0 {
outer[i] = 0.0;
inner[i] = INF;
} else if a <= 0.0 {
outer[i] = INF;
inner[i] = 0.0;
} else {
let o = (0.5 - a).max(0.0);
let n = (a - 0.5).max(0.0);
outer[i] = o * o;
inner[i] = n * n;
}
}
edt(&mut outer, bw as usize, bh as usize);
edt(&mut inner, bw as usize, bh as usize);
let radius = SDF_RADIUS_PX;
let bitmap: Vec<u8> = (0..len)
.map(|i| {
let d = outer[i].sqrt() - inner[i].sqrt();
let v = 255.0 - 255.0 * (d / radius + CUTOFF);
v.round().clamp(0.0, 255.0) as u8
})
.collect();
let advance = face
.glyph_hor_advance(ttf_parser::GlyphId(glyph_id))
.map(|a| (a as f32 * scale).round().max(0.0) as u32)
.unwrap_or(0);
Some(SdfGlyph {
id: u32::from(glyph_id),
bitmap,
width: gw,
height: gh,
left: ix0,
top: -iy0,
advance,
})
}
fn edt(grid: &mut [f32], width: usize, height: usize) {
let max = width.max(height);
let mut f = vec![0f32; max];
let mut v = vec![0usize; max];
let mut z = vec![0f32; max + 1];
for x in 0..width {
edt1d(grid, x, width, height, &mut f, &mut v, &mut z);
}
for y in 0..height {
edt1d(grid, y * width, 1, width, &mut f, &mut v, &mut z);
}
}
fn edt1d(
grid: &mut [f32],
offset: usize,
stride: usize,
length: usize,
f: &mut [f32],
v: &mut [usize],
z: &mut [f32],
) {
v[0] = 0;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
f[0] = grid[offset];
let mut k = 0usize;
for q in 1..length {
f[q] = grid[offset + q * stride];
let q2 = (q * q) as f32;
loop {
let r = v[k];
let s = (f[q] - f[r] + q2 - (r * r) as f32) / (q - r) as f32 / 2.0;
if s <= z[k] {
if k == 0 {
v[0] = q;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
break;
}
k -= 1;
} else {
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = f32::INFINITY;
break;
}
}
}
k = 0;
for q in 0..length {
while z[k + 1] < q as f32 {
k += 1;
}
let r = v[k];
let d = q as f32 - r as f32;
grid[offset + q * stride] = d * d + f[r];
}
}