use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
pub struct FontAtlas {
pub bitmap: Vec<u8>,
pub width: u32,
pub height: u32,
pub glyphs: HashMap<char, GlyphInfo>,
pub is_sdf: bool,
}
#[derive(Clone, Debug)]
pub struct GlyphInfo {
pub atlas_x: u32,
pub atlas_y: u32,
pub width: u32,
pub height: u32,
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
}
pub struct GlyphAtlasGpu {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
}
static ATLAS_CACHE: OnceLock<RwLock<HashMap<String, Arc<FontAtlas>>>> = OnceLock::new();
fn atlas_cache() -> &'static RwLock<HashMap<String, Arc<FontAtlas>>> {
ATLAS_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
}
pub fn get_or_build_atlas(
font_id: &str,
font_data: &[u8],
font_size: f32,
is_sdf: bool,
chars: &str,
) -> anyhow::Result<Arc<FontAtlas>> {
let cache_key = format!(
"{}:{}:{}",
font_id,
font_size as u32,
if is_sdf { "sdf" } else { "bmp" }
);
if let Ok(cache) = atlas_cache().read() {
if let Some(atlas) = cache.get(&cache_key) {
return Ok(Arc::clone(atlas));
}
}
let font = fontdue::Font::from_bytes(font_data, fontdue::FontSettings::default())
.map_err(|e| anyhow::anyhow!("Failed to parse font: {}", e))?;
let mut unique_chars: Vec<char> = Vec::new();
for c in 32u8..=126 {
unique_chars.push(c as char);
}
for ch in chars.chars() {
if !unique_chars.contains(&ch) {
unique_chars.push(ch);
}
}
let sdf_padding = if is_sdf { 8u32 } else { 1 };
let render_scale: f32 = if is_sdf { 4.0 } else { 1.0 };
let render_size = font_size * render_scale;
let mut glyph_bitmaps: Vec<(char, Vec<u8>, usize, usize, fontdue::Metrics)> = Vec::new();
for &ch in &unique_chars {
let (metrics, bitmap) = font.rasterize(ch, render_size);
if is_sdf {
let sdf = generate_sdf(&bitmap, metrics.width, metrics.height, sdf_padding);
let sdf_w = metrics.width + sdf_padding as usize * 2;
let sdf_h = metrics.height + sdf_padding as usize * 2;
let sdf_metrics = fontdue::Metrics {
width: sdf_w,
height: sdf_h,
..metrics
};
glyph_bitmaps.push((ch, sdf, sdf_w, sdf_h, sdf_metrics));
} else {
let w = metrics.width;
let h = metrics.height;
glyph_bitmaps.push((ch, bitmap, w, h, metrics));
}
}
let max_glyph_h = glyph_bitmaps
.iter()
.map(|(_, _, _, h, _)| *h)
.max()
.unwrap_or(0);
let total_area: usize = glyph_bitmaps
.iter()
.map(|(_, _, w, h, _)| (w + 2) * (h + 2))
.sum();
let atlas_width = ((total_area as f64).sqrt() * 1.2) as u32;
let atlas_width = atlas_width.max(512).min(4096).next_power_of_two();
let mut cursor_x = 1u32;
let mut cursor_y = 1u32;
let mut row_height = 0u32;
let mut glyphs = HashMap::new();
for (ch, _, gw, gh, metrics) in &glyph_bitmaps {
let gw = *gw as u32;
let gh = *gh as u32;
if cursor_x + gw + 1 > atlas_width {
cursor_x = 1;
cursor_y += row_height + 1;
row_height = 0;
}
glyphs.insert(
*ch,
GlyphInfo {
atlas_x: cursor_x,
atlas_y: cursor_y,
width: gw,
height: gh,
advance: metrics.advance_width,
bearing_x: metrics.xmin as f32,
bearing_y: metrics.ymin as f32,
},
);
cursor_x += gw + 1;
row_height = row_height.max(gh);
}
let atlas_height = (cursor_y + row_height + 1)
.next_power_of_two()
.max(max_glyph_h as u32 + 2)
.min(8192);
let mut bitmap = vec![0u8; (atlas_width * atlas_height) as usize];
for (ch, glyph_bmp, _gw, _, _) in &glyph_bitmaps {
if let Some(info) = glyphs.get(ch) {
for y in 0..info.height {
for x in 0..info.width {
let src = (y * info.width + x) as usize;
let dst = ((info.atlas_y + y) * atlas_width + info.atlas_x + x) as usize;
if src < glyph_bmp.len() && dst < bitmap.len() {
bitmap[dst] = glyph_bmp[src];
}
}
}
}
}
let atlas = Arc::new(FontAtlas {
bitmap,
width: atlas_width,
height: atlas_height,
glyphs,
is_sdf,
});
if let Ok(mut cache) = atlas_cache().write() {
cache.insert(cache_key, Arc::clone(&atlas));
}
Ok(atlas)
}
fn generate_sdf(bitmap: &[u8], w: usize, h: usize, padding: u32) -> Vec<u8> {
let pw = w + padding as usize * 2;
let ph = h + padding as usize * 2;
let spread = padding as f32;
let pad = padding as usize;
let n = pw * ph;
let _inf = (pw + ph) as f32;
let sample = |gx: i32, gy: i32| -> bool {
if gx >= 0 && gx < w as i32 && gy >= 0 && gy < h as i32 {
bitmap[gy as usize * w + gx as usize] > 127
} else {
false
}
};
let mut inside = vec![false; n];
for sy in 0..ph {
for sx in 0..pw {
inside[sy * pw + sx] = sample(sx as i32 - pad as i32, sy as i32 - pad as i32);
}
}
let dist_out = edt_8ssedt(&inside, pw, ph, false);
let dist_in = edt_8ssedt(&inside, pw, ph, true);
let mut sdf = vec![0u8; n];
for i in 0..n {
let signed = dist_in[i] - dist_out[i];
let normalized = (signed / spread * 127.0 + 128.0).clamp(0.0, 255.0) as u8;
sdf[i] = normalized;
}
sdf
}
fn edt_8ssedt(mask: &[bool], w: usize, h: usize, invert: bool) -> Vec<f32> {
let n = w * h;
let big = (w + h) as i32;
let mut dist = vec![0.0f32; n];
let mut ox = vec![0i32; n];
let mut oy = vec![0i32; n];
for i in 0..n {
let is_target = mask[i] ^ invert; if is_target {
dist[i] = 0.0;
ox[i] = 0;
oy[i] = 0;
} else {
let x = (i % w) as i32;
let y = (i / w) as i32;
let has_target_neighbor =
[(-1i32, 0i32), (1, 0), (0, -1), (0, 1)]
.iter()
.any(|&(dx, dy)| {
let nx = x + dx;
let ny = y + dy;
if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
mask[ny as usize * w + nx as usize] ^ invert
} else {
false
}
});
if has_target_neighbor {
dist[i] = 0.5; ox[i] = 0;
oy[i] = 0;
} else {
dist[i] = big as f32;
ox[i] = big;
oy[i] = big;
}
}
}
let fwd_offsets: [(i32, i32); 4] = [(-1, 0), (-1, -1), (0, -1), (1, -1)];
for y in 0..h as i32 {
for x in 0..w as i32 {
let i = y as usize * w + x as usize;
for &(ddx, ddy) in &fwd_offsets {
let nx = x + ddx;
let ny = y + ddy;
if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
let ni = ny as usize * w + nx as usize;
let ndx = ox[ni] - ddx;
let ndy = oy[ni] - ddy;
let nd = ((ndx * ndx + ndy * ndy) as f32).sqrt();
if nd < dist[i] {
dist[i] = nd;
ox[i] = ndx;
oy[i] = ndy;
}
}
}
}
}
let bwd_offsets: [(i32, i32); 4] = [(1, 0), (1, 1), (0, 1), (-1, 1)];
for y in (0..h as i32).rev() {
for x in (0..w as i32).rev() {
let i = y as usize * w + x as usize;
for &(ddx, ddy) in &bwd_offsets {
let nx = x + ddx;
let ny = y + ddy;
if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
let ni = ny as usize * w + nx as usize;
let ndx = ox[ni] - ddx;
let ndy = oy[ni] - ddy;
let nd = ((ndx * ndx + ndy * ndy) as f32).sqrt();
if nd < dist[i] {
dist[i] = nd;
ox[i] = ndx;
oy[i] = ndy;
}
}
}
}
}
dist
}
impl FontAtlas {
pub fn to_gpu(&self) -> GlyphAtlasGpu {
GlyphAtlasGpu {
data: self.bitmap.clone(),
width: self.width,
height: self.height,
}
}
}