//! Packs reusable rasterized glyphs into shared texture pages. use crate::{ PatchRect, TextureHandle, Textures, util::{HashMap, Vec2}, }; use image::RgbaImage; use swash::scale::image::{Content, Image}; /// A 1024-pixel RGBA8 page occupies 4 MiB. const PAGE: u32 = 1024; /// Transparent margin kept around every glyph, so that sampling one cannot /// pick up its neighbour along a shared edge. const PAD: u32 = 1; /// Includes every input that can change the rasterized pixels. #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct GlyphKey { pub font: u64, pub glyph: u32, /// Font size in 1/16 px, so sizes that round to the same pixels share a /// raster instead of filling the atlas with near-duplicates. pub size: u32, /// Horizontal subpixel phase, in 1/4 px. pub subpixel: u8, /// Hash of the variation coordinates; a variable font at two weights is two /// different sets of pixels from one glyph id. pub coords: u64, } #[derive(Clone, Copy)] pub struct GlyphEntry { pub uv_min: [f32; 2], pub uv_max: [f32; 2], /// Offset from the glyph's pen position to the top-left of its pixels. pub left: i32, pub top: i32, pub width: u32, pub height: u32, pub is_color: bool, pub view_idx: u32, pub sampler_idx: u32, } struct Page { handle: TextureHandle, /// Shelf packing is effective because glyphs at one size have similar heights. x: u32, y: u32, shelf_height: u32, } #[derive(Default)] pub struct GlyphAtlas { pages: Vec, /// `None` for a glyph that rasterised to nothing -- a space, say. Cached /// too, so it is not re-rasterised on every layout. entries: HashMap>, } impl GlyphAtlas { pub fn get(&self, key: &GlyphKey) -> Option> { self.entries.get(key).copied() } /// Returns `None` when a glyph has no pixels or cannot fit on a page. pub fn insert( &mut self, key: GlyphKey, image: &Image, textures: &mut Textures, ) -> Option { let w = image.placement.width; let h = image.placement.height; if w == 0 || h == 0 { self.entries.insert(key, None); return None; } if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE { // Never silently crop an oversized glyph. self.entries.insert(key, None); return None; } let (page_idx, x, y) = self.allocate(w, h, textures); let page = &self.pages[page_idx]; let img = textures.image_mut(&page.handle); let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); write_glyph(rgba, image, x, y); let handle = page.handle.clone(); let rect = PatchRect { x, y, width: w, height: h, }; textures.patch(&handle, rect); let page = &self.pages[page_idx]; let scale = 1.0 / PAGE as f32; let entry = GlyphEntry { uv_min: [x as f32 * scale, y as f32 * scale], uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale], left: image.placement.left, top: image.placement.top, width: w, height: h, is_color: matches!(image.content, Content::Color), view_idx: page.handle.primitive().view_idx, sampler_idx: page.handle.primitive().sampler_idx, }; self.entries.insert(key, Some(entry)); Some(entry) } fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { let need_w = w + PAD; let need_h = h + PAD; if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) { let page = &mut self.pages[i]; if page.x + need_w > PAGE { page.y += page.shelf_height; page.x = PAD; page.shelf_height = 0; } let (x, y) = (page.x, page.y); page.x += need_w; page.shelf_height = page.shelf_height.max(need_h); return (i, x, y); } let handle = textures.add(RgbaImage::new(PAGE, PAGE)); self.pages.push(Page { handle, x: PAD + w + PAD, y: PAD, shelf_height: h + PAD, }); (self.pages.len() - 1, PAD, PAD) } /// Record that a glyph has no pixels, so it is not re-rasterised. pub fn insert_empty(&mut self, key: GlyphKey) { self.entries.insert(key, None); } pub fn page_count(&self) -> usize { self.pages.len() } pub fn glyph_count(&self) -> usize { self.entries.len() } } fn fits(page: &Page, need_w: u32, need_h: u32) -> bool { // On the current shelf, or on a new one above it. (page.x + need_w <= PAGE && page.y + need_h <= PAGE) || (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE) } /// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time. fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) { let w = image.placement.width; let h = image.placement.height; match image.content { Content::Mask => { for row in 0..h { for col in 0..w { let a = image.data[(row * w + col) as usize]; page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a])); } } } Content::Color => { for row in 0..h { for col in 0..w { let i = ((row * w + col) * 4) as usize; let px = [ image.data[i], image.data[i + 1], image.data[i + 2], image.data[i + 3], ]; page.put_pixel(x + col, y + row, image::Rgba(px)); } } } Content::SubpixelMask => { // Preserve readable output if the rasterizer returns a subpixel mask. for row in 0..h { for col in 0..w { let i = ((row * w + col) * 4) as usize; let a = image.data[i + 1]; page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a])); } } } } } /// Where a glyph goes on screen, in pixels relative to the text's origin. #[derive(Clone, Copy)] pub struct PlacedGlyph { pub entry: GlyphEntry, pub offset: Vec2, }