use crate::{
atlas::{AtlasOptions, SdfAtlas},
edt::SdfError,
};
pub fn generate_atlas_binary(
font_data: &[u8],
glyph_ids: &[u16],
px_size: f32,
tile_size: u32,
spread: f32,
atlas_size: u32,
output_path: &std::path::Path,
) -> Result<(), SdfError> {
let mut tiles = Vec::with_capacity(glyph_ids.len());
for &glyph_id in glyph_ids {
if let Some(tile) = crate::analytic::glyph_to_sdf_tile_analytic(
font_data, glyph_id, px_size, tile_size, spread,
)? {
tiles.push(tile);
}
}
let options = AtlasOptions {
atlas_size,
padding: 1,
..Default::default()
};
let (atlas, stats) = SdfAtlas::pack_with_options(&tiles, &options);
if stats.tiles_dropped > 0 {
return Err(SdfError::InvalidInput(format!(
"{} tile(s) did not fit in the {}×{} atlas (packed {}, dropped {}). \
Increase atlas_size or reduce the glyph set.",
stats.tiles_dropped, atlas.width, atlas.height, stats.tiles_packed, stats.tiles_dropped,
)));
}
let bytes = atlas.to_bytes();
std::fs::write(output_path, &bytes).map_err(|e| SdfError::Io(e.to_string()))
}
pub fn generate_ascii_atlas(
font_data: &[u8],
px_size: f32,
output_path: &std::path::Path,
) -> Result<(), SdfError> {
let face = ttf_parser::Face::parse(font_data, 0).map_err(|_| SdfError::InvalidFont)?;
let glyph_ids: Vec<u16> = (0x0020u32..=0x007E)
.filter_map(char::from_u32)
.filter_map(|ch| face.glyph_index(ch))
.map(|gid| gid.0)
.collect();
generate_atlas_binary(font_data, &glyph_ids, px_size, 64, 4.0, 512, output_path)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_font_data() -> Vec<u8> {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
let path = std::path::PathBuf::from(&manifest).join("../../tests/fixtures/test-font.ttf");
std::fs::read(&path).expect("test font should be readable")
}
#[test]
fn test_generate_atlas_binary_writes_valid_file() {
let font_data = test_font_data();
let out = std::env::temp_dir().join("test_build_atlas.bin");
let glyph_ids = vec![36u16, 37, 38];
let result = generate_atlas_binary(&font_data, &glyph_ids, 16.0, 32, 4.0, 128, &out);
match result {
Ok(()) => {
assert!(out.exists(), "output file should exist on success");
let bytes = std::fs::read(&out).expect("output file should be readable");
let atlas = SdfAtlas::from_bytes(&bytes);
assert!(atlas.is_ok(), "generated atlas should be deserializable");
}
Err(SdfError::InvalidInput(_)) => {
}
Err(e) => panic!("unexpected error: {e}"),
}
std::fs::remove_file(&out).ok();
}
#[test]
#[ignore]
fn test_generate_ascii_atlas_for_test_font() {
let font_data = test_font_data();
let out = std::env::temp_dir().join("test_ascii_atlas.bin");
let _ = generate_ascii_atlas(&font_data, 16.0, &out);
std::fs::remove_file(&out).ok();
}
#[test]
fn test_generate_atlas_binary_empty_glyph_ids() {
let font_data = test_font_data();
let out = std::env::temp_dir().join("test_empty_atlas.bin");
let result = generate_atlas_binary(&font_data, &[], 16.0, 32, 4.0, 128, &out);
assert!(
result.is_ok(),
"empty glyph set should produce an empty atlas, got: {result:?}"
);
if out.exists() {
let bytes = std::fs::read(&out).expect("output file readable");
let atlas = SdfAtlas::from_bytes(&bytes);
assert!(atlas.is_ok(), "empty atlas should be deserializable");
}
std::fs::remove_file(&out).ok();
}
}