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 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 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 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 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 let mut next_cache: HashMap<CosmicCacheKey, GlyphImage<CosmicCacheKey>> = HashMap::new();
108
109 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 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 let cache_key = (physical_glyph.cache_key, text_color);
161
162 if let Some(glyph_image) = next_cache.get(&cache_key) {
163 glyphs.push((glyph_image.without_image(), phys_pos));
166 } else if let Some(glyph_bbox_and_altas_coords) = cached_glyphs.get(&cache_key) {
167 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 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 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 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 let mut img = image::RgbaImage::new(
225 monochrome_img.width(),
226 monochrome_img.height(),
227 );
228
229 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 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 let pixel = image::Rgba::from(pixel_color);
241 img.put_pixel(x, y, pixel);
242 }
243 }
244 img
245 }
246 };
247
248 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 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}