1use crate::{
2 Bounds, DevicePixels, Font, FontFeature, FontFeatures, FontId, FontMetrics, FontRun, FontStyle,
3 FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams,
4 SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point,
5 size,
6};
7use anyhow::{Context as _, Ok, Result};
8use collections::HashMap;
9use cosmic_text::{
10 Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
11 FontSystem, ShapeBuffer, ShapeLine, SwashCache,
12};
13
14use itertools::Itertools;
15use parking_lot::RwLock;
16use pathfinder_geometry::{
17 rect::{RectF, RectI},
18 vector::{Vector2F, Vector2I},
19};
20use smallvec::SmallVec;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::{borrow::Cow, sync::Arc};
23
24const EMOJI_SIZE_SCALE: f32 = 1.1;
29
30pub(crate) struct CosmicTextSystem {
31 state: Arc<RwLock<CosmicTextSystemState>>,
32 fonts_loaded: Arc<AtomicBool>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38struct FontKey {
39 family: SharedString,
40 features: FontFeatures,
41}
42
43impl FontKey {
44 fn new(family: SharedString, features: FontFeatures) -> Self {
45 Self { family, features }
46 }
47}
48
49struct CosmicTextSystemState {
50 swash_cache: SwashCache,
51 font_system: FontSystem,
52 scratch: ShapeBuffer,
53 loaded_fonts: Vec<LoadedFont>,
55 font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
58}
59
60struct LoadedFont {
61 font: Arc<CosmicTextFont>,
62 features: CosmicFontFeatures,
63 is_known_emoji_font: bool,
64 weight: cosmic_text::fontdb::Weight,
65}
66
67impl CosmicTextSystem {
68 pub(crate) fn new() -> Self {
69 let fonts_loaded = Arc::new(AtomicBool::new(false));
70 let fonts_loaded_clone = fonts_loaded.clone();
71
72 let locale = std::env::var("LANG")
76 .ok()
77 .and_then(|l| l.split('.').next().map(|s| s.replace('_', "-")))
78 .unwrap_or_else(|| String::from("en-US"));
79 let font_system =
80 FontSystem::new_with_locale_and_db(locale, cosmic_text::fontdb::Database::new());
81
82 let state = Arc::new(RwLock::new(CosmicTextSystemState {
83 font_system,
84 swash_cache: SwashCache::new(),
85 scratch: ShapeBuffer::default(),
86 loaded_fonts: Vec::new(),
87 font_ids_by_family_cache: HashMap::default(),
88 }));
89
90 let result = Self {
91 state: state.clone(),
92 fonts_loaded,
93 };
94
95 std::thread::Builder::new()
97 .name("font-loader".into())
98 .spawn(move || {
99 let loaded_font_system = FontSystem::new();
101
102 let mut state_guard = state.write();
104 state_guard.font_system = loaded_font_system;
105 drop(state_guard);
106
107 fonts_loaded_clone.store(true, Ordering::Release);
108 })
109 .expect("failed to spawn font-loader thread");
110
111 result
112 }
113
114 fn ensure_fonts_loaded(&self) {
118 if !self.fonts_loaded.load(Ordering::Acquire) {
119 while !self.fonts_loaded.load(Ordering::Acquire) {
121 std::thread::yield_now();
122 }
123 }
124 }
125}
126
127impl Default for CosmicTextSystem {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl PlatformTextSystem for CosmicTextSystem {
134 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
135 self.state.write().add_fonts(fonts)
136 }
137
138 fn all_font_names(&self) -> Vec<String> {
139 self.ensure_fonts_loaded();
140 let mut result = self
141 .state
142 .read()
143 .font_system
144 .db()
145 .faces()
146 .filter_map(|face| face.families.first().map(|family| family.0.clone()))
147 .collect_vec();
148 result.sort();
149 result.dedup();
150 result
151 }
152
153 fn font_id(&self, font: &Font) -> Result<FontId> {
154 self.ensure_fonts_loaded();
155 let mut state = self.state.write();
156 let key = FontKey::new(font.family.clone(), font.features.clone());
157 let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
158 font_ids.as_slice()
159 } else {
160 let font_ids = state.load_family(&font.family, &font.features)?;
161 state.font_ids_by_family_cache.insert(key.clone(), font_ids);
162 state.font_ids_by_family_cache[&key].as_ref()
163 };
164
165 let candidate_properties = candidates
166 .iter()
167 .map(|font_id| {
168 let database_id = state.loaded_font(*font_id).font.id();
169 let face_info = state.font_system.db().face(database_id).expect("");
170 face_info_into_properties(face_info)
171 })
172 .collect::<SmallVec<[_; 4]>>();
173
174 let ix =
175 font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
176 .context("requested font family contains no font matching the other parameters")?;
177
178 Ok(candidates[ix])
179 }
180
181 fn font_metrics(&self, font_id: FontId) -> FontMetrics {
182 let lock = self.state.read();
183 let loaded_font = lock.loaded_font(font_id);
184 let swash_font = loaded_font.font.as_swash();
185 let metrics = swash_font.metrics(&[]);
186
187 let bbox_height = metrics.ascent + metrics.descent.abs();
191 let bounding_box = Bounds {
192 origin: point(0.0, -metrics.descent.abs()),
193 size: size(metrics.max_width, bbox_height),
194 };
195
196 FontMetrics {
197 units_per_em: metrics.units_per_em as u32,
198 ascent: metrics.ascent,
199 descent: -metrics.descent.abs(),
200 line_gap: metrics.leading,
201 underline_position: metrics.underline_offset,
202 underline_thickness: metrics.stroke_size,
203 cap_height: metrics.cap_height,
204 x_height: metrics.x_height,
205 bounding_box,
206 }
207 }
208
209 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
210 let lock = self.state.read();
211 let loaded_font = lock.loaded_font(font_id);
212 let swash_font = loaded_font.font.as_swash();
213 let glyph_metrics = swash_font.glyph_metrics(&[]);
214 let glyph_id_u16 = glyph_id.0 as u16;
215
216 let advance_width = glyph_metrics.advance_width(glyph_id_u16);
219 let advance_height = glyph_metrics.advance_height(glyph_id_u16);
220 let lsb = glyph_metrics.lsb(glyph_id_u16);
221 let tsb = glyph_metrics.tsb(glyph_id_u16);
222
223 Ok(Bounds {
224 origin: point(lsb, tsb),
225 size: size(advance_width, advance_height),
226 })
227 }
228
229 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
230 self.state.read().advance(font_id, glyph_id)
231 }
232
233 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
234 self.state.read().glyph_for_char(font_id, ch)
235 }
236
237 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
238 self.state.write().raster_bounds(params)
239 }
240
241 fn rasterize_glyph(
242 &self,
243 params: &RenderGlyphParams,
244 raster_bounds: Bounds<DevicePixels>,
245 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
246 self.state.write().rasterize_glyph(params, raster_bounds)
247 }
248
249 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
250 self.ensure_fonts_loaded();
251 self.state.write().layout_line(text, font_size, runs)
252 }
253
254 fn layout_line_with_features(
255 &self,
256 text: &str,
257 font_size: Pixels,
258 runs: &[FontRun],
259 features: &[FontFeature],
260 ) -> LineLayout {
261 if features.is_empty() {
262 return self.layout_line(text, font_size, runs);
263 }
264 self.ensure_fonts_loaded();
265 self.state
266 .write()
267 .layout_line_with_features(text, font_size, runs, features)
268 }
269}
270
271impl CosmicTextSystemState {
272 fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
273 &self.loaded_fonts[font_id.0]
274 }
275
276 #[profiling::function]
277 fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
278 let db = self.font_system.db_mut();
279 for bytes in fonts {
280 match bytes {
281 Cow::Borrowed(embedded_font) => {
282 db.load_font_data(embedded_font.to_vec());
283 }
284 Cow::Owned(bytes) => {
285 db.load_font_data(bytes);
286 }
287 }
288 }
289 Ok(())
290 }
291
292 #[profiling::function]
293 fn load_family(
294 &mut self,
295 name: &str,
296 features: &FontFeatures,
297 ) -> Result<SmallVec<[FontId; 4]>> {
298 let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
300
301 let families = self
302 .font_system
303 .db()
304 .faces()
305 .filter(|face| face.families.iter().any(|family| *name == family.0))
306 .map(|face| (face.id, face.post_script_name.clone(), face.weight))
307 .collect::<SmallVec<[_; 4]>>();
308
309 let mut loaded_font_ids = SmallVec::new();
310 for (font_id, postscript_name, weight) in families {
311 let font = self
312 .font_system
313 .get_font(font_id, weight)
314 .context("Could not load font")?;
315
316 let allowed_bad_font_names = [
318 "SegoeFluentIcons", "Segoe Fluent Icons",
320 ];
321
322 if font.as_swash().charmap().map('m') == 0
323 && !allowed_bad_font_names.contains(&postscript_name.as_str())
324 {
325 self.font_system.db_mut().remove_face(font.id());
326 continue;
327 };
328
329 let font_id = FontId(self.loaded_fonts.len());
330 loaded_font_ids.push(font_id);
331 self.loaded_fonts.push(LoadedFont {
332 font: font.clone(),
333 features: features.try_into()?,
334 is_known_emoji_font: is_color_emoji_font(&postscript_name, &font),
335 weight,
336 });
337 }
338
339 Ok(loaded_font_ids)
340 }
341
342 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
343 let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
344 Ok(Size {
345 width: glyph_metrics.advance_width(glyph_id.0 as u16),
346 height: glyph_metrics.advance_height(glyph_id.0 as u16),
347 })
348 }
349
350 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
351 let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
352 if glyph_id == 0 {
353 None
354 } else {
355 Some(GlyphId(glyph_id.into()))
356 }
357 }
358
359 fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
360 let font = &self.loaded_fonts[params.font_id.0].font;
361 let is_emoji = self.loaded_fonts[params.font_id.0].is_known_emoji_font;
362 let font_weight = self.loaded_fonts[params.font_id.0].weight;
363 let effective_font_size = if is_emoji {
366 params.font_size * EMOJI_SIZE_SCALE
367 } else {
368 params.font_size
369 };
370 let subpixel_shift = point(
371 params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
372 params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
373 );
374 let image = self
375 .swash_cache
376 .get_image(
377 &mut self.font_system,
378 CacheKey::new(
379 font.id(),
380 params.glyph_id.0 as u16,
381 (effective_font_size * params.scale_factor).into(),
382 (subpixel_shift.x, subpixel_shift.y.trunc()),
383 font_weight,
384 cosmic_text::CacheKeyFlags::empty(),
385 )
386 .0,
387 )
388 .clone()
389 .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
390 Ok(Bounds {
391 origin: point(image.placement.left.into(), (-image.placement.top).into()),
392 size: size(image.placement.width.into(), image.placement.height.into()),
393 })
394 }
395
396 #[profiling::function]
397 fn rasterize_glyph(
398 &mut self,
399 params: &RenderGlyphParams,
400 glyph_bounds: Bounds<DevicePixels>,
401 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
402 if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
403 anyhow::bail!("glyph bounds are empty");
404 } else {
405 let bitmap_size = glyph_bounds.size;
406 let font = &self.loaded_fonts[params.font_id.0].font;
407 let is_emoji = self.loaded_fonts[params.font_id.0].is_known_emoji_font;
408 let font_weight = self.loaded_fonts[params.font_id.0].weight;
409 let effective_font_size = if is_emoji {
411 params.font_size * EMOJI_SIZE_SCALE
412 } else {
413 params.font_size
414 };
415 let subpixel_shift = point(
416 params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
417 params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
418 );
419 let mut image = self
420 .swash_cache
421 .get_image(
422 &mut self.font_system,
423 CacheKey::new(
424 font.id(),
425 params.glyph_id.0 as u16,
426 (effective_font_size * params.scale_factor).into(),
427 (subpixel_shift.x, subpixel_shift.y.trunc()),
428 font_weight,
429 cosmic_text::CacheKeyFlags::empty(),
430 )
431 .0,
432 )
433 .clone()
434 .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
435
436 if params.is_emoji {
437 for pixel in image.data.chunks_exact_mut(4) {
439 pixel.swap(0, 2);
440 }
441 }
442
443 Ok((bitmap_size, image.data))
444 }
445 }
446
447 fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
456 if let Some(ix) = self
457 .loaded_fonts
458 .iter()
459 .position(|loaded_font| loaded_font.font.id() == id)
460 {
461 FontId(ix)
462 } else {
463 let (face_weight, face_post_script_name) = {
464 let face = self.font_system.db().face(id).unwrap();
465 (face.weight, face.post_script_name.clone())
466 };
467 let font = self.font_system.get_font(id, face_weight).unwrap();
468
469 let font_id = FontId(self.loaded_fonts.len());
470 self.loaded_fonts.push(LoadedFont {
471 font: font.clone(),
472 features: CosmicFontFeatures::new(),
473 is_known_emoji_font: is_color_emoji_font(&face_post_script_name, &font),
474 weight: face_weight,
475 });
476
477 font_id
478 }
479 }
480
481 #[profiling::function]
482 fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
483 let mut attrs_list = AttrsList::new(&Attrs::new());
484 let mut offs = 0;
485 for run in font_runs {
486 let loaded_font = self.loaded_font(run.font_id);
487 let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
488
489 attrs_list.add_span(
490 offs..(offs + run.len),
491 &Attrs::new()
492 .metadata(run.font_id.0)
493 .family(Family::Name(&font.families.first().unwrap().0))
494 .stretch(font.stretch)
495 .style(font.style)
496 .weight(font.weight)
497 .font_features(loaded_font.features.clone()),
498 );
499 offs += run.len;
500 }
501
502 let line = ShapeLine::new(
503 &mut self.font_system,
504 text,
505 &attrs_list,
506 cosmic_text::Shaping::Advanced,
507 4,
508 );
509 let mut layout_lines = Vec::with_capacity(1);
510 line.layout_to_buffer(
511 &mut self.scratch,
512 font_size.0,
513 None, cosmic_text::Wrap::None,
515 cosmic_text::Ellipsize::None,
516 None,
517 &mut layout_lines,
518 None,
519 cosmic_text::Hinting::default(),
520 );
521 let layout = layout_lines.first().unwrap();
522
523 let mut runs: Vec<ShapedRun> = Vec::new();
524 for glyph in &layout.glyphs {
525 let mut font_id = FontId(glyph.metadata);
526 let mut loaded_font = self.loaded_font(font_id);
527 if loaded_font.font.id() != glyph.font_id {
528 font_id = self.font_id_for_cosmic_id(glyph.font_id);
529 loaded_font = self.loaded_font(font_id);
530 }
531 let is_emoji = loaded_font.is_known_emoji_font;
532
533 if glyph.glyph_id == 3 && is_emoji {
535 continue;
536 }
537
538 let shaped_glyph = ShapedGlyph {
539 id: GlyphId(glyph.glyph_id as u32),
540 position: point(glyph.x.into(), glyph.y.into()),
541 index: glyph.start,
542 is_emoji,
543 };
544
545 if let Some(last_run) = runs
546 .last_mut()
547 .filter(|last_run| last_run.font_id == font_id)
548 {
549 last_run.glyphs.push(shaped_glyph);
550 } else {
551 runs.push(ShapedRun {
552 font_id,
553 glyphs: vec![shaped_glyph],
554 });
555 }
556 }
557
558 LineLayout {
559 font_size,
560 width: layout.w.into(),
561 ascent: layout.max_ascent.into(),
562 descent: layout.max_descent.into(),
563 runs,
564 len: text.len(),
565 }
566 }
567
568 fn layout_line_with_features(
569 &mut self,
570 text: &str,
571 font_size: Pixels,
572 font_runs: &[FontRun],
573 features: &[FontFeature],
574 ) -> LineLayout {
575 let mut attrs_list = AttrsList::new(&Attrs::new());
576 let mut offs = 0;
577 for run in font_runs {
578 let loaded_font = self.loaded_font(run.font_id);
579 let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
580
581 let mut merged = loaded_font.features.clone();
585 for feature in features {
586 let tag = cosmic_text::FeatureTag::new(&feature.tag);
587 merged.set(tag, feature.value);
588 }
589
590 attrs_list.add_span(
591 offs..(offs + run.len),
592 &Attrs::new()
593 .metadata(run.font_id.0)
594 .family(Family::Name(&font.families.first().unwrap().0))
595 .stretch(font.stretch)
596 .style(font.style)
597 .weight(font.weight)
598 .font_features(merged),
599 );
600 offs += run.len;
601 }
602
603 let line = ShapeLine::new(
604 &mut self.font_system,
605 text,
606 &attrs_list,
607 cosmic_text::Shaping::Advanced,
608 4,
609 );
610 let mut layout_lines = Vec::with_capacity(1);
611 line.layout_to_buffer(
612 &mut self.scratch,
613 font_size.0,
614 None,
615 cosmic_text::Wrap::None,
616 cosmic_text::Ellipsize::None,
617 None,
618 &mut layout_lines,
619 None,
620 cosmic_text::Hinting::default(),
621 );
622 let layout = layout_lines.first().unwrap();
623
624 let mut runs: Vec<ShapedRun> = Vec::new();
625 for glyph in &layout.glyphs {
626 let mut font_id = FontId(glyph.metadata);
627 let mut loaded_font = self.loaded_font(font_id);
628 if loaded_font.font.id() != glyph.font_id {
629 font_id = self.font_id_for_cosmic_id(glyph.font_id);
630 loaded_font = self.loaded_font(font_id);
631 }
632 let is_emoji = loaded_font.is_known_emoji_font;
633
634 if glyph.glyph_id == 3 && is_emoji {
635 continue;
636 }
637
638 let shaped_glyph = ShapedGlyph {
639 id: GlyphId(glyph.glyph_id as u32),
640 position: point(glyph.x.into(), glyph.y.into()),
641 index: glyph.start,
642 is_emoji,
643 };
644
645 if let Some(last_run) = runs
646 .last_mut()
647 .filter(|last_run| last_run.font_id == font_id)
648 {
649 last_run.glyphs.push(shaped_glyph);
650 } else {
651 runs.push(ShapedRun {
652 font_id,
653 glyphs: vec![shaped_glyph],
654 });
655 }
656 }
657
658 LineLayout {
659 font_size,
660 width: layout.w.into(),
661 ascent: layout.max_ascent.into(),
662 descent: layout.max_descent.into(),
663 runs,
664 len: text.len(),
665 }
666 }
667}
668
669impl TryFrom<&FontFeatures> for CosmicFontFeatures {
670 type Error = anyhow::Error;
671
672 fn try_from(features: &FontFeatures) -> Result<Self> {
673 let mut result = CosmicFontFeatures::new();
674 for feature in features.0.iter() {
675 let name_bytes: [u8; 4] = feature
676 .0
677 .as_bytes()
678 .try_into()
679 .context("Incorrect feature flag format")?;
680
681 let tag = cosmic_text::FeatureTag::new(&name_bytes);
682
683 result.set(tag, feature.1);
684 }
685 Ok(result)
686 }
687}
688
689impl From<RectF> for Bounds<f32> {
690 fn from(rect: RectF) -> Self {
691 Bounds {
692 origin: point(rect.origin_x(), rect.origin_y()),
693 size: size(rect.width(), rect.height()),
694 }
695 }
696}
697
698impl From<RectI> for Bounds<DevicePixels> {
699 fn from(rect: RectI) -> Self {
700 Bounds {
701 origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
702 size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
703 }
704 }
705}
706
707impl From<Vector2I> for Size<DevicePixels> {
708 fn from(value: Vector2I) -> Self {
709 size(value.x().into(), value.y().into())
710 }
711}
712
713impl From<RectI> for Bounds<i32> {
714 fn from(rect: RectI) -> Self {
715 Bounds {
716 origin: point(rect.origin_x(), rect.origin_y()),
717 size: size(rect.width(), rect.height()),
718 }
719 }
720}
721
722impl From<Point<u32>> for Vector2I {
723 fn from(size: Point<u32>) -> Self {
724 Vector2I::new(size.x as i32, size.y as i32)
725 }
726}
727
728impl From<Vector2F> for Size<f32> {
729 fn from(vec: Vector2F) -> Self {
730 size(vec.x(), vec.y())
731 }
732}
733
734impl From<FontWeight> for cosmic_text::Weight {
735 fn from(value: FontWeight) -> Self {
736 cosmic_text::Weight(value.0 as u16)
737 }
738}
739
740impl From<FontStyle> for cosmic_text::Style {
741 fn from(style: FontStyle) -> Self {
742 match style {
743 FontStyle::Normal => cosmic_text::Style::Normal,
744 FontStyle::Italic => cosmic_text::Style::Italic,
745 FontStyle::Oblique => cosmic_text::Style::Oblique,
746 }
747 }
748}
749
750fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
751 font_kit::properties::Properties {
752 style: match font.style {
753 crate::FontStyle::Normal => font_kit::properties::Style::Normal,
754 crate::FontStyle::Italic => font_kit::properties::Style::Italic,
755 crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
756 },
757 weight: font_kit::properties::Weight(font.weight.0),
758 stretch: Default::default(),
759 }
760}
761
762fn face_info_into_properties(
763 face_info: &cosmic_text::fontdb::FaceInfo,
764) -> font_kit::properties::Properties {
765 font_kit::properties::Properties {
766 style: match face_info.style {
767 cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
768 cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
769 cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
770 },
771 weight: font_kit::properties::Weight(face_info.weight.0.into()),
773 stretch: match face_info.stretch {
774 cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
775 cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
776 cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
777 cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
778 cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
779 cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
780 cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
781 cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
782 cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
783 },
784 }
785}
786
787fn check_is_known_emoji_font(postscript_name: &str) -> bool {
796 matches!(
797 postscript_name,
798 "NotoColorEmoji"
799 | "Noto-COLRv1"
800 | "NotoColorEmoji-Regular"
801 | "Twemoji"
802 | "TwemojiMozilla"
803 | "TwitterColorEmoji"
804 | "EmojiOneMozilla"
805 | "EmojiOneColor"
806 | "JoyPixels"
807 | "AppleColorEmoji"
808 | "SegoeUIEmoji"
809 | "OpenMoji"
810 | "OpenMojiColor"
811 )
812}
813
814fn has_color_font_tables(font: &cosmic_text::Font) -> bool {
821 let swash = font.as_swash();
822 let color_table_tags: &[[u8; 4]] = &[
825 *b"CBDT", *b"CBLC", *b"COLR", *b"CPAL", *b"sbix", *b"SVG ", ];
832
833 for tag in color_table_tags {
834 let tag_value = u32::from_be_bytes(*tag);
835 if swash.table(tag_value).is_some() {
836 return true;
837 }
838 }
839 false
840}
841
842fn is_color_emoji_font(postscript_name: &str, font: &cosmic_text::Font) -> bool {
846 check_is_known_emoji_font(postscript_name) || has_color_font_tables(font)
847}