1use crate::{
2 AssetRegistry, DevicePixels, IsZero, RenderImage, Result, SharedString, Size,
3 swap_rgba_pa_to_bgra,
4};
5use image::Frame;
6use resvg::tiny_skia::Pixmap;
7use skrifa::{FontRef, MetadataProvider};
8use smallvec::SmallVec;
9use std::{
10 hash::Hash,
11 sync::{Arc, LazyLock, OnceLock},
12};
13
14#[cfg(target_os = "macos")]
15const EMOJI_FONT_FAMILIES: &[&str] = &["Apple Color Emoji", ".AppleColorEmojiUI"];
16
17#[cfg(target_os = "windows")]
18const EMOJI_FONT_FAMILIES: &[&str] = &["Segoe UI Emoji", "Segoe UI Symbol"];
19
20#[cfg(any(target_os = "linux", target_os = "freebsd"))]
21const EMOJI_FONT_FAMILIES: &[&str] = &[
22 "Noto Color Emoji",
23 "Emoji One",
24 "Twitter Color Emoji",
25 "JoyPixels",
26];
27
28#[cfg(not(any(
29 target_os = "macos",
30 target_os = "windows",
31 target_os = "linux",
32 target_os = "freebsd",
33)))]
34const EMOJI_FONT_FAMILIES: &[&str] = &[];
35
36fn is_emoji_presentation(c: char) -> bool {
37 static EMOJI_PRESENTATION_REGEX: LazyLock<regex::Regex> =
38 LazyLock::new(|| regex::Regex::new("\\p{Emoji_Presentation}").unwrap());
39 let mut buf = [0u8; 4];
40 EMOJI_PRESENTATION_REGEX.is_match(c.encode_utf8(&mut buf))
41}
42
43fn font_has_char(db: &usvg::fontdb::Database, id: usvg::fontdb::ID, ch: char) -> bool {
44 db.with_face_data(id, |font_data, face_index| {
45 FontRef::from_index(font_data, face_index)
46 .ok()
47 .and_then(|face| face.charmap().map(ch))
48 .is_some()
49 })
50 .unwrap_or(false)
51}
52
53fn select_emoji_font(
54 ch: char,
55 fonts: &[usvg::fontdb::ID],
56 db: &usvg::fontdb::Database,
57 families: &[&str],
58) -> Option<usvg::fontdb::ID> {
59 for family_name in families {
60 let query = usvg::fontdb::Query {
61 families: &[usvg::fontdb::Family::Name(family_name)],
62 weight: usvg::fontdb::Weight(400),
63 stretch: usvg::fontdb::Stretch::Normal,
64 style: usvg::fontdb::Style::Normal,
65 };
66
67 let Some(id) = db.query(&query) else {
68 continue;
69 };
70
71 if fonts.contains(&id) || !font_has_char(db, id, ch) {
72 continue;
73 }
74
75 return Some(id);
76 }
77
78 None
79}
80
81pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
86
87#[derive(Clone, PartialEq, Hash, Eq)]
88#[expect(missing_docs)]
89pub struct RenderSvgParams {
90 pub path: SharedString,
91 pub size: Size<DevicePixels>,
92}
93
94#[derive(Clone)]
101pub struct SvgRenderer {
102 asset_registry: Arc<AssetRegistry>,
103 usvg_options: Arc<usvg::Options<'static>>,
104}
105
106pub struct ParsedSvg(usvg::Tree);
113
114#[derive(Clone, Copy)]
116pub enum SvgSize {
117 Size(Size<DevicePixels>),
119 ExactSize(Size<DevicePixels>),
121 ScaleFactor(f32),
123}
124
125impl From<f32> for SvgSize {
126 fn from(scale_factor: f32) -> Self {
127 Self::ScaleFactor(scale_factor)
128 }
129}
130
131impl SvgRenderer {
132 pub fn new(asset_registry: Arc<AssetRegistry>) -> Self {
137 static SYSTEM_FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
138 let mut db = usvg::fontdb::Database::new();
139 db.load_system_fonts();
140 Arc::new(db)
141 });
142
143 let enriched_fontdb: Arc<OnceLock<Arc<usvg::fontdb::Database>>> = Arc::new(OnceLock::new());
148
149 let default_font_resolver = usvg::FontResolver::default_font_selector();
150 let font_resolver = Box::new({
151 let asset_database = asset_registry.clone();
152 move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| {
153 if db.is_empty() {
154 let fontdb = enriched_fontdb.get_or_init(|| {
155 let mut db = (**SYSTEM_FONT_DB).clone();
156 load_bundled_fonts(&asset_database, &mut db);
157 fix_generic_font_families(&mut db);
158 Arc::new(db)
159 });
160 *db = fontdb.clone();
161 }
162 if let Some(id) = default_font_resolver(font, db) {
163 return Some(id);
164 }
165 let sans_query = usvg::fontdb::Query {
168 families: &[usvg::fontdb::Family::SansSerif],
169 ..Default::default()
170 };
171 db.query(&sans_query)
172 .or_else(|| db.faces().next().map(|f| f.id))
173 }
174 });
175 let default_fallback_selection = usvg::FontResolver::default_fallback_selector();
176 let fallback_selection = Box::new(
177 move |ch: char, fonts: &[usvg::fontdb::ID], db: &mut Arc<usvg::fontdb::Database>| {
178 if is_emoji_presentation(ch) {
179 if let Some(id) = select_emoji_font(ch, fonts, db.as_ref(), EMOJI_FONT_FAMILIES)
180 {
181 return Some(id);
182 }
183 }
184
185 default_fallback_selection(ch, fonts, db)
186 },
187 );
188 let options = usvg::Options {
189 font_resolver: usvg::FontResolver {
190 select_font: font_resolver,
191 select_fallback: fallback_selection,
192 },
193 ..Default::default()
194 };
195 Self {
196 asset_registry,
197 usvg_options: Arc::new(options),
198 }
199 }
200
201 #[tracing::instrument(skip_all)]
207 pub fn parse_svg(&self, bytes: &[u8]) -> Result<ParsedSvg, usvg::Error> {
208 usvg::Tree::from_data(bytes, &self.usvg_options).map(ParsedSvg)
209 }
210
211 #[tracing::instrument(skip_all)]
222 pub fn render_parsed(
223 &self,
224 svg: &ParsedSvg,
225 size: impl Into<SvgSize>,
226 ) -> Result<Arc<RenderImage>, usvg::Error> {
227 let (size, image_scale_factor) = match size.into() {
228 SvgSize::Size(size) => (SvgSize::Size(size), 1.0),
229 SvgSize::ExactSize(size) => (SvgSize::ExactSize(size), 1.0),
230 SvgSize::ScaleFactor(scale_factor) => (
231 SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR),
232 SMOOTH_SVG_SCALE_FACTOR,
233 ),
234 };
235 let pixmap = rasterize_tree(&svg.0, size)?;
236 let mut buffer =
237 image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
238
239 for pixel in buffer.chunks_exact_mut(4) {
240 swap_rgba_pa_to_bgra(pixel);
241 }
242
243 let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)]));
244 image.scale_factor = image_scale_factor;
245 Ok(Arc::new(image))
246 }
247
248 pub fn render_single_frame(
270 &self,
271 bytes: &[u8],
272 scale_factor: f32,
273 ) -> Result<Arc<RenderImage>, usvg::Error> {
274 let svg = self.parse_svg(bytes)?;
275 self.render_parsed(&svg, scale_factor)
276 }
277
278 pub(crate) fn render_alpha_mask(
279 &self,
280 params: &RenderSvgParams,
281 bytes: Option<&[u8]>,
282 ) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
283 anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
284
285 let render_pixmap = |bytes| {
286 let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
287
288 let size = Size::new(
290 DevicePixels(pixmap.width() as i32),
291 DevicePixels(pixmap.height() as i32),
292 );
293 let alpha_mask = pixmap
294 .pixels()
295 .iter()
296 .map(|p| p.alpha())
297 .collect::<Vec<_>>();
298
299 Ok(Some((size, alpha_mask)))
300 };
301
302 if let Some(bytes) = bytes {
303 render_pixmap(bytes)
304 } else if let Some(bytes) = self.asset_registry.load(¶ms.path) {
305 render_pixmap(&bytes)
306 } else {
307 Ok(None)
308 }
309 }
310
311 fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
312 let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
313 rasterize_tree(&tree, size)
314 }
315}
316
317fn rasterize_tree(tree: &usvg::Tree, size: SvgSize) -> Result<Pixmap, usvg::Error> {
318 const MAX_SIZE: f32 = 8192.0;
321
322 let svg_size = tree.size();
323 let (mut width, mut height) = match size {
324 SvgSize::Size(size) => {
325 let scale = i32::from(size.width) as f32 / svg_size.width();
326 (svg_size.width() * scale, svg_size.height() * scale)
327 }
328 SvgSize::ExactSize(size) => (i32::from(size.width) as f32, i32::from(size.height) as f32),
329 SvgSize::ScaleFactor(scale) => (svg_size.width() * scale, svg_size.height() * scale),
330 };
331
332 if width > MAX_SIZE {
333 log::warn!("Attempted to render pixmap where width ({width}) > MAX_SIZE ({MAX_SIZE})");
334 }
335 if height > MAX_SIZE {
336 log::warn!("Attempted to render pixmap where height ({height}) > MAX_SIZE ({MAX_SIZE})");
337 }
338 let scale = (MAX_SIZE / width).min(MAX_SIZE / height).min(1.0);
339 width *= scale;
340 height *= scale;
341
342 let mut pixmap = resvg::tiny_skia::Pixmap::new(width as u32, height as u32)
344 .ok_or(usvg::Error::InvalidSize)?;
345
346 let transform = resvg::tiny_skia::Transform::from_scale(
347 width / svg_size.width(),
348 height / svg_size.height(),
349 );
350
351 resvg::render(tree, transform, &mut pixmap.as_mut());
352
353 Ok(pixmap)
354}
355
356fn load_bundled_fonts(asset_database: &AssetRegistry, db: &mut usvg::fontdb::Database) {
357 let font_paths = [
358 "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf",
359 "fonts/lilex/Lilex-Regular.ttf",
360 ];
361 for path in font_paths {
362 match asset_database.load(path) {
363 Some(data) => db.load_font_data(data.into_owned()),
364 None => log::warn!("Bundled font not found: {path}"),
365 }
366 }
367}
368
369fn fix_generic_font_families(db: &mut usvg::fontdb::Database) {
373 use usvg::fontdb::{Family, Query};
374
375 let families_and_fallbacks: &[(Family<'_>, &str)] = &[
376 (Family::SansSerif, "IBM Plex Sans"),
377 (Family::Serif, "IBM Plex Sans"),
379 (Family::Monospace, "Lilex"),
380 (Family::Cursive, "IBM Plex Sans"),
381 (Family::Fantasy, "IBM Plex Sans"),
382 ];
383
384 for (family, fallback_name) in families_and_fallbacks {
385 let query = Query {
386 families: &[*family],
387 ..Default::default()
388 };
389 if db.query(&query).is_none() {
390 match family {
391 Family::SansSerif => db.set_sans_serif_family(*fallback_name),
392 Family::Serif => db.set_serif_family(*fallback_name),
393 Family::Monospace => db.set_monospace_family(*fallback_name),
394 Family::Cursive => db.set_cursive_family(*fallback_name),
395 Family::Fantasy => db.set_fantasy_family(*fallback_name),
396 _ => {}
397 }
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use usvg::fontdb::{Database, Family, Query};
406
407 #[test]
408 fn renders_parsed_svg_at_requested_size() -> Result<()> {
409 let renderer = SvgRenderer::new(Arc::new(AssetRegistry::default()));
410 let svg = renderer.parse_svg(
411 br#"<svg xmlns="http://www.w3.org/2000/svg" width="24pt" height="12pt"></svg>"#,
412 )?;
413 let requested_size = Size::new(DevicePixels(24), DevicePixels(12));
414 let image = renderer.render_parsed(&svg, SvgSize::ExactSize(requested_size))?;
415
416 assert_eq!(image.size(0), requested_size);
417 Ok(())
418 }
419
420 #[test]
421 fn preserves_aspect_ratio_for_width_constrained_size() -> Result<()> {
422 let renderer = SvgRenderer::new(Arc::new(AssetRegistry::default()));
423 let svg = renderer.parse_svg(
424 br#"<svg xmlns="http://www.w3.org/2000/svg" width="24pt" height="12pt"></svg>"#,
425 )?;
426 let image = renderer.render_parsed(
427 &svg,
428 SvgSize::Size(Size::new(DevicePixels(24), DevicePixels(24))),
429 )?;
430
431 assert_eq!(image.size(0), Size::new(DevicePixels(24), DevicePixels(12)));
432 Ok(())
433 }
434
435 fn db_with_bundled_fonts() -> Database {
436 let mut db = Database::new();
437 db.load_font_data(gpui_fonts::IBM_PLEX.to_vec());
438 db.load_font_data(gpui_fonts::LILEX.to_vec());
439 db
440 }
441
442 #[test]
443 fn text_with_split_glyph_clusters_in_mixed_fonts_does_not_panic() {
444 let mut db = Database::new();
445 db.load_font_data(gpui_fonts::IBM_PLEX.to_vec());
446 db.load_font_data(gpui_fonts::LILEX.to_vec());
447 let options = usvg::Options {
448 fontdb: std::sync::Arc::new(db),
449 ..Default::default()
450 };
451
452 let zalgo = "e\u{0301}\u{0302}\u{0303}\u{0304}\u{0306}\u{0307}\u{0308}\u{030a}";
457 let svg = format!(
458 r#"<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><text font-family="Lilex" font-size="32">{zalgo}<tspan font-family="IBM Plex Sans">{zalgo}</tspan></text></svg>"#
459 );
460
461 usvg::Tree::from_data(svg.as_bytes(), &options)
464 .expect("SVG with mixed-font text should parse");
465 }
466
467 #[test]
468 fn test_is_emoji_presentation() {
469 let cases = [
470 ("a", false),
471 ("Z", false),
472 ("1", false),
473 ("#", false),
474 ("*", false),
475 ("漢", false),
476 ("中", false),
477 ("カ", false),
478 ("©", false),
479 ("♥", false),
480 ("😀", true),
481 ("✅", true),
482 ("🇺🇸", true),
483 ("©️", false),
485 ("♥️", false),
486 ("1️⃣", false),
487 ];
488 for (s, expected) in cases {
489 assert_eq!(
490 is_emoji_presentation(s.chars().next().unwrap()),
491 expected,
492 "for char {:?}",
493 s
494 );
495 }
496 }
497
498 #[test]
499 fn fix_generic_font_families_sets_all_families() {
500 let mut db = db_with_bundled_fonts();
501 fix_generic_font_families(&mut db);
502
503 let families = [
504 Family::SansSerif,
505 Family::Serif,
506 Family::Monospace,
507 Family::Cursive,
508 Family::Fantasy,
509 ];
510
511 for family in families {
512 let query = Query {
513 families: &[family],
514 ..Default::default()
515 };
516 assert!(
517 db.query(&query).is_some(),
518 "Expected generic family {family:?} to resolve after fix_generic_font_families"
519 );
520 }
521 }
522
523 #[test]
524 fn test_select_emoji_font_skips_family_without_glyph() {
525 let mut db = db_with_bundled_fonts();
526
527 let ibm_plex_sans = db
528 .query(&usvg::fontdb::Query {
529 families: &[usvg::fontdb::Family::Name("IBM Plex Sans")],
530 weight: usvg::fontdb::Weight(400),
531 stretch: usvg::fontdb::Stretch::Normal,
532 style: usvg::fontdb::Style::Normal,
533 })
534 .unwrap();
535 let lilex = db
536 .query(&usvg::fontdb::Query {
537 families: &[usvg::fontdb::Family::Name("Lilex")],
538 weight: usvg::fontdb::Weight(400),
539 stretch: usvg::fontdb::Stretch::Normal,
540 style: usvg::fontdb::Style::Normal,
541 })
542 .unwrap();
543 let selected = select_emoji_font('│', &[], &db, &["IBM Plex Sans", "Lilex"]).unwrap();
544
545 assert_eq!(selected, lilex);
546 assert!(!font_has_char(&db, ibm_plex_sans, '│'));
547 assert!(font_has_char(&db, selected, '│'));
548 }
549
550 #[test]
551 fn fix_generic_font_families_monospace_resolves_to_lilex() {
552 let mut db = db_with_bundled_fonts();
553 fix_generic_font_families(&mut db);
554
555 let query = Query {
556 families: &[Family::Monospace],
557 ..Default::default()
558 };
559 let id = db.query(&query).expect("Monospace should resolve");
560 let face = db.face(id).expect("Face should exist");
561 assert!(
562 face.families.iter().any(|(name, _)| name.contains("Lilex")),
563 "Monospace should map to Lilex, got {:?}",
564 face.families
565 );
566 }
567}