use crate::{SubtitleError, SubtitleResult};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AtlasConfig {
pub width: u32,
pub height: u32,
pub padding: u32,
}
impl Default for AtlasConfig {
fn default() -> Self {
Self {
width: 1024,
height: 1024,
padding: 1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AtlasSlot {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub shelf_index: usize,
}
impl AtlasSlot {
#[must_use]
pub fn u0(&self, atlas_width: u32) -> f32 {
self.x as f32 / atlas_width as f32
}
#[must_use]
pub fn u1(&self, atlas_width: u32) -> f32 {
(self.x + self.width) as f32 / atlas_width as f32
}
#[must_use]
pub fn v0(&self, atlas_height: u32) -> f32 {
self.y as f32 / atlas_height as f32
}
#[must_use]
pub fn v1(&self, atlas_height: u32) -> f32 {
(self.y + self.height) as f32 / atlas_height as f32
}
}
#[derive(Debug, Clone)]
struct Shelf {
y: u32,
height: u32,
cursor_x: u32,
}
impl Shelf {
fn new(y: u32, height: u32) -> Self {
Self {
y,
height,
cursor_x: 0,
}
}
fn try_allocate(&mut self, width: u32, padding: u32, atlas_width: u32) -> Option<u32> {
let required = width + padding;
if self.cursor_x + required > atlas_width {
return None;
}
let x = self.cursor_x;
self.cursor_x += required;
Some(x)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub codepoint: char,
pub size_q: u32,
pub font_index: u8,
}
impl GlyphKey {
#[must_use]
pub fn new(codepoint: char, size_px: f32, font_index: u8) -> Self {
Self {
codepoint,
size_q: (size_px * 4.0) as u32,
font_index,
}
}
}
pub struct GlyphAtlas {
pub config: AtlasConfig,
pixels: Vec<u8>,
shelves: Vec<Shelf>,
next_shelf_y: u32,
cache: HashMap<GlyphKey, AtlasSlot>,
allocation_count: u64,
hit_count: u64,
}
impl GlyphAtlas {
#[must_use]
pub fn new(config: AtlasConfig) -> Self {
let size = (config.width * config.height * 4) as usize;
Self {
config,
pixels: vec![0u8; size],
shelves: Vec::new(),
next_shelf_y: 0,
cache: HashMap::new(),
allocation_count: 0,
hit_count: 0,
}
}
#[must_use]
pub fn default_size() -> Self {
Self::new(AtlasConfig::default())
}
pub fn allocate(&mut self, width: u32, height: u32) -> SubtitleResult<AtlasSlot> {
if width == 0 || height == 0 {
return Err(SubtitleError::Internal(
"glyph dimensions must be non-zero".to_string(),
));
}
if width > self.config.width || height > self.config.height {
return Err(SubtitleError::Internal(format!(
"glyph {}x{} exceeds atlas {}x{}",
width, height, self.config.width, self.config.height
)));
}
let padding = self.config.padding;
let shelf_index = self.find_best_shelf(height);
if let Some(idx) = shelf_index {
if let Some(x) = self.shelves[idx].try_allocate(width, padding, self.config.width) {
let slot = AtlasSlot {
x,
y: self.shelves[idx].y,
width,
height,
shelf_index: idx,
};
self.allocation_count += 1;
return Ok(slot);
}
}
let new_shelf_y = self.next_shelf_y;
let shelf_height = height + padding;
if new_shelf_y + shelf_height > self.config.height {
return Err(SubtitleError::Internal(
"glyph atlas is full — increase atlas dimensions or flush cache".to_string(),
));
}
let new_idx = self.shelves.len();
let mut shelf = Shelf::new(new_shelf_y, shelf_height);
let x = shelf
.try_allocate(width, padding, self.config.width)
.ok_or_else(|| SubtitleError::Internal("glyph wider than atlas".to_string()))?;
self.next_shelf_y += shelf_height;
self.shelves.push(shelf);
let slot = AtlasSlot {
x,
y: new_shelf_y,
width,
height,
shelf_index: new_idx,
};
self.allocation_count += 1;
Ok(slot)
}
fn find_best_shelf(&self, glyph_height: u32) -> Option<usize> {
let mut best_idx: Option<usize> = None;
let mut best_waste = u32::MAX;
for (idx, shelf) in self.shelves.iter().enumerate() {
if shelf.height < glyph_height {
continue;
}
let waste = shelf.height - glyph_height;
if waste < best_waste {
best_waste = waste;
best_idx = Some(idx);
}
}
best_idx
}
pub fn get(&mut self, key: &GlyphKey) -> Option<&AtlasSlot> {
if let Some(slot) = self.cache.get(key) {
self.hit_count += 1;
Some(slot)
} else {
None
}
}
pub fn insert(&mut self, key: GlyphKey, slot: AtlasSlot, bitmap: &[u8]) -> SubtitleResult<()> {
let expected = (slot.width * slot.height) as usize;
if bitmap.len() != expected {
return Err(SubtitleError::Internal(format!(
"bitmap length {} != expected {}",
bitmap.len(),
expected
)));
}
let atlas_w = self.config.width as usize;
for row in 0..slot.height as usize {
for col in 0..slot.width as usize {
let atlas_x = slot.x as usize + col;
let atlas_y = slot.y as usize + row;
let pixel_idx = (atlas_y * atlas_w + atlas_x) * 4;
let alpha = bitmap[row * slot.width as usize + col];
self.pixels[pixel_idx] = 255; self.pixels[pixel_idx + 1] = 255; self.pixels[pixel_idx + 2] = 255; self.pixels[pixel_idx + 3] = alpha; }
}
self.cache.insert(key, slot);
Ok(())
}
#[must_use]
pub fn pixels(&self) -> &[u8] {
&self.pixels
}
pub fn pixels_mut(&mut self) -> &mut [u8] {
&mut self.pixels
}
#[must_use]
pub fn pixel_slice_mut(&mut self, slot: &AtlasSlot) -> Option<PixelSliceMut<'_>> {
let atlas_w = self.config.width;
if slot.x + slot.width > atlas_w || slot.y + slot.height > self.config.height {
return None;
}
Some(PixelSliceMut {
pixels: &mut self.pixels,
atlas_width: atlas_w,
slot: *slot,
})
}
#[must_use]
pub fn cached_count(&self) -> usize {
self.cache.len()
}
#[must_use]
pub fn allocation_count(&self) -> u64 {
self.allocation_count
}
#[must_use]
pub fn hit_count(&self) -> u64 {
self.hit_count
}
#[must_use]
pub fn hit_ratio(&self) -> f64 {
let total = self.allocation_count + self.hit_count;
if total == 0 {
return 0.0;
}
self.hit_count as f64 / total as f64
}
pub fn clear(&mut self) {
self.pixels.iter_mut().for_each(|p| *p = 0);
self.shelves.clear();
self.next_shelf_y = 0;
self.cache.clear();
self.allocation_count = 0;
self.hit_count = 0;
}
#[must_use]
pub fn shelf_count(&self) -> usize {
self.shelves.len()
}
#[must_use]
pub fn remaining_height(&self) -> u32 {
self.config.height.saturating_sub(self.next_shelf_y)
}
#[must_use]
pub fn vertical_utilisation(&self) -> f32 {
if self.config.height == 0 {
return 0.0;
}
self.next_shelf_y as f32 / self.config.height as f32
}
}
pub struct PixelSliceMut<'a> {
pixels: &'a mut Vec<u8>,
atlas_width: u32,
slot: AtlasSlot,
}
impl<'a> PixelSliceMut<'a> {
pub fn set_pixel(&mut self, col: u32, row: u32, r: u8, g: u8, b: u8, a: u8) {
if col >= self.slot.width || row >= self.slot.height {
return;
}
let ax = self.slot.x + col;
let ay = self.slot.y + row;
let idx = ((ay * self.atlas_width + ax) * 4) as usize;
if idx + 3 < self.pixels.len() {
self.pixels[idx] = r;
self.pixels[idx + 1] = g;
self.pixels[idx + 2] = b;
self.pixels[idx + 3] = a;
}
}
pub fn set_alpha(&mut self, col: u32, row: u32, alpha: u8) {
self.set_pixel(col, row, 255, 255, 255, alpha);
}
#[must_use]
pub fn width(&self) -> u32 {
self.slot.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.slot.height
}
}
#[cfg(test)]
mod tests {
use super::*;
fn small_atlas() -> GlyphAtlas {
GlyphAtlas::new(AtlasConfig {
width: 128,
height: 128,
padding: 1,
})
}
#[test]
fn test_allocate_single_glyph() {
let mut atlas = small_atlas();
let slot = atlas.allocate(16, 20).expect("allocation should succeed");
assert_eq!(slot.x, 0);
assert_eq!(slot.y, 0);
assert_eq!(slot.width, 16);
assert_eq!(slot.height, 20);
}
#[test]
fn test_allocate_multiple_glyphs_same_shelf() {
let mut atlas = small_atlas();
let s1 = atlas.allocate(10, 16).expect("first allocation");
let s2 = atlas.allocate(12, 16).expect("second allocation");
assert_eq!(s1.shelf_index, s2.shelf_index);
assert!(s2.x > s1.x);
}
#[test]
fn test_allocate_new_shelf_for_taller_glyph() {
let mut atlas = GlyphAtlas::new(AtlasConfig {
width: 128,
height: 256,
padding: 1,
});
let s1 = atlas.allocate(32, 16).expect("small glyph");
let s2 = atlas.allocate(32, 32).expect("tall glyph");
assert_ne!(s1.shelf_index, s2.shelf_index);
}
#[test]
fn test_atlas_full_error() {
let mut atlas = GlyphAtlas::new(AtlasConfig {
width: 4,
height: 4,
padding: 0,
});
atlas.allocate(4, 4).expect("should fit");
let result = atlas.allocate(4, 4);
assert!(result.is_err(), "atlas should be full");
}
#[test]
fn test_insert_and_get_from_cache() {
let mut atlas = small_atlas();
let key = GlyphKey::new('A', 24.0, 0);
let slot = atlas.allocate(8, 10).expect("allocation");
let bitmap = vec![128u8; (slot.width * slot.height) as usize];
atlas.insert(key, slot, &bitmap).expect("insert");
let result = atlas.get(&key);
assert!(result.is_some(), "should find glyph in cache");
}
#[test]
fn test_cache_hit_count() {
let mut atlas = small_atlas();
let key = GlyphKey::new('B', 16.0, 0);
let slot = atlas.allocate(6, 8).expect("allocation");
let bitmap = vec![255u8; (slot.width * slot.height) as usize];
atlas.insert(key, slot, &bitmap).expect("insert");
atlas.get(&key);
atlas.get(&key);
assert_eq!(atlas.hit_count(), 2);
}
#[test]
fn test_insert_wrong_bitmap_size_errors() {
let mut atlas = small_atlas();
let key = GlyphKey::new('C', 12.0, 0);
let slot = atlas.allocate(8, 8).expect("allocation");
let bad_bitmap = vec![0u8; 10]; let result = atlas.insert(key, slot, &bad_bitmap);
assert!(result.is_err());
}
#[test]
fn test_atlas_clear_resets_state() {
let mut atlas = small_atlas();
atlas.allocate(20, 20).expect("allocation");
atlas.clear();
assert_eq!(atlas.shelf_count(), 0);
assert_eq!(atlas.cached_count(), 0);
assert_eq!(atlas.allocation_count(), 0);
assert_eq!(atlas.remaining_height(), 128);
}
#[test]
fn test_uv_coordinates() {
let slot = AtlasSlot {
x: 0,
y: 0,
width: 512,
height: 512,
shelf_index: 0,
};
assert!((slot.u0(1024) - 0.0).abs() < 1e-6);
assert!((slot.u1(1024) - 0.5).abs() < 1e-6);
assert!((slot.v0(1024) - 0.0).abs() < 1e-6);
assert!((slot.v1(1024) - 0.5).abs() < 1e-6);
}
#[test]
fn test_vertical_utilisation() {
let mut atlas = GlyphAtlas::new(AtlasConfig {
width: 64,
height: 64,
padding: 0,
});
let util_before = atlas.vertical_utilisation();
assert!((util_before - 0.0).abs() < 1e-6);
atlas.allocate(8, 32).expect("allocation");
let util_after = atlas.vertical_utilisation();
assert!(util_after > 0.0);
assert!(util_after <= 1.0);
}
#[test]
fn test_pixel_slice_mut_writes() {
let mut atlas = small_atlas();
let slot = atlas.allocate(4, 4).expect("allocation");
{
let mut slice = atlas.pixel_slice_mut(&slot).expect("pixel slice");
slice.set_alpha(0, 0, 200);
slice.set_pixel(1, 1, 100, 150, 200, 255);
}
let w = atlas.config.width as usize;
let idx = (slot.y as usize * w + slot.x as usize) * 4;
assert_eq!(atlas.pixels()[idx + 3], 200, "alpha at (0,0) should be 200");
}
#[test]
fn test_glyph_key_equality() {
let k1 = GlyphKey::new('A', 24.0, 0);
let k2 = GlyphKey::new('A', 24.0, 0);
let k3 = GlyphKey::new('B', 24.0, 0);
assert_eq!(k1, k2);
assert_ne!(k1, k3);
}
#[test]
fn test_hit_ratio_zero_on_empty() {
let atlas = small_atlas();
assert!((atlas.hit_ratio() - 0.0).abs() < 1e-6);
}
#[test]
fn test_zero_dimension_error() {
let mut atlas = small_atlas();
assert!(atlas.allocate(0, 16).is_err());
assert!(atlas.allocate(16, 0).is_err());
}
#[test]
fn test_shelf_count_increases() {
let mut atlas = GlyphAtlas::new(AtlasConfig {
width: 256,
height: 256,
padding: 1,
});
assert_eq!(atlas.shelf_count(), 0);
atlas.allocate(16, 16).expect("first");
assert_eq!(atlas.shelf_count(), 1);
atlas.allocate(16, 32).expect("second");
assert_eq!(atlas.shelf_count(), 2);
}
}