1use crate::{Attrs, Font, FontMatchAttrs, HashMap, ShapeBuffer};
2use alloc::boxed::Box;
3use alloc::collections::BTreeSet;
4use alloc::string::String;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7use core::fmt;
8use core::ops::{Deref, DerefMut};
9use fontdb::{FaceInfo, Query, Style};
10use skrifa::raw::{ReadError, TableProvider as _};
11use skrifa::MetadataProvider;
12
13pub use fontdb;
15pub use harfrust;
16
17use super::fallback::{Fallback, Fallbacks, MonospaceFallbackInfo, PlatformFallback};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub struct FontMatchKey {
22 pub(crate) not_emoji: bool,
23 pub(crate) font_weight_diff: u16,
24 pub(crate) font_stretch_diff: u16,
25 pub(crate) font_style_diff: u8,
26 pub(crate) font_weight: u16,
27 pub(crate) font_stretch: u16,
28 pub(crate) id: fontdb::ID,
29 pub(crate) variable_weight_match: bool,
30}
31
32impl FontMatchKey {
33 fn new(attrs: &Attrs, face: &FaceInfo, db: &fontdb::Database) -> FontMatchKey {
34 let not_emoji = !face.post_script_name.contains("Emoji");
36 let font_weight_diff = attrs.weight.0.abs_diff(face.weight.0);
37
38 let variable_weight_match = font_weight_diff != 0
39 && db.with_face_data(face.id, |font_data, face_index| {
40 let font_ref = skrifa::FontRef::from_index(font_data, face_index).ok()?;
41 let axis = font_ref.axes().get_by_tag(skrifa::Tag::new(b"wght"))?;
42 let w = attrs.weight.0 as f32;
43 Some(w >= axis.min_value() && w <= axis.max_value())
44 }) == Some(Some(true));
45 let font_weight = face.weight.0;
46 let font_stretch_diff = attrs.stretch.to_number().abs_diff(face.stretch.to_number());
47 let font_stretch = face.stretch.to_number();
48 let font_style_diff = match (attrs.style, face.style) {
49 (Style::Normal, Style::Normal)
50 | (Style::Italic, Style::Italic)
51 | (Style::Oblique, Style::Oblique) => 0,
52 (Style::Italic, Style::Oblique) | (Style::Oblique, Style::Italic) => 1,
53 (Style::Normal, Style::Italic)
54 | (Style::Normal, Style::Oblique)
55 | (Style::Italic, Style::Normal)
56 | (Style::Oblique, Style::Normal) => 2,
57 };
58 let id = face.id;
59 FontMatchKey {
60 not_emoji,
61 font_weight_diff,
62 font_stretch_diff,
63 font_style_diff,
64 font_weight,
65 font_stretch,
66 id,
67 variable_weight_match,
68 }
69 }
70}
71
72struct FontCachedCodepointSupportInfo {
73 supported: Vec<u32>,
74 not_supported: Vec<u32>,
75}
76
77impl FontCachedCodepointSupportInfo {
78 const SUPPORTED_MAX_SZ: usize = 512;
79 const NOT_SUPPORTED_MAX_SZ: usize = 1024;
80
81 fn new() -> Self {
82 Self {
83 supported: Vec::with_capacity(Self::SUPPORTED_MAX_SZ),
84 not_supported: Vec::with_capacity(Self::NOT_SUPPORTED_MAX_SZ),
85 }
86 }
87
88 #[inline(always)]
89 fn unknown_has_codepoint(
90 &mut self,
91 font_codepoints: &[u32],
92 codepoint: u32,
93 supported_insert_pos: usize,
94 not_supported_insert_pos: usize,
95 ) -> bool {
96 let ret = font_codepoints.contains(&codepoint);
97 if ret {
98 if supported_insert_pos != Self::SUPPORTED_MAX_SZ {
100 self.supported.insert(supported_insert_pos, codepoint);
101 self.supported.truncate(Self::SUPPORTED_MAX_SZ);
102 }
103 } else {
104 if not_supported_insert_pos != Self::NOT_SUPPORTED_MAX_SZ {
106 self.not_supported
107 .insert(not_supported_insert_pos, codepoint);
108 self.not_supported.truncate(Self::NOT_SUPPORTED_MAX_SZ);
109 }
110 }
111 ret
112 }
113
114 #[inline(always)]
115 fn has_codepoint(&mut self, font_codepoints: &[u32], codepoint: u32) -> bool {
116 match self.supported.binary_search(&codepoint) {
117 Ok(_) => true,
118 Err(supported_insert_pos) => match self.not_supported.binary_search(&codepoint) {
119 Ok(_) => false,
120 Err(not_supported_insert_pos) => self.unknown_has_codepoint(
121 font_codepoints,
122 codepoint,
123 supported_insert_pos,
124 not_supported_insert_pos,
125 ),
126 },
127 }
128 }
129}
130
131pub struct FontSystem {
133 locale: String,
135
136 db: fontdb::Database,
138
139 font_cache: HashMap<(fontdb::ID, fontdb::Weight), Option<Arc<Font>>>,
141
142 monospace_font_ids: Vec<fontdb::ID>,
144
145 per_script_monospace_font_ids: HashMap<[u8; 4], Vec<fontdb::ID>>,
149
150 font_codepoint_support_info_cache: HashMap<fontdb::ID, FontCachedCodepointSupportInfo>,
152
153 font_matches_cache: HashMap<FontMatchAttrs, Arc<Vec<FontMatchKey>>>,
155
156 pub(crate) shape_buffer: ShapeBuffer,
158
159 pub(crate) monospace_fallbacks_buffer: BTreeSet<MonospaceFallbackInfo>,
161
162 #[cfg(feature = "shape-run-cache")]
164 pub shape_run_cache: crate::ShapeRunCache,
165
166 pub(crate) dyn_fallback: Box<dyn Fallback>,
168
169 pub(crate) fallbacks: Fallbacks,
171}
172
173impl fmt::Debug for FontSystem {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.debug_struct("FontSystem")
176 .field("locale", &self.locale)
177 .field("db", &self.db)
178 .finish_non_exhaustive()
179 }
180}
181
182impl FontSystem {
183 const FONT_MATCHES_CACHE_SIZE_LIMIT: usize = 256;
184 pub fn new() -> Self {
192 Self::new_with_fonts(core::iter::empty())
193 }
194
195 pub fn new_with_fonts(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
197 let mut db = fontdb::Database::new();
198 Self::load_fonts(&mut db, fonts.into_iter());
199 Self::finish_with_db(db)
200 }
201
202 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
208 pub fn new_cached() -> Self {
209 Self::new_with_fonts_and_cache(core::iter::empty())
210 }
211
212 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
219 pub fn default_cache_path() -> Option<std::path::PathBuf> {
220 super::cache::default_cache_path()
221 }
222
223 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
235 pub fn new_with_fonts_and_cache(fonts: impl IntoIterator<Item = fontdb::Source>) -> Self {
236 Self::new_with_fonts_and_cache_inner(fonts, Self::default_cache_path())
237 }
238
239 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
243 pub fn new_with_fonts_and_cache_path(
244 fonts: impl IntoIterator<Item = fontdb::Source>,
245 cache_path: std::path::PathBuf,
246 ) -> Self {
247 Self::new_with_fonts_and_cache_inner(fonts, Some(cache_path))
248 }
249
250 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
251 fn new_with_fonts_and_cache_inner(
252 fonts: impl IntoIterator<Item = fontdb::Source>,
253 cache_path: Option<std::path::PathBuf>,
254 ) -> Self {
255 let mut db = fontdb::Database::new();
256
257 let now = std::time::Instant::now();
258
259 match cache_path {
260 Some(cache_path) => super::cache::load_system_fonts_cached(&mut db, &cache_path),
261 None => db.load_system_fonts(),
262 }
263 for source in fonts {
264 db.load_font_source(source);
265 }
266
267 log::debug!(
268 "Loaded {} font faces in {}ms.",
269 db.len(),
270 now.elapsed().as_millis()
271 );
272
273 Self::finish_with_db(db)
274 }
275
276 fn finish_with_db(mut db: fontdb::Database) -> Self {
279 let locale = Self::get_locale();
280 log::debug!("Locale: {locale}");
281
282 db.set_monospace_family("Noto Sans Mono");
284 db.set_sans_serif_family("Open Sans");
285 db.set_serif_family("DejaVu Serif");
286
287 Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
288 }
289
290 pub fn new_with_locale_and_db_and_fallback(
292 locale: String,
293 db: fontdb::Database,
294 impl_fallback: impl Fallback + 'static,
295 ) -> Self {
296 let mut monospace_font_ids = db
297 .faces()
298 .filter(|face_info| {
299 face_info.monospaced && !face_info.post_script_name.contains("Emoji")
300 })
301 .map(|face_info| face_info.id)
302 .collect::<Vec<_>>();
303 monospace_font_ids.sort();
304
305 let mut per_script_monospace_font_ids: HashMap<[u8; 4], BTreeSet<fontdb::ID>> =
306 HashMap::default();
307
308 if cfg!(feature = "monospace_fallback") {
309 for &id in &monospace_font_ids {
310 db.with_face_data(id, |font_data, face_index| {
311 let face = skrifa::FontRef::from_index(font_data, face_index)?;
312 for script in face
313 .gpos()?
314 .script_list()?
315 .script_records()
316 .iter()
317 .chain(face.gsub()?.script_list()?.script_records().iter())
318 {
319 per_script_monospace_font_ids
320 .entry(script.script_tag().into_bytes())
321 .or_default()
322 .insert(id);
323 }
324 Ok::<_, ReadError>(())
325 });
326 }
327 }
328
329 let per_script_monospace_font_ids = per_script_monospace_font_ids
330 .into_iter()
331 .map(|(k, v)| (k, Vec::from_iter(v)))
332 .collect();
333
334 let fallbacks = Fallbacks::new(&impl_fallback, &[], &locale);
335
336 Self {
337 locale,
338 db,
339 monospace_font_ids,
340 per_script_monospace_font_ids,
341 font_cache: HashMap::default(),
342 font_matches_cache: HashMap::default(),
343 font_codepoint_support_info_cache: HashMap::default(),
344 monospace_fallbacks_buffer: BTreeSet::default(),
345 #[cfg(feature = "shape-run-cache")]
346 shape_run_cache: crate::ShapeRunCache::default(),
347 shape_buffer: ShapeBuffer::default(),
348 dyn_fallback: Box::new(impl_fallback),
349 fallbacks,
350 }
351 }
352
353 pub fn new_with_locale_and_db(locale: String, db: fontdb::Database) -> Self {
355 Self::new_with_locale_and_db_and_fallback(locale, db, PlatformFallback)
356 }
357
358 pub fn locale(&self) -> &str {
360 &self.locale
361 }
362
363 pub const fn db(&self) -> &fontdb::Database {
365 &self.db
366 }
367
368 pub fn db_mut(&mut self) -> &mut fontdb::Database {
370 self.font_matches_cache.clear();
371 &mut self.db
372 }
373
374 pub fn into_locale_and_db(self) -> (String, fontdb::Database) {
376 (self.locale, self.db)
377 }
378
379 pub fn get_font(&mut self, id: fontdb::ID, weight: fontdb::Weight) -> Option<Arc<Font>> {
381 self.font_cache
382 .entry((id, weight))
383 .or_insert_with(|| {
384 #[cfg(feature = "std")]
385 unsafe {
386 self.db.make_shared_face_data(id);
387 }
388 if let Some(font) = Font::new(&self.db, id, weight) {
389 Some(Arc::new(font))
390 } else {
391 log::warn!(
392 "failed to load font '{}'",
393 self.db.face(id)?.post_script_name
394 );
395 None
396 }
397 })
398 .clone()
399 }
400
401 pub fn is_monospace(&self, id: fontdb::ID) -> bool {
402 self.monospace_font_ids.binary_search(&id).is_ok()
403 }
404
405 pub fn get_monospace_ids_for_scripts(
406 &self,
407 scripts: impl Iterator<Item = [u8; 4]>,
408 ) -> Vec<fontdb::ID> {
409 let mut ret = scripts
410 .filter_map(|script| self.per_script_monospace_font_ids.get(&script))
411 .flat_map(|ids| ids.iter().copied())
412 .collect::<Vec<_>>();
413 ret.sort();
414 ret.dedup();
415 ret
416 }
417
418 #[inline(always)]
419 pub fn get_font_supported_codepoints_in_word(
420 &mut self,
421 id: fontdb::ID,
422 weight: fontdb::Weight,
423 word: &str,
424 ) -> Option<usize> {
425 self.get_font(id, weight).map(|font| {
426 let code_points = font.unicode_codepoints();
427 let cache = self
428 .font_codepoint_support_info_cache
429 .entry(id)
430 .or_insert_with(FontCachedCodepointSupportInfo::new);
431 word.chars()
432 .filter(|ch| cache.has_codepoint(code_points, u32::from(*ch)))
433 .count()
434 })
435 }
436
437 pub fn get_font_matches(&mut self, attrs: &Attrs<'_>) -> Arc<Vec<FontMatchKey>> {
438 if self.font_matches_cache.len() >= Self::FONT_MATCHES_CACHE_SIZE_LIMIT {
440 log::trace!("clear font mache cache");
441 self.font_matches_cache.clear();
442 }
443
444 self.font_matches_cache
445 .entry(attrs.into())
447 .or_insert_with(|| {
448 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
449 let now = std::time::Instant::now();
450
451 let mut font_match_keys = self
452 .db
453 .faces()
454 .map(|face| FontMatchKey::new(attrs, face, &self.db))
455 .collect::<Vec<_>>();
456
457 font_match_keys.sort();
459
460 let query = Query {
462 families: &[attrs.family],
463 weight: attrs.weight,
464 stretch: attrs.stretch,
465 style: attrs.style,
466 };
467
468 if let Some(id) = self.db.query(&query) {
469 if let Some(i) = font_match_keys
470 .iter()
471 .enumerate()
472 .find(|(_i, key)| key.id == id)
473 .map(|(i, _)| i)
474 {
475 let match_key = font_match_keys.remove(i);
477 font_match_keys.insert(0, match_key);
478 } else if let Some(face) = self.db.face(id) {
479 let match_key = FontMatchKey::new(attrs, face, &self.db);
481 font_match_keys.insert(0, match_key);
482 } else {
483 log::error!("Could not get face from db, that should've been there.");
484 }
485 }
486
487 #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
488 {
489 let elapsed = now.elapsed();
490 log::debug!("font matches for {attrs:?} in {elapsed:?}");
491 }
492
493 Arc::new(font_match_keys)
494 })
495 .clone()
496 }
497
498 #[cfg(feature = "std")]
499 fn get_locale() -> String {
500 sys_locale::get_locale().unwrap_or_else(|| {
501 log::warn!("failed to get system locale, falling back to en-US");
502 String::from("en-US")
503 })
504 }
505
506 #[cfg(not(feature = "std"))]
507 fn get_locale() -> String {
508 String::from("en-US")
509 }
510
511 #[cfg(feature = "std")]
512 fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
513 #[cfg(not(target_arch = "wasm32"))]
514 let now = std::time::Instant::now();
515
516 db.load_system_fonts();
517
518 for source in fonts {
519 db.load_font_source(source);
520 }
521
522 #[cfg(not(target_arch = "wasm32"))]
523 log::debug!(
524 "Parsed {} font faces in {}ms.",
525 db.len(),
526 now.elapsed().as_millis()
527 );
528 }
529
530 #[cfg(not(feature = "std"))]
531 fn load_fonts(db: &mut fontdb::Database, fonts: impl Iterator<Item = fontdb::Source>) {
532 for source in fonts {
533 db.load_font_source(source);
534 }
535 }
536}
537
538#[derive(Debug)]
540pub struct BorrowedWithFontSystem<'a, T> {
541 pub(crate) inner: &'a mut T,
542 pub(crate) font_system: &'a mut FontSystem,
543}
544
545impl<T> Deref for BorrowedWithFontSystem<'_, T> {
546 type Target = T;
547
548 fn deref(&self) -> &Self::Target {
549 self.inner
550 }
551}
552
553impl<T> DerefMut for BorrowedWithFontSystem<'_, T> {
554 fn deref_mut(&mut self) -> &mut Self::Target {
555 self.inner
556 }
557}