avenger_wgpu/marks/
cosmic.rs

1use crate::canvas::CanvasDimensions;
2use crate::error::AvengerWgpuError;
3use crate::marks::text::{
4    GlyphBBox, GlyphBBoxAndAtlasCoords, GlyphImage, PhysicalGlyphPosition, TextRasterizationBuffer,
5    TextRasterizationConfig, TextRasterizer,
6};
7use avenger::marks::text::{FontWeightNameSpec, FontWeightSpec};
8use cosmic_text::fontdb::Database;
9use cosmic_text::{
10    Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache, SwashContent, Weight,
11};
12use lazy_static;
13use std::collections::{HashMap, HashSet};
14use std::sync::Mutex;
15
16lazy_static! {
17    static ref FONT_SYSTEM: Mutex<FontSystem> = Mutex::new(build_font_system());
18    static ref SWASH_CACHE: Mutex<SwashCache> = Mutex::new(SwashCache::new());
19}
20
21fn build_font_system() -> FontSystem {
22    let mut font_system = FontSystem::new();
23
24    // Override default families based on what system fonts are available
25    let fontdb = font_system.db_mut();
26    setup_default_fonts(fontdb);
27    font_system
28}
29
30fn setup_default_fonts(fontdb: &mut Database) {
31    let families: HashSet<String> = fontdb
32        .faces()
33        .flat_map(|face| {
34            face.families
35                .iter()
36                .map(|(fam, _lang)| fam.clone())
37                .collect::<Vec<_>>()
38        })
39        .collect();
40
41    // Set default sans serif
42    for family in ["Helvetica", "Arial", "Liberation Sans"] {
43        if families.contains(family) {
44            fontdb.set_sans_serif_family(family);
45            break;
46        }
47    }
48
49    // Set default monospace font family
50    for family in [
51        "Courier New",
52        "Courier",
53        "Liberation Mono",
54        "DejaVu Sans Mono",
55    ] {
56        if families.contains(family) {
57            fontdb.set_monospace_family(family);
58            break;
59        }
60    }
61
62    // Set default serif font family
63    for family in [
64        "Times New Roman",
65        "Times",
66        "Liberation Serif",
67        "DejaVu Serif",
68    ] {
69        if families.contains(family) {
70            fontdb.set_serif_family(family);
71            break;
72        }
73    }
74}
75
76pub fn register_font_directory(dir: &str) {
77    let mut font_system = FONT_SYSTEM
78        .lock()
79        .expect("Failed to acquire lock on FONT_SYSTEM");
80    let fontdb = font_system.db_mut();
81    fontdb.load_fonts_dir(dir);
82    setup_default_fonts(fontdb);
83}
84
85type CosmicCacheKey = (cosmic_text::CacheKey, [u8; 4]);
86
87#[derive(Clone, Debug)]
88pub struct CosmicTextRasterizer;
89
90impl TextRasterizer for CosmicTextRasterizer {
91    type CacheKey = CosmicCacheKey;
92
93    fn rasterize(
94        &self,
95        dimensions: CanvasDimensions,
96        config: &TextRasterizationConfig,
97        cached_glyphs: &HashMap<Self::CacheKey, GlyphBBoxAndAtlasCoords>,
98    ) -> Result<TextRasterizationBuffer<Self::CacheKey>, AvengerWgpuError> {
99        let mut font_system = FONT_SYSTEM
100            .lock()
101            .expect("Failed to acquire lock on FONT_SYSTEM");
102        let mut cache = SWASH_CACHE
103            .lock()
104            .expect("Failed to acquire lock on SWASH_CACHE");
105
106        // Build image cache
107        let mut next_cache: HashMap<CosmicCacheKey, GlyphImage<CosmicCacheKey>> = HashMap::new();
108
109        // Build cosmic-text Buffer
110        let mut buffer = Buffer::new(
111            &mut font_system,
112            Metrics::new(config.font_size, config.font_size),
113        );
114        let family = match config.font.to_lowercase().as_str() {
115            "serif" => Family::Serif,
116            "sans serif" | "sans-serif" => Family::SansSerif,
117            "cursive" => Family::Cursive,
118            "fantasy" => Family::Fantasy,
119            "monospace" => Family::Monospace,
120            _ => Family::Name(config.font.as_str()),
121        };
122        let weight = match config.font_weight {
123            FontWeightSpec::Name(FontWeightNameSpec::Bold) => Weight::BOLD,
124            FontWeightSpec::Name(FontWeightNameSpec::Normal) => Weight::NORMAL,
125            FontWeightSpec::Number(w) => Weight(*w as u16),
126        };
127
128        buffer.set_text(
129            &mut font_system,
130            config.text,
131            Attrs::new().family(family).weight(weight),
132            Shaping::Advanced,
133        );
134
135        buffer.set_size(&mut font_system, dimensions.size[0], dimensions.size[1]);
136        buffer.shape_until_scroll(&mut font_system, false);
137
138        let (buffer_width, line_y, buffer_height) = measure(&buffer);
139
140        let text_color = [
141            (config.color[0] * 255.0).round() as u8,
142            (config.color[1] * 255.0).round() as u8,
143            (config.color[2] * 255.0).round() as u8,
144            (config.color[3] * 255.0).round() as u8,
145        ];
146
147        // Initialize glyphs
148        let mut glyphs: Vec<(GlyphImage<CosmicCacheKey>, PhysicalGlyphPosition)> = Vec::new();
149
150        for run in buffer.layout_runs() {
151            for glyph in run.glyphs.iter() {
152                let physical_glyph = glyph.physical((0.0, 0.0), dimensions.scale);
153
154                let phys_pos = PhysicalGlyphPosition {
155                    x: physical_glyph.x as f32,
156                    y: physical_glyph.y as f32,
157                };
158
159                // Compute cache key which combines glyph and color
160                let cache_key = (physical_glyph.cache_key, text_color);
161
162                if let Some(glyph_image) = next_cache.get(&cache_key) {
163                    // Glyph has already been rasterized by this call to rasterize and the full image
164                    // is already in the glyphs Vec, so we can store the reference only.
165                    glyphs.push((glyph_image.without_image(), phys_pos));
166                } else if let Some(glyph_bbox_and_altas_coords) = cached_glyphs.get(&cache_key) {
167                    // Glyph already rasterized by a prior call to rasterize(), so we can just
168                    // store the cache key and position info.
169                    glyphs.push((
170                        GlyphImage {
171                            cache_key,
172                            image: None,
173                            bbox: glyph_bbox_and_altas_coords.bbox,
174                        },
175                        phys_pos,
176                    ));
177                } else {
178                    // We need to rasterize glyph and write it to next_atlas
179                    let Some(image) = cache
180                        .get_image(&mut font_system, physical_glyph.cache_key)
181                        .as_ref()
182                    else {
183                        return Err(AvengerWgpuError::ImageAllocationError(
184                            "Failed to create glyph image".to_string(),
185                        ));
186                    };
187
188                    let width = image.placement.width as usize;
189                    let height = image.placement.height as usize;
190                    let should_rasterize = width > 0 && height > 0;
191
192                    if !should_rasterize {
193                        continue;
194                    }
195
196                    let img = match image.content {
197                        SwashContent::Color => {
198                            // Image is rgba (like an emoji)
199                            let Some(img) = image::RgbaImage::from_vec(
200                                width as u32,
201                                height as u32,
202                                image.data.clone(),
203                            ) else {
204                                return Err(AvengerWgpuError::ImageAllocationError(
205                                    "Failed to parse text rasterization as Rgba image".to_string(),
206                                ));
207                            };
208                            img
209                        }
210                        SwashContent::Mask | SwashContent::SubpixelMask => {
211                            // Image is monochrome (like regular text)
212                            let Some(monochrome_img) = image::GrayImage::from_vec(
213                                width as u32,
214                                height as u32,
215                                image.data.clone(),
216                            ) else {
217                                return Err(AvengerWgpuError::ImageAllocationError(
218                                    "Failed to parse text rasterization as Grayscale image"
219                                        .to_string(),
220                                ));
221                            };
222
223                            // Initialize empty rgba image
224                            let mut img = image::RgbaImage::new(
225                                monochrome_img.width(),
226                                monochrome_img.height(),
227                            );
228
229                            // Write colored image
230                            for x in 0..monochrome_img.width() {
231                                for y in 0..monochrome_img.height() {
232                                    let pixel_lum = monochrome_img.get_pixel(x, y).0[0];
233                                    // Compute pixel color, adjusting alpha by pixel luminance
234                                    let mut pixel_color = text_color;
235                                    pixel_color[3] =
236                                        ((text_color[3] as f32) * (pixel_lum as f32 / 255.0))
237                                            .round() as u8;
238
239                                    // Write pixel to rgba image
240                                    let pixel = image::Rgba::from(pixel_color);
241                                    img.put_pixel(x, y, pixel);
242                                }
243                            }
244                            img
245                        }
246                    };
247
248                    // Create new glyph image
249                    let glyph_image = GlyphImage {
250                        cache_key: (physical_glyph.cache_key, text_color),
251                        image: Some(img),
252                        bbox: GlyphBBox {
253                            top: image.placement.top,
254                            left: image.placement.left,
255                            width: image.placement.width,
256                            height: image.placement.height,
257                        },
258                    };
259
260                    // Update cache
261                    next_cache.insert(cache_key, glyph_image.without_image());
262
263                    glyphs.push((glyph_image, phys_pos));
264                };
265            }
266        }
267
268        Ok(TextRasterizationBuffer {
269            glyphs,
270            buffer_width,
271            buffer_height,
272            buffer_line_y: line_y,
273        })
274    }
275}
276
277pub fn measure(buffer: &Buffer) -> (f32, f32, f32) {
278    let (width, line_y, total_lines) =
279        buffer
280            .layout_runs()
281            .fold((0.0, 0.0, 0usize), |(width, line_y, total_lines), run| {
282                (
283                    run.line_w.max(width),
284                    run.line_y.max(line_y),
285                    total_lines + 1,
286                )
287            });
288    (
289        width,
290        line_y,
291        total_lines as f32 * buffer.metrics().line_height,
292    )
293}