use std::collections::HashMap;
use std::path::{Path, PathBuf};
use ab_glyph::{Font, FontVec, PxScale, ScaleFont};
use super::atlas::ATLAS_CHARS;
#[derive(Clone, Debug)]
pub struct SdfConfig {
pub hires_size: u32,
pub output_size: u32,
pub spread: f32,
pub msdf: bool,
pub cache_path: Option<PathBuf>,
}
impl Default for SdfConfig {
fn default() -> Self {
Self {
hires_size: 256,
output_size: 64,
spread: 8.0,
msdf: false,
cache_path: None,
}
}
}
#[derive(Clone, Debug)]
pub struct SdfGlyphData {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
pub bbox_w: f32,
pub bbox_h: f32,
}
#[derive(Clone, Debug)]
pub struct MsdfGlyphData {
pub r_channel: Vec<u8>,
pub g_channel: Vec<u8>,
pub b_channel: Vec<u8>,
pub width: u32,
pub height: u32,
pub advance: f32,
pub bearing_x: f32,
pub bearing_y: f32,
pub bbox_w: f32,
pub bbox_h: f32,
}
#[derive(Copy, Clone, Debug)]
pub struct SdfGlyphMetric {
pub uv_rect: [f32; 4],
pub size: glam::Vec2,
pub bearing: glam::Vec2,
pub advance: f32,
}
pub struct SdfAtlasData {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub channels: u32,
pub metrics: HashMap<char, SdfGlyphMetric>,
pub spread: f32,
pub font_size_px: f32,
}
#[derive(Copy, Clone)]
struct Offset {
dx: i32,
dy: i32,
}
impl Offset {
const FAR: Self = Self { dx: 9999, dy: 9999 };
const ZERO: Self = Self { dx: 0, dy: 0 };
fn dist_sq(self) -> i32 {
self.dx * self.dx + self.dy * self.dy
}
}
fn dead_reckoning_udf(bitmap: &[bool], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut grid = vec![Offset::FAR; n];
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
let inside = bitmap[idx];
let on_boundary = if inside {
(x > 0 && !bitmap[idx - 1])
|| (x + 1 < w && !bitmap[idx + 1])
|| (y > 0 && !bitmap[idx - w])
|| (y + 1 < h && !bitmap[idx + w])
} else {
(x > 0 && bitmap[idx - 1])
|| (x + 1 < w && bitmap[idx + 1])
|| (y > 0 && bitmap[idx - w])
|| (y + 1 < h && bitmap[idx + w])
};
if on_boundary {
grid[idx] = Offset::ZERO;
}
}
}
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
let cur = grid[idx];
macro_rules! check {
($nx:expr, $ny:expr, $ddx:expr, $ddy:expr) => {
if $nx < w && $ny < h {
let nidx = $ny * w + $nx;
let candidate = Offset {
dx: grid[nidx].dx + $ddx,
dy: grid[nidx].dy + $ddy,
};
if candidate.dist_sq() < grid[idx].dist_sq() {
grid[idx] = candidate;
}
}
};
}
if y > 0 {
if x > 0 { check!(x - 1, y - 1, 1, 1); }
check!(x, y - 1, 0, 1);
if x + 1 < w { check!(x + 1, y - 1, -1, 1); }
}
if x > 0 { check!(x - 1, y, 1, 0); }
}
}
for y in (0..h).rev() {
for x in (0..w).rev() {
let idx = y * w + x;
macro_rules! check {
($nx:expr, $ny:expr, $ddx:expr, $ddy:expr) => {
if $nx < w && $ny < h {
let nidx = $ny * w + $nx;
let candidate = Offset {
dx: grid[nidx].dx + $ddx,
dy: grid[nidx].dy + $ddy,
};
if candidate.dist_sq() < grid[idx].dist_sq() {
grid[idx] = candidate;
}
}
};
}
if y + 1 < h {
if x + 1 < w { check!(x + 1, y + 1, -1, -1); }
check!(x, y + 1, 0, -1);
if x > 0 { check!(x - 1, y + 1, 1, -1); }
}
if x + 1 < w { check!(x + 1, y, -1, 0); }
}
}
grid.iter().map(|o| (o.dist_sq() as f32).sqrt()).collect()
}
fn compute_sdf(bitmap: &[bool], w: usize, h: usize) -> Vec<f32> {
let outside_dist = dead_reckoning_udf(bitmap, w, h);
let inverted: Vec<bool> = bitmap.iter().map(|b| !b).collect();
let inside_dist = dead_reckoning_udf(&inverted, w, h);
outside_dist
.iter()
.zip(inside_dist.iter())
.map(|(out_d, in_d)| *in_d - *out_d)
.collect()
}
fn rasterize_glyph(
font: &FontVec,
ch: char,
hires_px: f32,
) -> Option<(Vec<f32>, u32, u32, f32, f32, f32, f32, f32)> {
let scale = PxScale::from(hires_px);
let scaled = font.as_scaled(scale);
let glyph_id = font.glyph_id(ch);
if glyph_id.0 == 0 && ch != ' ' {
return None;
}
let advance = scaled.h_advance(glyph_id);
let ascent = scaled.ascent();
let glyph = glyph_id.with_scale_and_position(scale, ab_glyph::point(0.0, ascent));
if let Some(outlined) = font.outline_glyph(glyph) {
let bounds = outlined.px_bounds();
let w = (bounds.max.x - bounds.min.x).ceil() as u32 + 2;
let h = (bounds.max.y - bounds.min.y).ceil() as u32 + 2;
if w == 0 || h == 0 {
return None;
}
let mut coverage = vec![0.0_f32; (w * h) as usize];
let ox = bounds.min.x.floor() as i32;
let oy = bounds.min.y.floor() as i32;
outlined.draw(|x, y, v| {
let px = x as i32 - ox + 1;
let py = y as i32 - oy + 1;
if px >= 0 && py >= 0 && (px as u32) < w && (py as u32) < h {
coverage[(py as u32 * w + px as u32) as usize] = v;
}
});
let bearing_x = bounds.min.x;
let bearing_y = bounds.min.y;
let bbox_w = (bounds.max.x - bounds.min.x).max(1.0);
let bbox_h = (bounds.max.y - bounds.min.y).max(1.0);
Some((coverage, w, h, advance, bearing_x, bearing_y, bbox_w, bbox_h))
} else {
Some((vec![0.0; 4], 2, 2, advance, 0.0, 0.0, 1.0, 1.0))
}
}
pub fn generate_glyph_sdf(
font: &FontVec,
ch: char,
config: &SdfConfig,
) -> Option<SdfGlyphData> {
let (coverage, hi_w, hi_h, advance, bearing_x, bearing_y, bbox_w, bbox_h) =
rasterize_glyph(font, ch, config.hires_size as f32)?;
let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
let sdf_hires = compute_sdf(&bitmap, hi_w as usize, hi_h as usize);
let scale_factor = config.output_size as f32 / config.hires_size as f32;
let out_w = ((hi_w as f32 * scale_factor).ceil() as u32).max(1);
let out_h = ((hi_h as f32 * scale_factor).ceil() as u32).max(1);
let pad = (config.spread * 1.5).ceil() as u32;
let padded_w = out_w + pad * 2;
let padded_h = out_h + pad * 2;
let inv_scale = 1.0 / scale_factor;
let spread_pixels = config.spread;
let mut sdf_out = vec![128u8; (padded_w * padded_h) as usize];
for py in 0..padded_h {
for px in 0..padded_w {
let hx = ((px as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
let hy = ((py as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
let dist = sample_bilinear_f32(&sdf_hires, hi_w as usize, hi_h as usize, hx, hy);
let dist_scaled = dist * scale_factor;
let normalized = (dist_scaled / spread_pixels) * 0.5 + 0.5;
let byte = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
sdf_out[(py * padded_w + px) as usize] = byte;
}
}
Some(SdfGlyphData {
pixels: sdf_out,
width: padded_w,
height: padded_h,
advance,
bearing_x,
bearing_y,
bbox_w,
bbox_h,
})
}
pub fn generate_glyph_msdf(
font: &FontVec,
ch: char,
config: &SdfConfig,
) -> Option<MsdfGlyphData> {
let (coverage, hi_w, hi_h, advance, bearing_x, bearing_y, bbox_w, bbox_h) =
rasterize_glyph(font, ch, config.hires_size as f32)?;
let w = hi_w as usize;
let h = hi_h as usize;
let bitmap: Vec<bool> = coverage.iter().map(|&v| v > 0.5).collect();
let mut edge_class = vec![0u8; w * h]; for y in 1..h.saturating_sub(1) {
for x in 1..w.saturating_sub(1) {
let idx = y * w + x;
if !is_edge(&bitmap, w, h, x, y) {
continue;
}
let gx = coverage[idx + 1] - coverage[idx.saturating_sub(1)];
let gy = coverage[idx + w] - coverage[idx.saturating_sub(w)];
let angle = gy.atan2(gx); let angle_deg = (angle.to_degrees() + 360.0) % 360.0;
edge_class[idx] = if angle_deg < 120.0 {
0
} else if angle_deg < 240.0 {
1
} else {
2
};
}
}
let mut channels = Vec::new();
for ch_idx in 0..3u8 {
let channel_bitmap: Vec<bool> = (0..w * h)
.map(|i| {
if bitmap[i] {
true
} else {
false
}
})
.collect();
let sdf = compute_sdf(&channel_bitmap, w, h);
let full_sdf = compute_sdf(&bitmap, w, h);
let blended: Vec<f32> = (0..w * h)
.map(|i| {
if is_edge(&bitmap, w, h, i % w, i / w) && edge_class[i] != ch_idx {
full_sdf[i] + 0.5
} else {
full_sdf[i]
}
})
.collect();
channels.push(blended);
}
let scale_factor = config.output_size as f32 / config.hires_size as f32;
let out_w = ((hi_w as f32 * scale_factor).ceil() as u32).max(1);
let out_h = ((hi_h as f32 * scale_factor).ceil() as u32).max(1);
let pad = (config.spread * 1.5).ceil() as u32;
let padded_w = out_w + pad * 2;
let padded_h = out_h + pad * 2;
let inv_scale = 1.0 / scale_factor;
let mut r_out = vec![128u8; (padded_w * padded_h) as usize];
let mut g_out = vec![128u8; (padded_w * padded_h) as usize];
let mut b_out = vec![128u8; (padded_w * padded_h) as usize];
for py in 0..padded_h {
for px in 0..padded_w {
let hx = ((px as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
let hy = ((py as f32 - pad as f32 + 0.5) * inv_scale).max(0.0);
for (ch_idx, out) in [&mut r_out, &mut g_out, &mut b_out].iter_mut().enumerate() {
let dist = sample_bilinear_f32(&channels[ch_idx], w, h, hx, hy);
let dist_scaled = dist * scale_factor;
let normalized = (dist_scaled / config.spread) * 0.5 + 0.5;
out[(py * padded_w + px) as usize] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
}
}
}
Some(MsdfGlyphData {
r_channel: r_out,
g_channel: g_out,
b_channel: b_out,
width: padded_w,
height: padded_h,
advance,
bearing_x,
bearing_y,
bbox_w,
bbox_h,
})
}
fn is_edge(bitmap: &[bool], w: usize, h: usize, x: usize, y: usize) -> bool {
let idx = y * w + x;
if !bitmap[idx] {
return false;
}
(x > 0 && !bitmap[idx - 1])
|| (x + 1 < w && !bitmap[idx + 1])
|| (y > 0 && !bitmap[idx - w])
|| (y + 1 < h && !bitmap[idx + w])
}
fn sample_bilinear_f32(data: &[f32], w: usize, h: usize, x: f32, y: f32) -> f32 {
let x0 = (x.floor() as usize).min(w.saturating_sub(1));
let y0 = (y.floor() as usize).min(h.saturating_sub(1));
let x1 = (x0 + 1).min(w.saturating_sub(1));
let y1 = (y0 + 1).min(h.saturating_sub(1));
let fx = x - x.floor();
let fy = y - y.floor();
let c00 = data[y0 * w + x0];
let c10 = data[y0 * w + x1];
let c01 = data[y1 * w + x0];
let c11 = data[y1 * w + x1];
let c0 = c00 + (c10 - c00) * fx;
let c1 = c01 + (c11 - c01) * fx;
c0 + (c1 - c0) * fy
}
struct ShelfPacker {
atlas_width: u32,
atlas_height: u32,
shelf_x: u32,
shelf_y: u32,
shelf_height: u32,
}
impl ShelfPacker {
fn new(atlas_width: u32, atlas_height: u32) -> Self {
Self {
atlas_width,
atlas_height,
shelf_x: 0,
shelf_y: 0,
shelf_height: 0,
}
}
fn pack(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
if w > self.atlas_width {
return None;
}
if self.shelf_x + w > self.atlas_width {
self.shelf_y += self.shelf_height;
self.shelf_x = 0;
self.shelf_height = 0;
}
if self.shelf_y + h > self.atlas_height {
return None;
}
let pos = (self.shelf_x, self.shelf_y);
self.shelf_x += w;
if h > self.shelf_height {
self.shelf_height = h;
}
Some(pos)
}
}
fn load_system_font() -> Option<FontVec> {
let paths: &[&str] = &[
r"C:\Windows\Fonts\consola.ttf",
r"C:\Windows\Fonts\cour.ttf",
r"C:\Windows\Fonts\lucon.ttf",
"/System/Library/Fonts/Menlo.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
];
for p in paths {
if let Ok(data) = std::fs::read(p) {
if let Ok(f) = FontVec::try_from_vec(data) {
log::info!("SdfGenerator: loaded '{}'", p);
return Some(f);
}
}
}
None
}
pub fn generate_sdf_atlas(config: &SdfConfig) -> SdfAtlasData {
if let Some(ref cache_path) = config.cache_path {
if let Some(cached) = load_cached_atlas(cache_path, config) {
log::info!("SdfGenerator: loaded cached atlas from {:?}", cache_path);
return cached;
}
}
let font = load_system_font();
let chars: Vec<char> = ATLAS_CHARS.chars().collect();
let mut glyph_sdfs: Vec<(char, SdfGlyphData)> = Vec::new();
if let Some(ref font) = font {
for &ch in &chars {
if let Some(sdf) = generate_glyph_sdf(font, ch, config) {
glyph_sdfs.push((ch, sdf));
} else {
glyph_sdfs.push((ch, SdfGlyphData {
pixels: vec![0u8; 16],
width: 4,
height: 4,
advance: config.output_size as f32 * 0.5,
bearing_x: 0.0,
bearing_y: 0.0,
bbox_w: 4.0,
bbox_h: 4.0,
}));
}
}
} else {
log::warn!("SdfGenerator: no system font found, generating fallback SDF atlas");
for &ch in &chars {
glyph_sdfs.push((ch, generate_fallback_sdf(config)));
}
}
let max_glyph_w = glyph_sdfs.iter().map(|(_, g)| g.width).max().unwrap_or(64);
let max_glyph_h = glyph_sdfs.iter().map(|(_, g)| g.height).max().unwrap_or(64);
let cells_per_row = 2048 / max_glyph_w.max(1);
let rows_needed = (glyph_sdfs.len() as u32 + cells_per_row - 1) / cells_per_row.max(1);
let atlas_h_needed = rows_needed * max_glyph_h;
let atlas_w = (cells_per_row * max_glyph_w).max(256).min(4096);
let atlas_h = atlas_h_needed.max(256).min(4096);
let mut atlas_pixels = vec![0u8; (atlas_w * atlas_h) as usize];
let mut metrics = HashMap::new();
let mut packer = ShelfPacker::new(atlas_w, atlas_h);
let hires = config.hires_size as f32;
for (ch, glyph_sdf) in &glyph_sdfs {
if let Some((ax, ay)) = packer.pack(glyph_sdf.width, glyph_sdf.height) {
for gy in 0..glyph_sdf.height {
for gx in 0..glyph_sdf.width {
let src = (gy * glyph_sdf.width + gx) as usize;
let dst = ((ay + gy) * atlas_w + (ax + gx)) as usize;
if src < glyph_sdf.pixels.len() && dst < atlas_pixels.len() {
atlas_pixels[dst] = glyph_sdf.pixels[src];
}
}
}
metrics.insert(*ch, SdfGlyphMetric {
uv_rect: [
ax as f32 / atlas_w as f32,
ay as f32 / atlas_h as f32,
(ax + glyph_sdf.width) as f32 / atlas_w as f32,
(ay + glyph_sdf.height) as f32 / atlas_h as f32,
],
size: glam::Vec2::new(glyph_sdf.bbox_w, glyph_sdf.bbox_h),
bearing: glam::Vec2::new(glyph_sdf.bearing_x, glyph_sdf.bearing_y),
advance: glyph_sdf.advance,
});
} else {
log::warn!("SdfGenerator: atlas full, could not pack glyph '{}'", ch);
}
}
let atlas = SdfAtlasData {
pixels: atlas_pixels,
width: atlas_w,
height: atlas_h,
channels: 1,
metrics,
spread: config.spread,
font_size_px: config.output_size as f32,
};
if let Some(ref cache_path) = config.cache_path {
save_atlas_cache(cache_path, &atlas);
}
atlas
}
fn generate_fallback_sdf(config: &SdfConfig) -> SdfGlyphData {
let size = config.output_size.max(8);
let pad = (config.spread * 1.5).ceil() as u32;
let total = size + pad * 2;
let mut pixels = vec![0u8; (total * total) as usize];
for y in 0..total {
for x in 0..total {
let dx = if x < pad {
pad as f32 - x as f32
} else if x >= size + pad {
(x - size - pad + 1) as f32
} else {
0.0
};
let dy = if y < pad {
pad as f32 - y as f32
} else if y >= size + pad {
(y - size - pad + 1) as f32
} else {
0.0
};
let dist = (dx * dx + dy * dy).sqrt();
let normalized = (-dist / config.spread) * 0.5 + 0.5;
pixels[(y * total + x) as usize] = (normalized.clamp(0.0, 1.0) * 255.0) as u8;
}
}
SdfGlyphData {
pixels,
width: total,
height: total,
advance: size as f32,
bearing_x: 0.0,
bearing_y: 0.0,
bbox_w: size as f32,
bbox_h: size as f32,
}
}
fn save_atlas_cache(path: &Path, atlas: &SdfAtlasData) {
let mut data = Vec::new();
data.extend_from_slice(b"SDF1");
data.extend_from_slice(&atlas.width.to_le_bytes());
data.extend_from_slice(&atlas.height.to_le_bytes());
data.extend_from_slice(&atlas.spread.to_le_bytes());
data.extend_from_slice(&atlas.font_size_px.to_le_bytes());
data.extend_from_slice(&(atlas.metrics.len() as u32).to_le_bytes());
for (&ch, metric) in &atlas.metrics {
data.extend_from_slice(&(ch as u32).to_le_bytes());
for &uv in &metric.uv_rect {
data.extend_from_slice(&uv.to_le_bytes());
}
data.extend_from_slice(&metric.size.x.to_le_bytes());
data.extend_from_slice(&metric.size.y.to_le_bytes());
data.extend_from_slice(&metric.bearing.x.to_le_bytes());
data.extend_from_slice(&metric.bearing.y.to_le_bytes());
data.extend_from_slice(&metric.advance.to_le_bytes());
}
data.extend_from_slice(&atlas.pixels);
if let Err(e) = std::fs::write(path, &data) {
log::warn!("SdfGenerator: failed to write cache to {:?}: {}", path, e);
} else {
log::info!("SdfGenerator: cached atlas to {:?} ({} bytes)", path, data.len());
}
}
fn load_cached_atlas(path: &Path, config: &SdfConfig) -> Option<SdfAtlasData> {
let data = std::fs::read(path).ok()?;
if data.len() < 24 {
return None;
}
if &data[0..4] != b"SDF1" {
return None;
}
let mut cursor = 4usize;
macro_rules! read_u32 {
() => {{
if cursor + 4 > data.len() { return None; }
let val = u32::from_le_bytes(data[cursor..cursor + 4].try_into().ok()?);
cursor += 4;
val
}};
}
macro_rules! read_f32 {
() => {{
if cursor + 4 > data.len() { return None; }
let val = f32::from_le_bytes(data[cursor..cursor + 4].try_into().ok()?);
cursor += 4;
val
}};
}
let width = read_u32!();
let height = read_u32!();
let spread = read_f32!();
let font_size_px = read_f32!();
let num_glyphs = read_u32!();
if (spread - config.spread).abs() > 0.01 || (font_size_px - config.output_size as f32).abs() > 0.01 {
return None;
}
let mut metrics = HashMap::new();
for _ in 0..num_glyphs {
let ch_u32 = read_u32!();
let ch = char::from_u32(ch_u32)?;
let uv_rect = [read_f32!(), read_f32!(), read_f32!(), read_f32!()];
let size = glam::Vec2::new(read_f32!(), read_f32!());
let bearing = glam::Vec2::new(read_f32!(), read_f32!());
let advance = read_f32!();
metrics.insert(ch, SdfGlyphMetric {
uv_rect,
size,
bearing,
advance,
});
}
let pixel_count = (width * height) as usize;
if cursor + pixel_count > data.len() {
return None;
}
let pixels = data[cursor..cursor + pixel_count].to_vec();
Some(SdfAtlasData {
pixels,
width,
height,
channels: 1,
metrics,
spread,
font_size_px,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dead_reckoning_zero_for_boundary() {
let bitmap = vec![
false, false, false,
false, true, false,
false, false, false,
];
let sdf = compute_sdf(&bitmap, 3, 3);
assert!(sdf[4] > 0.0);
assert!(sdf[0] < 0.0);
}
#[test]
fn dead_reckoning_all_inside() {
let bitmap = vec![true; 9];
let udf = dead_reckoning_udf(&bitmap, 3, 3);
let sdf = compute_sdf(&bitmap, 3, 3);
for &d in &sdf {
assert!(d >= 0.0);
}
}
#[test]
fn shelf_packer_fits_glyphs() {
let mut packer = ShelfPacker::new(128, 128);
let pos1 = packer.pack(32, 32);
assert!(pos1.is_some());
let pos2 = packer.pack(32, 32);
assert!(pos2.is_some());
assert_ne!(pos1, pos2);
}
#[test]
fn shelf_packer_new_shelf() {
let mut packer = ShelfPacker::new(64, 128);
let _ = packer.pack(40, 30); let pos2 = packer.pack(40, 30); assert!(pos2.is_some());
assert_eq!(pos2.unwrap().0, 0); assert_eq!(pos2.unwrap().1, 30); }
#[test]
fn shelf_packer_overflow() {
let mut packer = ShelfPacker::new(64, 64);
let _ = packer.pack(64, 64); let pos = packer.pack(10, 10); assert!(pos.is_none());
}
#[test]
fn bilinear_center() {
let data = vec![0.0, 1.0, 0.0, 1.0];
let val = sample_bilinear_f32(&data, 2, 2, 0.5, 0.5);
assert!((val - 0.5).abs() < 0.01);
}
#[test]
fn fallback_sdf_nonzero() {
let config = SdfConfig { output_size: 16, spread: 4.0, ..SdfConfig::default() };
let glyph = generate_fallback_sdf(&config);
assert!(!glyph.pixels.is_empty());
let cx = glyph.width / 2;
let cy = glyph.height / 2;
let center = glyph.pixels[(cy * glyph.width + cx) as usize];
assert!(center > 100, "Center pixel should be > 100, got {}", center);
}
}