1#![allow(non_snake_case)]
68
69extern crate alloc;
70
71use alloc::collections::btree_map::BTreeMap;
72use alloc::string::{String, ToString};
73use alloc::vec::Vec;
74#[cfg(all(feature = "std", feature = "parsing"))]
75use allsorts::binary::read::ReadScope;
76#[cfg(all(feature = "std", feature = "parsing"))]
77use allsorts::get_name::fontcode_get_name;
78#[cfg(all(feature = "std", feature = "parsing"))]
79use allsorts::tables::os2::Os2;
80#[cfg(all(feature = "std", feature = "parsing"))]
81use allsorts::tables::{FontTableProvider, HheaTable, HmtxTable, MaxpTable};
82#[cfg(all(feature = "std", feature = "parsing"))]
83use allsorts::tag;
84#[cfg(feature = "std")]
85use std::path::PathBuf;
86
87#[cfg(feature = "std")]
88pub mod config;
89pub mod fallback;
90pub mod utils;
91#[cfg(feature = "std")]
92pub use config::{FcFallbackConfig, FcScriptFallback, GenericFamily};
93#[cfg(feature = "std")]
94use fallback::FontChainCacheKey;
95#[cfg(feature = "std")]
96pub use fallback::{CssFallbackGroup, FontFallbackChain, ScriptFallbackGroup};
97
98#[cfg(feature = "ffi")]
99pub mod ffi;
100
101#[cfg(feature = "cache")]
102pub mod disk_cache;
103#[cfg(feature = "async-registry")]
104pub mod multithread;
105#[cfg(feature = "async-registry")]
106pub mod registry;
107#[cfg(feature = "async-registry")]
108pub mod scoring;
109
110#[cfg(all(target_os = "ios", feature = "std", feature = "parsing"))]
111mod mobile_ios;
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub enum OperatingSystem {
116 Windows,
117 Linux,
118 MacOS,
119 IOS,
120 Android,
121 Wasm,
122}
123
124impl OperatingSystem {
125 pub fn current() -> Self {
127 #[cfg(target_os = "windows")]
128 return OperatingSystem::Windows;
129
130 #[cfg(target_os = "linux")]
131 return OperatingSystem::Linux;
132
133 #[cfg(target_os = "macos")]
134 return OperatingSystem::MacOS;
135
136 #[cfg(target_os = "ios")]
137 return OperatingSystem::IOS;
138
139 #[cfg(target_os = "android")]
140 return OperatingSystem::Android;
141
142 #[cfg(target_family = "wasm")]
143 return OperatingSystem::Wasm;
144
145 #[cfg(not(any(
146 target_os = "windows",
147 target_os = "linux",
148 target_os = "macos",
149 target_os = "ios",
150 target_os = "android",
151 target_family = "wasm"
152 )))]
153 return OperatingSystem::Linux; }
155
156 #[cfg(feature = "std")]
158 #[deprecated(
159 since = "5.0.0",
160 note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::Serif, ranges)`"
161 )]
162 pub fn get_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
163 FcFallbackConfig::os_defaults(*self).expand_generic(GenericFamily::Serif, unicode_ranges)
164 }
165
166 #[cfg(feature = "std")]
168 #[deprecated(
169 since = "5.0.0",
170 note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::SansSerif, ranges)`"
171 )]
172 pub fn get_sans_serif_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
173 FcFallbackConfig::os_defaults(*self)
174 .expand_generic(GenericFamily::SansSerif, unicode_ranges)
175 }
176
177 #[cfg(feature = "std")]
179 #[deprecated(
180 since = "5.0.0",
181 note = "use `FcFallbackConfig::os_defaults(os).expand_generic(GenericFamily::Monospace, ranges)`"
182 )]
183 pub fn get_monospace_fonts(&self, unicode_ranges: &[UnicodeRange]) -> Vec<String> {
184 FcFallbackConfig::os_defaults(*self)
185 .expand_generic(GenericFamily::Monospace, unicode_ranges)
186 }
187
188 #[cfg(feature = "std")]
190 #[deprecated(
191 since = "5.0.0",
192 note = "use `FcFallbackConfig::os_defaults(os).expand_family(family, ranges)`"
193 )]
194 pub fn expand_generic_family(
195 &self,
196 family: &str,
197 unicode_ranges: &[UnicodeRange],
198 ) -> Vec<String> {
199 FcFallbackConfig::os_defaults(*self).expand_family(family, unicode_ranges)
200 }
201}
202
203#[cfg(feature = "std")]
205#[deprecated(
206 since = "5.0.0",
207 note = "use `FcFallbackConfig::os_defaults(os).candidate_families(families, ranges)`"
208)]
209pub fn expand_font_families(
210 families: &[String],
211 os: OperatingSystem,
212 unicode_ranges: &[UnicodeRange],
213) -> Vec<String> {
214 FcFallbackConfig::os_defaults(os).candidate_families(families, unicode_ranges)
215}
216
217#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
219#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
220pub struct FontId(pub u128);
221
222impl core::fmt::Debug for FontId {
223 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224 core::fmt::Display::fmt(self, f)
225 }
226}
227
228impl core::fmt::Display for FontId {
229 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230 let id = self.0;
231 write!(
232 f,
233 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
234 (id >> 96) & 0xFFFFFFFF,
235 (id >> 80) & 0xFFFF,
236 (id >> 64) & 0xFFFF,
237 (id >> 48) & 0xFFFF,
238 id & 0xFFFFFFFFFFFF
239 )
240 }
241}
242
243impl FontId {
244 pub fn new() -> Self {
246 use core::sync::atomic::{AtomicU64, Ordering};
247 static COUNTER: AtomicU64 = AtomicU64::new(1);
248 let id = COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
249 FontId(id)
250 }
251}
252
253#[derive(Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
255#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
256#[repr(C)]
257pub enum PatternMatch {
258 #[default]
260 DontCare = 2,
261 True = 0,
263 False = 1,
265}
266
267impl PatternMatch {
268 fn needs_to_match(&self) -> bool {
269 matches!(self, PatternMatch::True | PatternMatch::False)
270 }
271
272 fn matches(&self, other: &PatternMatch) -> bool {
273 match (self, other) {
274 (PatternMatch::DontCare, _) => true,
275 (_, PatternMatch::DontCare) => true,
276 (a, b) => a == b,
277 }
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
283#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
284#[repr(C)]
285pub enum FcWeight {
286 Thin = 100,
287 ExtraLight = 200,
288 Light = 300,
289 Normal = 400,
290 Medium = 500,
291 SemiBold = 600,
292 Bold = 700,
293 ExtraBold = 800,
294 Black = 900,
295}
296
297impl FcWeight {
298 pub fn from_u16(weight: u16) -> Self {
299 match weight {
300 0..=149 => FcWeight::Thin,
301 150..=249 => FcWeight::ExtraLight,
302 250..=349 => FcWeight::Light,
303 350..=449 => FcWeight::Normal,
304 450..=549 => FcWeight::Medium,
305 550..=649 => FcWeight::SemiBold,
306 650..=749 => FcWeight::Bold,
307 750..=849 => FcWeight::ExtraBold,
308 _ => FcWeight::Black,
309 }
310 }
311
312 pub fn find_best_match(&self, available: &[FcWeight]) -> Option<FcWeight> {
313 if available.is_empty() {
314 return None;
315 }
316
317 if available.contains(self) {
319 return Some(*self);
320 }
321
322 let self_value = *self as u16;
324
325 match *self {
326 FcWeight::Normal => {
327 if available.contains(&FcWeight::Medium) {
329 return Some(FcWeight::Medium);
330 }
331 for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
333 if available.contains(weight) {
334 return Some(*weight);
335 }
336 }
337 for weight in &[
339 FcWeight::SemiBold,
340 FcWeight::Bold,
341 FcWeight::ExtraBold,
342 FcWeight::Black,
343 ] {
344 if available.contains(weight) {
345 return Some(*weight);
346 }
347 }
348 }
349 FcWeight::Medium => {
350 if available.contains(&FcWeight::Normal) {
352 return Some(FcWeight::Normal);
353 }
354 for weight in &[FcWeight::Light, FcWeight::ExtraLight, FcWeight::Thin] {
356 if available.contains(weight) {
357 return Some(*weight);
358 }
359 }
360 for weight in &[
362 FcWeight::SemiBold,
363 FcWeight::Bold,
364 FcWeight::ExtraBold,
365 FcWeight::Black,
366 ] {
367 if available.contains(weight) {
368 return Some(*weight);
369 }
370 }
371 }
372 FcWeight::Thin | FcWeight::ExtraLight | FcWeight::Light => {
373 let mut best_match = None;
375 let mut smallest_diff = u16::MAX;
376
377 for weight in available {
379 let weight_value = *weight as u16;
380 if weight_value <= self_value {
382 let diff = self_value - weight_value;
383 if diff < smallest_diff {
384 smallest_diff = diff;
385 best_match = Some(*weight);
386 }
387 }
388 }
389
390 if best_match.is_some() {
391 return best_match;
392 }
393
394 best_match = None;
396 smallest_diff = u16::MAX;
397
398 for weight in available {
399 let weight_value = *weight as u16;
400 if weight_value > self_value {
401 let diff = weight_value - self_value;
402 if diff < smallest_diff {
403 smallest_diff = diff;
404 best_match = Some(*weight);
405 }
406 }
407 }
408
409 return best_match;
410 }
411 FcWeight::SemiBold | FcWeight::Bold | FcWeight::ExtraBold | FcWeight::Black => {
412 let mut best_match = None;
414 let mut smallest_diff = u16::MAX;
415
416 for weight in available {
418 let weight_value = *weight as u16;
419 if weight_value >= self_value {
421 let diff = weight_value - self_value;
422 if diff < smallest_diff {
423 smallest_diff = diff;
424 best_match = Some(*weight);
425 }
426 }
427 }
428
429 if best_match.is_some() {
430 return best_match;
431 }
432
433 best_match = None;
435 smallest_diff = u16::MAX;
436
437 for weight in available {
438 let weight_value = *weight as u16;
439 if weight_value < self_value {
440 let diff = self_value - weight_value;
441 if diff < smallest_diff {
442 smallest_diff = diff;
443 best_match = Some(*weight);
444 }
445 }
446 }
447
448 return best_match;
449 }
450 }
451
452 Some(available[0])
454 }
455}
456
457impl Default for FcWeight {
458 fn default() -> Self {
459 FcWeight::Normal
460 }
461}
462
463#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
465#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
466#[repr(C)]
467pub enum FcStretch {
468 UltraCondensed = 1,
469 ExtraCondensed = 2,
470 Condensed = 3,
471 SemiCondensed = 4,
472 Normal = 5,
473 SemiExpanded = 6,
474 Expanded = 7,
475 ExtraExpanded = 8,
476 UltraExpanded = 9,
477}
478
479impl FcStretch {
480 pub fn is_condensed(&self) -> bool {
481 use self::FcStretch::*;
482 match self {
483 UltraCondensed => true,
484 ExtraCondensed => true,
485 Condensed => true,
486 SemiCondensed => true,
487 Normal => false,
488 SemiExpanded => false,
489 Expanded => false,
490 ExtraExpanded => false,
491 UltraExpanded => false,
492 }
493 }
494 pub fn from_u16(width_class: u16) -> Self {
495 match width_class {
496 1 => FcStretch::UltraCondensed,
497 2 => FcStretch::ExtraCondensed,
498 3 => FcStretch::Condensed,
499 4 => FcStretch::SemiCondensed,
500 5 => FcStretch::Normal,
501 6 => FcStretch::SemiExpanded,
502 7 => FcStretch::Expanded,
503 8 => FcStretch::ExtraExpanded,
504 9 => FcStretch::UltraExpanded,
505 _ => FcStretch::Normal,
506 }
507 }
508
509 pub fn find_best_match(&self, available: &[FcStretch]) -> Option<FcStretch> {
511 if available.is_empty() {
512 return None;
513 }
514
515 if available.contains(self) {
516 return Some(*self);
517 }
518
519 if *self <= FcStretch::Normal {
521 let mut closest_narrower = None;
523 for stretch in available.iter() {
524 if *stretch < *self
525 && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
526 {
527 closest_narrower = Some(*stretch);
528 }
529 }
530
531 if closest_narrower.is_some() {
532 return closest_narrower;
533 }
534
535 let mut closest_wider = None;
537 for stretch in available.iter() {
538 if *stretch > *self
539 && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
540 {
541 closest_wider = Some(*stretch);
542 }
543 }
544
545 return closest_wider;
546 } else {
547 let mut closest_wider = None;
549 for stretch in available.iter() {
550 if *stretch > *self
551 && (closest_wider.is_none() || *stretch < closest_wider.unwrap())
552 {
553 closest_wider = Some(*stretch);
554 }
555 }
556
557 if closest_wider.is_some() {
558 return closest_wider;
559 }
560
561 let mut closest_narrower = None;
563 for stretch in available.iter() {
564 if *stretch < *self
565 && (closest_narrower.is_none() || *stretch > closest_narrower.unwrap())
566 {
567 closest_narrower = Some(*stretch);
568 }
569 }
570
571 return closest_narrower;
572 }
573 }
574}
575
576impl Default for FcStretch {
577 fn default() -> Self {
578 FcStretch::Normal
579 }
580}
581
582#[repr(C)]
584#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
585#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
586pub struct UnicodeRange {
587 pub start: u32,
588 pub end: u32,
589}
590
591pub const DEFAULT_UNICODE_FALLBACK_SCRIPTS: &[UnicodeRange] = &[
593 UnicodeRange {
594 start: 0x0400,
595 end: 0x04FF,
596 }, UnicodeRange {
598 start: 0x0600,
599 end: 0x06FF,
600 }, UnicodeRange {
602 start: 0x0900,
603 end: 0x097F,
604 }, UnicodeRange {
606 start: 0x3040,
607 end: 0x309F,
608 }, UnicodeRange {
610 start: 0x30A0,
611 end: 0x30FF,
612 }, UnicodeRange {
614 start: 0x4E00,
615 end: 0x9FFF,
616 }, UnicodeRange {
618 start: 0xAC00,
619 end: 0xD7A3,
620 }, ];
622
623impl UnicodeRange {
624 pub fn contains(&self, c: char) -> bool {
625 let c = c as u32;
626 c >= self.start && c <= self.end
627 }
628
629 pub fn overlaps(&self, other: &UnicodeRange) -> bool {
630 self.start <= other.end && other.start <= self.end
631 }
632
633 pub fn is_subset_of(&self, other: &UnicodeRange) -> bool {
634 self.start >= other.start && self.end <= other.end
635 }
636}
637
638pub fn has_cjk_ranges(ranges: &[UnicodeRange]) -> bool {
640 const BLOCKS: [UnicodeRange; 4] = [
641 UnicodeRange {
642 start: 0x3040,
643 end: 0x309F,
644 }, UnicodeRange {
646 start: 0x30A0,
647 end: 0x30FF,
648 }, UnicodeRange {
650 start: 0x4E00,
651 end: 0x9FFF,
652 }, UnicodeRange {
654 start: 0xAC00,
655 end: 0xD7AF,
656 }, ];
658 ranges.iter().any(|r| BLOCKS.iter().any(|b| r.overlaps(b)))
659}
660
661pub fn has_arabic_ranges(ranges: &[UnicodeRange]) -> bool {
663 ranges.iter().any(|r| {
664 r.overlaps(&UnicodeRange {
665 start: 0x0600,
666 end: 0x06FF,
667 })
668 })
669}
670
671pub fn has_cyrillic_ranges(ranges: &[UnicodeRange]) -> bool {
673 ranges.iter().any(|r| {
674 r.overlaps(&UnicodeRange {
675 start: 0x0400,
676 end: 0x04FF,
677 })
678 })
679}
680
681pub fn has_hebrew_ranges(ranges: &[UnicodeRange]) -> bool {
683 ranges.iter().any(|r| {
684 r.overlaps(&UnicodeRange {
685 start: 0x0590,
686 end: 0x05FF,
687 })
688 })
689}
690
691pub fn has_thai_ranges(ranges: &[UnicodeRange]) -> bool {
693 ranges.iter().any(|r| {
694 r.overlaps(&UnicodeRange {
695 start: 0x0E00,
696 end: 0x0E7F,
697 })
698 })
699}
700
701#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
703pub enum TraceLevel {
704 Debug,
705 Info,
706 Warning,
707 Error,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq, Hash)]
712pub enum MatchReason {
713 NameMismatch {
714 requested: Option<String>,
715 found: Option<String>,
716 },
717 FamilyMismatch {
718 requested: Option<String>,
719 found: Option<String>,
720 },
721 StyleMismatch {
722 property: &'static str,
723 requested: String,
724 found: String,
725 },
726 WeightMismatch {
727 requested: FcWeight,
728 found: FcWeight,
729 },
730 StretchMismatch {
731 requested: FcStretch,
732 found: FcStretch,
733 },
734 UnicodeRangeMismatch {
735 character: char,
736 ranges: Vec<UnicodeRange>,
737 },
738 Success,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
743pub struct TraceMsg {
744 pub level: TraceLevel,
745 pub path: String,
746 pub reason: MatchReason,
747}
748
749#[repr(C)]
751#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
752#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
753pub enum FcHintStyle {
754 #[default]
755 None = 0,
756 Slight = 1,
757 Medium = 2,
758 Full = 3,
759}
760
761#[repr(C)]
763#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
764#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
765pub enum FcRgba {
766 #[default]
767 Unknown = 0,
768 Rgb = 1,
769 Bgr = 2,
770 Vrgb = 3,
771 Vbgr = 4,
772 None = 5,
773}
774
775#[repr(C)]
777#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
778#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
779pub enum FcLcdFilter {
780 #[default]
781 None = 0,
782 Default = 1,
783 Light = 2,
784 Legacy = 3,
785}
786
787#[derive(Debug, Default, Clone)]
789#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
790pub struct FcFontRenderConfig {
791 pub antialias: Option<bool>,
792 pub hinting: Option<bool>,
793 pub hintstyle: Option<FcHintStyle>,
794 pub autohint: Option<bool>,
795 pub rgba: Option<FcRgba>,
796 pub lcdfilter: Option<FcLcdFilter>,
797 pub embeddedbitmap: Option<bool>,
798 pub embolden: Option<bool>,
799 pub dpi: Option<f64>,
800 pub scale: Option<f64>,
801 pub minspace: Option<bool>,
802}
803
804impl Eq for FcFontRenderConfig {}
806
807impl PartialEq for FcFontRenderConfig {
809 fn eq(&self, other: &Self) -> bool {
810 self.cmp(other) == core::cmp::Ordering::Equal
811 }
812}
813
814impl PartialOrd for FcFontRenderConfig {
815 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
816 Some(self.cmp(other))
817 }
818}
819
820impl Ord for FcFontRenderConfig {
821 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
822 let ord = self
824 .antialias
825 .cmp(&other.antialias)
826 .then_with(|| self.hinting.cmp(&other.hinting))
827 .then_with(|| self.hintstyle.cmp(&other.hintstyle))
828 .then_with(|| self.autohint.cmp(&other.autohint))
829 .then_with(|| self.rgba.cmp(&other.rgba))
830 .then_with(|| self.lcdfilter.cmp(&other.lcdfilter))
831 .then_with(|| self.embeddedbitmap.cmp(&other.embeddedbitmap))
832 .then_with(|| self.embolden.cmp(&other.embolden))
833 .then_with(|| self.minspace.cmp(&other.minspace));
834
835 let ord = ord.then_with(|| {
837 let a = self.dpi.map(|v| v.to_bits());
838 let b = other.dpi.map(|v| v.to_bits());
839 a.cmp(&b)
840 });
841 ord.then_with(|| {
842 let a = self.scale.map(|v| v.to_bits());
843 let b = other.scale.map(|v| v.to_bits());
844 a.cmp(&b)
845 })
846 }
847}
848
849#[derive(Default, Clone, PartialOrd, Ord, PartialEq, Eq)]
851#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
852#[repr(C)]
853pub struct FcPattern {
854 pub name: Option<String>,
856 pub family: Option<String>,
858 pub italic: PatternMatch,
860 pub oblique: PatternMatch,
862 pub bold: PatternMatch,
864 pub monospace: PatternMatch,
866 pub condensed: PatternMatch,
868 pub weight: FcWeight,
870 pub stretch: FcStretch,
872 pub unicode_ranges: Vec<UnicodeRange>,
874 pub metadata: FcFontMetadata,
876 pub render_config: FcFontRenderConfig,
878}
879
880impl core::fmt::Debug for FcPattern {
881 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
882 let mut d = f.debug_struct("FcPattern");
883
884 if let Some(name) = &self.name {
885 d.field("name", name);
886 }
887
888 if let Some(family) = &self.family {
889 d.field("family", family);
890 }
891
892 if self.italic != PatternMatch::DontCare {
893 d.field("italic", &self.italic);
894 }
895
896 if self.oblique != PatternMatch::DontCare {
897 d.field("oblique", &self.oblique);
898 }
899
900 if self.bold != PatternMatch::DontCare {
901 d.field("bold", &self.bold);
902 }
903
904 if self.monospace != PatternMatch::DontCare {
905 d.field("monospace", &self.monospace);
906 }
907
908 if self.condensed != PatternMatch::DontCare {
909 d.field("condensed", &self.condensed);
910 }
911
912 if self.weight != FcWeight::Normal {
913 d.field("weight", &self.weight);
914 }
915
916 if self.stretch != FcStretch::Normal {
917 d.field("stretch", &self.stretch);
918 }
919
920 if !self.unicode_ranges.is_empty() {
921 d.field("unicode_ranges", &self.unicode_ranges);
922 }
923
924 let empty_metadata = FcFontMetadata::default();
926 if self.metadata != empty_metadata {
927 d.field("metadata", &self.metadata);
928 }
929
930 let empty_render_config = FcFontRenderConfig::default();
932 if self.render_config != empty_render_config {
933 d.field("render_config", &self.render_config);
934 }
935
936 d.finish()
937 }
938}
939
940#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
942#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
943pub struct FcFontMetadata {
944 pub copyright: Option<String>,
945 pub designer: Option<String>,
946 pub designer_url: Option<String>,
947 pub font_family: Option<String>,
948 pub font_subfamily: Option<String>,
949 pub full_name: Option<String>,
950 pub id_description: Option<String>,
951 pub license: Option<String>,
952 pub license_url: Option<String>,
953 pub manufacturer: Option<String>,
954 pub manufacturer_url: Option<String>,
955 pub postscript_name: Option<String>,
956 pub preferred_family: Option<String>,
957 pub preferred_subfamily: Option<String>,
958 pub trademark: Option<String>,
959 pub unique_id: Option<String>,
960 pub version: Option<String>,
961}
962
963impl FcPattern {
964 pub fn contains_char(&self, c: char) -> bool {
966 if self.unicode_ranges.is_empty() {
967 return true; }
969
970 for range in &self.unicode_ranges {
971 if range.contains(c) {
972 return true;
973 }
974 }
975
976 false
977 }
978}
979
980#[derive(Debug, Clone, PartialEq, Eq)]
982pub struct FontMatch {
983 pub id: FontId,
984 pub unicode_ranges: Vec<UnicodeRange>,
985 pub fallbacks: Vec<FontMatchNoFallback>,
986}
987
988#[derive(Debug, Clone, PartialEq, Eq)]
990pub struct FontMatchNoFallback {
991 pub id: FontId,
992 pub unicode_ranges: Vec<UnicodeRange>,
993}
994
995#[derive(Debug, Clone, PartialEq, Eq)]
997pub struct ResolvedFontRun {
998 pub text: String,
1000 pub start_byte: usize,
1002 pub end_byte: usize,
1004 pub font_id: Option<FontId>,
1006 pub css_source: String,
1008}
1009
1010#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
1012#[cfg_attr(feature = "cache", derive(serde::Serialize, serde::Deserialize))]
1013#[repr(C)]
1014pub struct FcFontPath {
1015 pub path: String,
1016 pub font_index: usize,
1017 #[cfg_attr(feature = "cache", serde(default))]
1019 pub bytes_hash: u64,
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Eq)]
1024#[repr(C)]
1025pub struct FcFont {
1026 pub bytes: Vec<u8>,
1027 pub font_index: usize,
1028 pub id: String, }
1030
1031#[derive(Debug, Clone)]
1033pub enum OwnedFontSource {
1034 Memory(FcFont),
1036 Disk(FcFontPath),
1038}
1039
1040#[cfg(feature = "std")]
1042pub enum FontBytes {
1043 Owned(std::sync::Arc<[u8]>),
1045 #[cfg(not(target_family = "wasm"))]
1047 Mmapped(mmapio::Mmap),
1048}
1049
1050#[cfg(feature = "std")]
1051impl FontBytes {
1052 #[inline]
1054 pub fn as_slice(&self) -> &[u8] {
1055 match self {
1056 FontBytes::Owned(arc) => arc,
1057 #[cfg(not(target_family = "wasm"))]
1058 FontBytes::Mmapped(m) => &m[..],
1059 }
1060 }
1061}
1062
1063#[cfg(feature = "std")]
1064impl core::ops::Deref for FontBytes {
1065 type Target = [u8];
1066 #[inline]
1067 fn deref(&self) -> &[u8] {
1068 self.as_slice()
1069 }
1070}
1071
1072#[cfg(feature = "std")]
1073impl AsRef<[u8]> for FontBytes {
1074 #[inline]
1075 fn as_ref(&self) -> &[u8] {
1076 self.as_slice()
1077 }
1078}
1079
1080#[cfg(feature = "std")]
1081impl core::fmt::Debug for FontBytes {
1082 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1083 let kind = match self {
1084 FontBytes::Owned(_) => "Owned",
1085 #[cfg(not(target_family = "wasm"))]
1086 FontBytes::Mmapped(_) => "Mmapped",
1087 };
1088 write!(f, "FontBytes::{}({} bytes)", kind, self.as_slice().len())
1089 }
1090}
1091
1092#[cfg(feature = "std")]
1094fn open_font_bytes_mmap(path: &str) -> Option<std::sync::Arc<FontBytes>> {
1095 use std::fs::File;
1096 use std::sync::Arc;
1097
1098 #[cfg(not(target_family = "wasm"))]
1099 {
1100 if let Ok(file) = File::open(path) {
1101 if let Ok(mmap) = unsafe { mmapio::MmapOptions::new().map(&file) } {
1103 return Some(Arc::new(FontBytes::Mmapped(mmap)));
1104 }
1105 }
1106 }
1107 let bytes = std::fs::read(path).ok()?;
1108 Some(Arc::new(FontBytes::Owned(Arc::from(bytes))))
1109}
1110
1111#[derive(Debug, Clone)]
1113pub struct NamedFont {
1114 pub name: String,
1116 pub bytes: Vec<u8>,
1118}
1119
1120impl NamedFont {
1121 pub fn new(name: impl Into<String>, bytes: Vec<u8>) -> Self {
1123 Self {
1124 name: name.into(),
1125 bytes,
1126 }
1127 }
1128}
1129
1130pub struct FcFontCache {
1132 pub(crate) shared: std::sync::Arc<FcFontCacheShared>,
1133}
1134
1135#[cfg(not(feature = "single-thread-unsafe-locks"))]
1139pub struct StLock<T> {
1140 lock: std::sync::RwLock<T>,
1141}
1142#[cfg(not(feature = "single-thread-unsafe-locks"))]
1143impl<T> core::fmt::Debug for StLock<T> {
1144 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1145 f.write_str("StLock(..)")
1146 }
1147}
1148#[cfg(not(feature = "single-thread-unsafe-locks"))]
1149impl<T> StLock<T> {
1150 pub fn new(v: T) -> Self {
1151 Self {
1152 lock: std::sync::RwLock::new(v),
1153 }
1154 }
1155 pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1156 Ok(StReadGuard {
1157 g: self.lock.read().unwrap_or_else(|e| e.into_inner()),
1158 })
1159 }
1160 pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1161 Ok(StWriteGuard {
1162 g: self.lock.write().unwrap_or_else(|e| e.into_inner()),
1163 })
1164 }
1165 pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1166 self.write()
1167 }
1168}
1169#[cfg(not(feature = "single-thread-unsafe-locks"))]
1170pub struct StReadGuard<'a, T> {
1171 g: std::sync::RwLockReadGuard<'a, T>,
1172}
1173#[cfg(not(feature = "single-thread-unsafe-locks"))]
1174impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1175 type Target = T;
1176 fn deref(&self) -> &T {
1177 &self.g
1178 }
1179}
1180#[cfg(not(feature = "single-thread-unsafe-locks"))]
1181pub struct StWriteGuard<'a, T> {
1182 g: std::sync::RwLockWriteGuard<'a, T>,
1183}
1184#[cfg(not(feature = "single-thread-unsafe-locks"))]
1185impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1186 type Target = T;
1187 fn deref(&self) -> &T {
1188 &self.g
1189 }
1190}
1191#[cfg(not(feature = "single-thread-unsafe-locks"))]
1192impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1193 fn deref_mut(&mut self) -> &mut T {
1194 &mut self.g
1195 }
1196}
1197
1198#[cfg(feature = "single-thread-unsafe-locks")]
1199pub struct StLock<T> {
1200 cell: std::cell::UnsafeCell<T>,
1201}
1202#[cfg(feature = "single-thread-unsafe-locks")]
1203unsafe impl<T> Sync for StLock<T> {}
1204#[cfg(feature = "single-thread-unsafe-locks")]
1205unsafe impl<T> Send for StLock<T> {}
1206#[cfg(feature = "single-thread-unsafe-locks")]
1207impl<T> core::fmt::Debug for StLock<T> {
1208 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1209 f.write_str("StLock(..)")
1210 }
1211}
1212#[cfg(feature = "single-thread-unsafe-locks")]
1213impl<T> StLock<T> {
1214 pub fn new(v: T) -> Self {
1215 Self {
1216 cell: std::cell::UnsafeCell::new(v),
1217 }
1218 }
1219 pub fn read(&self) -> Result<StReadGuard<'_, T>, core::convert::Infallible> {
1220 Ok(StReadGuard {
1221 r: unsafe { &*self.cell.get() },
1222 })
1223 }
1224 pub fn write(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1225 Ok(StWriteGuard {
1226 r: unsafe { &mut *self.cell.get() },
1227 })
1228 }
1229 pub fn lock(&self) -> Result<StWriteGuard<'_, T>, core::convert::Infallible> {
1230 Ok(StWriteGuard {
1231 r: unsafe { &mut *self.cell.get() },
1232 })
1233 }
1234}
1235#[cfg(feature = "single-thread-unsafe-locks")]
1236pub struct StReadGuard<'a, T> {
1237 r: &'a T,
1238}
1239#[cfg(feature = "single-thread-unsafe-locks")]
1240impl<'a, T> core::ops::Deref for StReadGuard<'a, T> {
1241 type Target = T;
1242 fn deref(&self) -> &T {
1243 self.r
1244 }
1245}
1246#[cfg(feature = "single-thread-unsafe-locks")]
1247pub struct StWriteGuard<'a, T> {
1248 r: &'a mut T,
1249}
1250#[cfg(feature = "single-thread-unsafe-locks")]
1251impl<'a, T> core::ops::Deref for StWriteGuard<'a, T> {
1252 type Target = T;
1253 fn deref(&self) -> &T {
1254 self.r
1255 }
1256}
1257#[cfg(feature = "single-thread-unsafe-locks")]
1258impl<'a, T> core::ops::DerefMut for StWriteGuard<'a, T> {
1259 fn deref_mut(&mut self) -> &mut T {
1260 self.r
1261 }
1262}
1263
1264pub(crate) struct FcFontCacheShared {
1265 pub(crate) state: StLock<FcFontCacheInner>,
1267 pub(crate) chain_cache: StLock<std::collections::HashMap<FontChainCacheKey, FontFallbackChain>>,
1269 pub(crate) shared_bytes: StLock<std::collections::HashMap<u64, std::sync::Weak<FontBytes>>>,
1271}
1272
1273#[derive(Default, Debug)]
1275pub(crate) struct FcFontCacheInner {
1276 pub(crate) by_path: BTreeMap<String, Vec<FontId>>,
1278 pub(crate) disk_fonts: BTreeMap<FontId, FcFontPath>,
1280 pub(crate) memory_fonts: BTreeMap<FontId, FcFont>,
1282 pub(crate) metadata: BTreeMap<FontId, FcPattern>,
1284 pub(crate) family_index: BTreeMap<String, alloc::vec::Vec<FontId>>,
1286 pub(crate) fallback_config: FcFallbackConfig,
1288}
1289
1290impl FcFontCacheInner {
1291 pub(crate) fn index_pattern_family(&mut self, pattern: &FcPattern, id: FontId) {
1293 for key in [pattern.family.as_deref(), pattern.name.as_deref()]
1294 .into_iter()
1295 .flatten()
1296 .map(crate::utils::normalize_family_name)
1297 .filter(|k| !k.is_empty())
1298 {
1299 let slot = self.family_index.entry(key).or_default();
1300 if !slot.contains(&id) {
1301 slot.push(id);
1302 }
1303 }
1304 }
1305
1306 pub(crate) fn insert_disk_font(
1308 &mut self,
1309 mut pattern: FcPattern,
1310 id: FontId,
1311 path: FcFontPath,
1312 ) -> FontId {
1313 pattern.unicode_ranges =
1314 FcFontCache::normalize_unicode_ranges(core::mem::take(&mut pattern.unicode_ranges));
1315 if let Some(existing) = self.by_path.get(&path.path).and_then(|ids| {
1316 ids.iter().copied().find(|existing| {
1317 self.disk_fonts
1318 .get(existing)
1319 .is_some_and(|p| p.font_index == path.font_index)
1320 && self.metadata.get(existing) == Some(&pattern)
1321 })
1322 }) {
1323 return existing;
1324 }
1325 self.index_pattern_family(&pattern, id);
1326 self.by_path.entry(path.path.clone()).or_default().push(id);
1327 self.disk_fonts.insert(id, path);
1328 self.metadata.insert(id, pattern);
1329 id
1330 }
1331
1332 pub(crate) fn insert_memory_font(
1334 &mut self,
1335 mut pattern: FcPattern,
1336 id: FontId,
1337 font: FcFont,
1338 ) -> FontId {
1339 pattern.unicode_ranges =
1340 FcFontCache::normalize_unicode_ranges(core::mem::take(&mut pattern.unicode_ranges));
1341 let hash = crate::utils::content_dedup_hash_u64(&font.bytes);
1342 if let Some(existing) = self.memory_fonts.iter().find_map(|(existing, f)| {
1343 (f.font_index == font.font_index
1344 && self.metadata.get(existing) == Some(&pattern)
1345 && crate::utils::content_dedup_hash_u64(&f.bytes) == hash)
1346 .then_some(*existing)
1347 }) {
1348 return existing;
1349 }
1350 self.index_pattern_family(&pattern, id);
1351 self.memory_fonts.insert(id, font);
1352 self.metadata.insert(id, pattern);
1353 id
1354 }
1355}
1356
1357impl Clone for FcFontCache {
1358 fn clone(&self) -> Self {
1360 Self {
1361 shared: std::sync::Arc::clone(&self.shared),
1362 }
1363 }
1364}
1365
1366impl core::fmt::Debug for FcFontCache {
1367 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1368 let state = self.state_read();
1369 f.debug_struct("FcFontCache")
1370 .field("fonts", &state.metadata.len())
1371 .field("metadata_len", &state.metadata.len())
1372 .field("disk_fonts_len", &state.disk_fonts.len())
1373 .field("memory_fonts_len", &state.memory_fonts.len())
1374 .finish()
1375 }
1376}
1377
1378impl Default for FcFontCache {
1379 fn default() -> Self {
1380 Self {
1381 shared: std::sync::Arc::new(FcFontCacheShared {
1382 state: StLock::new(FcFontCacheInner::default()),
1383 chain_cache: StLock::new(std::collections::HashMap::new()),
1384 shared_bytes: StLock::new(std::collections::HashMap::new()),
1385 }),
1386 }
1387 }
1388}
1389
1390impl FcFontCache {
1391 pub fn fallback_config(&self) -> FcFallbackConfig {
1393 self.state_read().fallback_config.clone()
1394 }
1395
1396 pub fn set_fallback_config(&self, config: FcFallbackConfig) -> &Self {
1398 self.state_write().fallback_config = config;
1399 self.clear_chain_cache();
1400 self
1401 }
1402
1403 pub fn with_fallback_config(self, config: FcFallbackConfig) -> Self {
1405 self.set_fallback_config(config);
1406 self
1407 }
1408
1409 pub(crate) fn clear_chain_cache(&self) {
1411 match self.shared.chain_cache.lock() {
1412 Ok(mut memo) => memo.clear(),
1413 Err(e) => match e {},
1414 }
1415 }
1416
1417 #[deprecated(
1419 since = "5.0.0",
1420 note = "read `fallback_config().generic_candidates(..)` / `substitutions_for(..)`"
1421 )]
1422 pub fn system_alias_prefs(&self, family: &str) -> Vec<String> {
1423 let state = self.state_read();
1424 match GenericFamily::from_css(family) {
1425 Some(generic) => state.fallback_config.generic_candidates(generic).to_vec(),
1426 None => state.fallback_config.substitutions_for(family).to_vec(),
1427 }
1428 }
1429
1430 #[deprecated(
1432 since = "5.0.0",
1433 note = "use `fallback_config().candidate_families(families, ranges)`"
1434 )]
1435 pub fn expand_font_families_config_first(
1436 &self,
1437 families: &[String],
1438 os: OperatingSystem,
1439 unicode_ranges: &[UnicodeRange],
1440 ) -> Vec<String> {
1441 let mut config = self.fallback_config();
1442 config.merge_defaults(&FcFallbackConfig::os_defaults(os));
1443 config.candidate_families(families, unicode_ranges)
1444 }
1445
1446 #[inline]
1448 pub(crate) fn state_read(&self) -> StReadGuard<'_, FcFontCacheInner> {
1449 match self.shared.state.read() {
1451 Ok(g) => g,
1452 Err(e) => match e {},
1453 }
1454 }
1455
1456 #[inline]
1458 pub(crate) fn state_write(&self) -> StWriteGuard<'_, FcFontCacheInner> {
1459 match self.shared.state.write() {
1461 Ok(g) => g,
1462 Err(e) => match e {},
1463 }
1464 }
1465
1466 pub fn with_memory_fonts(&self, fonts: Vec<(FcPattern, FcFont)>) -> &Self {
1468 let fonts: Vec<(FcPattern, FcFont)> = fonts
1470 .into_iter()
1471 .map(|(pattern, font)| (Self::populate_memory_font_ranges(pattern, &font), font))
1472 .collect();
1473 let mut state = self.state_write();
1474 for (pattern, font) in fonts {
1475 let id = FontId::new();
1476 state.insert_memory_font(pattern, id, font);
1477 }
1478 self
1479 }
1480
1481 pub fn with_memory_font_with_id(&self, id: FontId, pattern: FcPattern, font: FcFont) -> &Self {
1483 let pattern = Self::populate_memory_font_ranges(pattern, &font);
1484 let mut state = self.state_write();
1485 state.insert_memory_font(pattern, id, font);
1486 self
1487 }
1488
1489 #[cfg(all(feature = "std", feature = "parsing"))]
1491 fn populate_memory_font_ranges(mut pattern: FcPattern, font: &FcFont) -> FcPattern {
1492 if !pattern.unicode_ranges.is_empty() {
1493 return pattern;
1494 }
1495 if let Some(faces) = FcParseFontBytes(&font.bytes, &font.id) {
1496 let ranges = faces
1498 .iter()
1499 .find(|(_, f)| f.font_index == font.font_index)
1500 .or_else(|| faces.first())
1501 .map(|(p, _)| p.unicode_ranges.clone())
1502 .unwrap_or_default();
1503 if !ranges.is_empty() {
1504 pattern.unicode_ranges = ranges;
1505 }
1506 }
1507 pattern
1508 }
1509
1510 #[cfg(not(all(feature = "std", feature = "parsing")))]
1512 fn populate_memory_font_ranges(pattern: FcPattern, _font: &FcFont) -> FcPattern {
1513 pattern
1514 }
1515
1516 pub fn insert_builder_font(&self, pattern: FcPattern, path: FcFontPath) {
1518 let id = FontId::new();
1519 {
1520 let mut state = self.state_write();
1521 state.insert_disk_font(pattern, id, path);
1522 }
1523 self.clear_chain_cache();
1525 }
1526
1527 #[cfg(feature = "std")]
1528 #[doc(hidden)]
1529 pub fn chain_cache_len(&self) -> usize {
1530 self.shared.chain_cache.lock().map(|c| c.len()).unwrap_or(0)
1531 }
1532
1533 pub fn insert_fast_pattern(&self, pattern: FcPattern, path: FcFontPath) -> FontId {
1535 let id = {
1536 let mut state = self.state_write();
1537 state.insert_disk_font(pattern, FontId::new(), path)
1538 };
1539 self.clear_chain_cache();
1540 id
1541 }
1542
1543 pub fn lookup_paths_cached(&self, path: &str) -> Option<Vec<FontId>> {
1545 self.state_read()
1546 .by_path
1547 .get(path)
1548 .cloned()
1549 .filter(|ids| !ids.is_empty())
1550 }
1551
1552 pub fn get_font_by_id(&self, id: &FontId) -> Option<OwnedFontSource> {
1554 let state = self.state_read();
1555 if let Some(font) = state.memory_fonts.get(id) {
1556 return Some(OwnedFontSource::Memory(font.clone()));
1557 }
1558 if let Some(path) = state.disk_fonts.get(id) {
1559 return Some(OwnedFontSource::Disk(path.clone()));
1560 }
1561 None
1562 }
1563
1564 pub fn get_metadata_by_id(&self, id: &FontId) -> Option<FcPattern> {
1566 self.state_read().metadata.get(id).cloned()
1567 }
1568
1569 #[cfg(feature = "std")]
1571 pub fn get_font_bytes(&self, id: &FontId) -> Option<std::sync::Arc<FontBytes>> {
1572 use std::sync::Arc;
1573 match self.get_font_by_id(id)? {
1574 OwnedFontSource::Memory(font) => {
1575 Some(Arc::new(FontBytes::Owned(Arc::from(font.bytes.as_slice()))))
1576 }
1577 OwnedFontSource::Disk(path) => {
1578 let hash = path.bytes_hash;
1579 if hash != 0 {
1580 let guard = self.shared.shared_bytes.lock().unwrap();
1581 {
1582 if let Some(weak) = guard.get(&hash) {
1583 if let Some(arc) = weak.upgrade() {
1584 return Some(arc);
1585 }
1586 }
1587 }
1588 }
1589
1590 let arc = open_font_bytes_mmap(&path.path)?;
1591 if hash != 0 {
1592 let mut guard = self.shared.shared_bytes.lock().unwrap();
1593 {
1594 guard.insert(hash, Arc::downgrade(&arc));
1596 }
1597 }
1598 Some(arc)
1599 }
1600 }
1601 }
1602
1603 #[cfg(not(feature = "std"))]
1605 pub fn build() -> Self {
1606 Self::default()
1607 }
1608
1609 #[cfg(all(feature = "std", not(feature = "parsing")))]
1611 pub fn build() -> Self {
1612 Self::build_from_filenames()
1613 }
1614
1615 #[cfg(all(feature = "std", feature = "parsing"))]
1617 pub fn build() -> Self {
1618 Self::build_inner(None)
1619 }
1620
1621 #[cfg(all(feature = "std", not(feature = "parsing")))]
1623 fn build_from_filenames() -> Self {
1624 let cache = Self::default();
1625 {
1626 let mut state = cache.state_write();
1627 state.fallback_config = FcFallbackConfig::os_defaults(OperatingSystem::current());
1628 for dir in crate::config::font_directories(OperatingSystem::current()) {
1629 for path in FcCollectFontFilesRecursive(dir) {
1630 let pattern = match pattern_from_filename(&path) {
1631 Some(p) => p,
1632 None => continue,
1633 };
1634 state.insert_disk_font(
1635 pattern,
1636 FontId::new(),
1637 FcFontPath {
1638 path: path.to_string_lossy().to_string(),
1639 font_index: 0,
1640 bytes_hash: 0,
1643 },
1644 );
1645 }
1646 }
1647 }
1648 cache
1649 }
1650
1651 #[cfg(all(feature = "std", feature = "parsing"))]
1653 pub fn build_with_families(families: &[impl AsRef<str>]) -> Self {
1654 let os = OperatingSystem::current();
1656 let mut target_families: Vec<String> = Vec::new();
1657
1658 for family in families {
1659 let family_str = family.as_ref();
1660 let expanded = FcFallbackConfig::os_defaults(os)
1661 .expand_family(family_str, DEFAULT_UNICODE_FALLBACK_SCRIPTS);
1662 if expanded.is_empty() || (expanded.len() == 1 && expanded[0] == family_str) {
1663 target_families.push(family_str.to_string());
1664 } else {
1665 target_families.extend(expanded);
1666 }
1667 }
1668
1669 Self::build_inner(Some(&target_families))
1670 }
1671
1672 #[cfg(all(feature = "std", feature = "parsing"))]
1674 fn build_inner(family_filter: Option<&[String]>) -> Self {
1675 let cache = FcFontCache::default();
1676
1677 let filter_normalized: Option<Vec<String>> = family_filter.map(|families| {
1679 families
1680 .iter()
1681 .map(|f| crate::utils::normalize_family_name(f))
1682 .collect()
1683 });
1684
1685 let matches_filter = |pattern: &FcPattern| -> bool {
1687 match &filter_normalized {
1688 None => true, Some(targets) => {
1690 pattern.name.as_ref().map_or(false, |name| {
1691 let name_norm = crate::utils::normalize_family_name(name);
1692 targets.iter().any(|target| name_norm.contains(target))
1693 }) || pattern.family.as_ref().map_or(false, |family| {
1694 let family_norm = crate::utils::normalize_family_name(family);
1695 targets.iter().any(|target| family_norm.contains(target))
1696 })
1697 }
1698 }
1699 };
1700
1701 let mut state = cache.state_write();
1702 state.fallback_config = FcFallbackConfig::os_defaults(OperatingSystem::current());
1703
1704 #[cfg(target_os = "linux")]
1705 {
1706 if let Some((font_entries, render_configs, system_aliases)) = FcScanDirectories() {
1707 let mut config = FcFallbackConfig::default();
1710 config.absorb_system_aliases(system_aliases);
1711 config.merge_defaults(&state.fallback_config);
1712 state.fallback_config = config;
1713 for (mut pattern, path) in font_entries {
1714 if matches_filter(&pattern) {
1715 if let Some(family) = pattern.name.as_ref().or(pattern.family.as_ref()) {
1717 if let Some(rc) = render_configs.get(family) {
1718 pattern.render_config = rc.clone();
1719 }
1720 }
1721 let id = FontId::new();
1722 state.insert_disk_font(pattern, id, path);
1723 }
1724 }
1725 }
1726 }
1727
1728 #[cfg(target_os = "windows")]
1729 {
1730 let system_root = std::env::var("SystemRoot")
1731 .or_else(|_| std::env::var("WINDIR"))
1732 .unwrap_or_else(|_| "C:\\Windows".to_string());
1733
1734 let user_profile =
1735 std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\Default".to_string());
1736
1737 let font_dirs = vec![
1738 (None, format!("{}\\Fonts\\", system_root)),
1739 (
1740 None,
1741 format!(
1742 "{}\\AppData\\Local\\Microsoft\\Windows\\Fonts\\",
1743 user_profile
1744 ),
1745 ),
1746 ];
1747
1748 let font_entries = FcScanDirectoriesInner(&font_dirs);
1749 for (pattern, path) in font_entries {
1750 if matches_filter(&pattern) {
1751 let id = FontId::new();
1752 state.insert_disk_font(pattern, id, path);
1753 }
1754 }
1755 }
1756
1757 #[cfg(target_os = "macos")]
1758 {
1759 let font_dirs = vec![
1760 (None, "~/Library/Fonts".to_owned()),
1761 (None, "/System/Library/Fonts".to_owned()),
1762 (None, "/Library/Fonts".to_owned()),
1763 (None, "/System/Library/AssetsV2".to_owned()),
1764 ];
1765
1766 let font_entries = FcScanDirectoriesInner(&font_dirs);
1767 for (pattern, path) in font_entries {
1768 if matches_filter(&pattern) {
1769 let id = FontId::new();
1770 state.insert_disk_font(pattern, id, path);
1771 }
1772 }
1773 }
1774
1775 #[cfg(target_os = "ios")]
1777 {
1778 let font_files = crate::mobile_ios::copy_available_font_urls();
1779 let font_entries = FcParseFontFiles(&font_files);
1780 for (pattern, path) in font_entries {
1781 if matches_filter(&pattern) {
1782 let id = FontId::new();
1783 state.insert_disk_font(pattern, id, path);
1784 }
1785 }
1786 }
1787
1788 #[cfg(target_os = "android")]
1790 {
1791 let font_dirs = vec![
1792 (None, "/system/fonts".to_owned()),
1793 (None, "/product/fonts".to_owned()),
1794 (None, "/system_ext/fonts".to_owned()),
1795 (None, "/data/fonts".to_owned()),
1796 ];
1797
1798 let font_entries = FcScanDirectoriesInner(&font_dirs);
1799 for (pattern, path) in font_entries {
1800 if matches_filter(&pattern) {
1801 let id = FontId::new();
1802 state.insert_disk_font(pattern, id, path);
1803 }
1804 }
1805 }
1806
1807 drop(state);
1808 cache
1809 }
1810
1811 pub fn is_memory_font(&self, id: &FontId) -> bool {
1813 self.state_read().memory_fonts.contains_key(id)
1814 }
1815
1816 pub fn list(&self) -> Vec<(FcPattern, FontId)> {
1818 self.state_read()
1819 .metadata
1820 .iter()
1821 .map(|(id, pattern)| (pattern.clone(), *id))
1822 .collect()
1823 }
1824
1825 pub fn for_each_pattern<F: FnMut(&FcPattern, &FontId)>(&self, mut f: F) {
1827 let state = self.state_read();
1828 for (id, pattern) in &state.metadata {
1829 f(pattern, id);
1830 }
1831 }
1832
1833 pub fn is_empty(&self) -> bool {
1834 self.state_read().metadata.is_empty()
1835 }
1836
1837 pub fn len(&self) -> usize {
1839 self.state_read().metadata.len()
1840 }
1841
1842 pub fn query_with_fallback(
1844 &self,
1845 pattern: &FcPattern,
1846 trace: &mut Vec<TraceMsg>,
1847 ) -> Option<FontMatch> {
1848 if let Some(m) = self.query(pattern, trace) {
1849 return Some(m);
1850 }
1851
1852 if pattern.name.is_some() || pattern.family.is_some() {
1854 let relaxed = FcPattern {
1855 name: None,
1856 family: None,
1857 ..pattern.clone()
1858 };
1859 if let Some(m) = self.query(&relaxed, trace) {
1860 return Some(m);
1861 }
1862 }
1863
1864 let bare = FcPattern {
1866 unicode_ranges: pattern.unicode_ranges.clone(),
1867 ..FcPattern::default()
1868 };
1869 self.query(&bare, trace)
1870 }
1871
1872 pub fn query(&self, pattern: &FcPattern, trace: &mut Vec<TraceMsg>) -> Option<FontMatch> {
1874 let state = self.state_read();
1875
1876 let mut matches: Vec<(bool, fallback::RankKey, FontId, &FcPattern)> = Vec::new();
1878
1879 for (id, metadata) in &state.metadata {
1880 if Self::query_matches_internal(metadata, pattern, trace) {
1881 let is_disk = !state.memory_fonts.contains_key(id);
1882 matches.push((
1883 is_disk,
1884 fallback::RankKey::for_request(pattern, metadata, &pattern.unicode_ranges),
1885 *id,
1886 metadata,
1887 ));
1888 }
1889 }
1890
1891 matches.sort();
1892
1893 matches.first().map(|(_, _, id, metadata)| FontMatch {
1894 id: *id,
1895 unicode_ranges: metadata.unicode_ranges.clone(),
1896 fallbacks: Vec::new(),
1897 })
1898 }
1899
1900 pub fn get_memory_font(&self, id: &FontId) -> Option<FcFont> {
1902 self.state_read().memory_fonts.get(id).cloned()
1903 }
1904
1905 fn trace_path(k: &FcPattern) -> String {
1907 k.name
1908 .as_ref()
1909 .cloned()
1910 .unwrap_or_else(|| "<unknown>".to_string())
1911 }
1912
1913 pub fn query_matches_internal(
1914 k: &FcPattern,
1915 pattern: &FcPattern,
1916 trace: &mut Vec<TraceMsg>,
1917 ) -> bool {
1918 if let Some(ref name) = pattern.name {
1920 if !k.name.as_ref().map_or(false, |kn| kn.contains(name)) {
1921 trace.push(TraceMsg {
1922 level: TraceLevel::Info,
1923 path: Self::trace_path(k),
1924 reason: MatchReason::NameMismatch {
1925 requested: pattern.name.clone(),
1926 found: k.name.clone(),
1927 },
1928 });
1929 return false;
1930 }
1931 }
1932
1933 if let Some(ref family) = pattern.family {
1935 if !k.family.as_ref().map_or(false, |kf| kf.contains(family)) {
1936 trace.push(TraceMsg {
1937 level: TraceLevel::Info,
1938 path: Self::trace_path(k),
1939 reason: MatchReason::FamilyMismatch {
1940 requested: pattern.family.clone(),
1941 found: k.family.clone(),
1942 },
1943 });
1944 return false;
1945 }
1946 }
1947
1948 let style_properties = [
1950 (
1951 "italic",
1952 pattern.italic.needs_to_match(),
1953 pattern.italic.matches(&k.italic),
1954 ),
1955 (
1956 "oblique",
1957 pattern.oblique.needs_to_match(),
1958 pattern.oblique.matches(&k.oblique),
1959 ),
1960 (
1961 "bold",
1962 pattern.bold.needs_to_match(),
1963 pattern.bold.matches(&k.bold),
1964 ),
1965 (
1966 "monospace",
1967 pattern.monospace.needs_to_match(),
1968 pattern.monospace.matches(&k.monospace),
1969 ),
1970 (
1971 "condensed",
1972 pattern.condensed.needs_to_match(),
1973 pattern.condensed.matches(&k.condensed),
1974 ),
1975 ];
1976
1977 for (property_name, needs_to_match, matches) in style_properties {
1978 if needs_to_match && !matches {
1979 let (requested, found) = match property_name {
1980 "italic" => (format!("{:?}", pattern.italic), format!("{:?}", k.italic)),
1981 "oblique" => (format!("{:?}", pattern.oblique), format!("{:?}", k.oblique)),
1982 "bold" => (format!("{:?}", pattern.bold), format!("{:?}", k.bold)),
1983 "monospace" => (
1984 format!("{:?}", pattern.monospace),
1985 format!("{:?}", k.monospace),
1986 ),
1987 "condensed" => (
1988 format!("{:?}", pattern.condensed),
1989 format!("{:?}", k.condensed),
1990 ),
1991 _ => (String::new(), String::new()),
1992 };
1993
1994 trace.push(TraceMsg {
1995 level: TraceLevel::Info,
1996 path: Self::trace_path(k),
1997 reason: MatchReason::StyleMismatch {
1998 property: property_name,
1999 requested,
2000 found,
2001 },
2002 });
2003 return false;
2004 }
2005 }
2006
2007 if pattern.weight != FcWeight::Normal && pattern.weight != k.weight {
2009 trace.push(TraceMsg {
2010 level: TraceLevel::Info,
2011 path: Self::trace_path(k),
2012 reason: MatchReason::WeightMismatch {
2013 requested: pattern.weight,
2014 found: k.weight,
2015 },
2016 });
2017 return false;
2018 }
2019
2020 if pattern.stretch != FcStretch::Normal && pattern.stretch != k.stretch {
2022 trace.push(TraceMsg {
2023 level: TraceLevel::Info,
2024 path: Self::trace_path(k),
2025 reason: MatchReason::StretchMismatch {
2026 requested: pattern.stretch,
2027 found: k.stretch,
2028 },
2029 });
2030 return false;
2031 }
2032
2033 if !pattern.unicode_ranges.is_empty() {
2035 let mut has_overlap = false;
2036
2037 for p_range in &pattern.unicode_ranges {
2038 for k_range in &k.unicode_ranges {
2039 if p_range.overlaps(k_range) {
2040 has_overlap = true;
2041 break;
2042 }
2043 }
2044 if has_overlap {
2045 break;
2046 }
2047 }
2048
2049 if !has_overlap {
2050 trace.push(TraceMsg {
2051 level: TraceLevel::Info,
2052 path: Self::trace_path(k),
2053 reason: MatchReason::UnicodeRangeMismatch {
2054 character: '\0', ranges: k.unicode_ranges.clone(),
2056 },
2057 });
2058 return false;
2059 }
2060 }
2061
2062 true
2063 }
2064
2065 pub fn extract_font_name_tokens(name: &str) -> Vec<String> {
2067 let mut tokens = Vec::new();
2068 let mut current_token = String::new();
2069 let mut last_was_lower = false;
2070
2071 for c in name.chars() {
2072 if c.is_whitespace() || c == '-' || c == '_' {
2073 if !current_token.is_empty() {
2075 tokens.push(current_token.clone());
2076 current_token.clear();
2077 }
2078 last_was_lower = false;
2079 } else if c.is_uppercase() && last_was_lower && !current_token.is_empty() {
2080 tokens.push(current_token.clone());
2082 current_token.clear();
2083 current_token.push(c);
2084 last_was_lower = false;
2085 } else {
2086 current_token.push(c);
2087 last_was_lower = c.is_lowercase();
2088 }
2089 }
2090
2091 if !current_token.is_empty() {
2092 tokens.push(current_token);
2093 }
2094
2095 tokens
2096 }
2097
2098 pub fn calculate_unicode_coverage(ranges: &[UnicodeRange]) -> u64 {
2101 ranges
2102 .iter()
2103 .map(|range| (range.end - range.start + 1) as u64)
2104 .sum()
2105 }
2106
2107 pub fn normalize_unicode_ranges(mut ranges: Vec<UnicodeRange>) -> Vec<UnicodeRange> {
2109 if ranges.len() < 2 {
2110 return ranges;
2111 }
2112
2113 ranges.sort_unstable();
2114
2115 let mut out: Vec<UnicodeRange> = Vec::with_capacity(ranges.len());
2116 for range in ranges {
2117 match out.last_mut() {
2118 Some(prev) if range.start <= prev.end.saturating_add(1) => {
2121 prev.end = prev.end.max(range.end);
2122 }
2123 _ => out.push(range),
2124 }
2125 }
2126 out
2127 }
2128
2129 pub fn calculate_unicode_compatibility(
2131 requested: &[UnicodeRange],
2132 available: &[UnicodeRange],
2133 ) -> i32 {
2134 if requested.is_empty() {
2135 return Self::calculate_unicode_coverage(available) as i32;
2137 }
2138
2139 let mut total_coverage = 0u32;
2140
2141 for req_range in requested {
2142 for avail_range in available {
2143 let overlap_start = req_range.start.max(avail_range.start);
2145 let overlap_end = req_range.end.min(avail_range.end);
2146
2147 if overlap_start <= overlap_end {
2148 let overlap_size = overlap_end - overlap_start + 1;
2150 total_coverage += overlap_size;
2151 }
2152 }
2153 }
2154
2155 total_coverage as i32
2156 }
2157
2158 pub fn calculate_style_score(original: &FcPattern, candidate: &FcPattern) -> i32 {
2159 let mut score = 0_i32;
2160
2161 if (original.bold == PatternMatch::True && candidate.weight == FcWeight::Bold)
2163 || (original.bold == PatternMatch::False && candidate.weight != FcWeight::Bold)
2164 {
2165 } else {
2168 let weight_diff = (original.weight as i32 - candidate.weight as i32).abs();
2170 score += weight_diff as i32;
2171 }
2172
2173 if original.weight == candidate.weight {
2176 score -= 15;
2177 if original.weight == FcWeight::Normal {
2178 score -= 10; }
2180 }
2181
2182 if (original.condensed == PatternMatch::True && candidate.stretch.is_condensed())
2184 || (original.condensed == PatternMatch::False && !candidate.stretch.is_condensed())
2185 {
2186 } else {
2189 let stretch_diff = (original.stretch as i32 - candidate.stretch as i32).abs();
2191 score += (stretch_diff * 100) as i32;
2192 }
2193
2194 let style_props = [
2196 (original.italic, candidate.italic, 300, 150),
2197 (original.oblique, candidate.oblique, 200, 100),
2198 (original.bold, candidate.bold, 300, 150),
2199 (original.monospace, candidate.monospace, 100, 50),
2200 (original.condensed, candidate.condensed, 100, 50),
2201 ];
2202
2203 for (orig, cand, mismatch_penalty, dontcare_penalty) in style_props {
2204 if orig.needs_to_match() {
2205 if orig == PatternMatch::False && cand == PatternMatch::DontCare {
2206 score += dontcare_penalty / 2;
2209 } else if !orig.matches(&cand) {
2210 if cand == PatternMatch::DontCare {
2211 score += dontcare_penalty;
2212 } else {
2213 score += mismatch_penalty;
2214 }
2215 } else if orig == PatternMatch::True && cand == PatternMatch::True {
2216 score -= 20;
2218 } else if orig == PatternMatch::False && cand == PatternMatch::False {
2219 score -= 20;
2222 }
2223 } else {
2224 if cand == PatternMatch::True {
2226 score += dontcare_penalty / 3;
2227 }
2228 }
2229 }
2230
2231 if let (Some(name), Some(family)) = (&candidate.name, &candidate.family) {
2234 let name_lower = name.to_ascii_lowercase();
2235 let family_lower = family.to_ascii_lowercase();
2236
2237 let extra = if name_lower.starts_with(&family_lower) {
2239 name_lower[family_lower.len()..].to_string()
2240 } else {
2241 String::new()
2242 };
2243
2244 let stripped = extra
2246 .replace("regular", "")
2247 .replace("normal", "")
2248 .replace("book", "")
2249 .replace("roman", "");
2250 let stripped = stripped.trim();
2251
2252 if stripped.is_empty() {
2253 score -= 50;
2255 } else {
2256 let extra_words = stripped.split_whitespace().count();
2258 score += (extra_words as i32) * 25;
2259 }
2260 }
2261
2262 if let Some(ref subfamily) = candidate.metadata.font_subfamily {
2264 let sf_lower = subfamily.to_ascii_lowercase();
2265 if sf_lower == "regular" {
2266 score -= 30;
2267 }
2268 }
2269
2270 score
2271 }
2272}
2273
2274#[cfg(all(feature = "std", feature = "parsing"))]
2275#[allow(non_snake_case, dead_code)]
2276fn FcScanDirectories() -> Option<(
2277 Vec<(FcPattern, FcFontPath)>,
2278 BTreeMap<String, FcFontRenderConfig>,
2279 BTreeMap<String, Vec<String>>,
2280)> {
2281 let config = FcSystemConfig::from_system()?;
2282 if config.font_dirs.is_empty() {
2283 return None;
2284 }
2285 let dirs: Vec<(Option<String>, String)> = config
2286 .font_dirs
2287 .iter()
2288 .map(|dir| (None, dir.to_string_lossy().into_owned()))
2289 .collect();
2290 Some((
2291 FcScanDirectoriesInner(&dirs),
2292 config.render_configs,
2293 config.aliases,
2294 ))
2295}
2296
2297#[cfg(all(feature = "std", feature = "parsing"))]
2299const MAX_INCLUDE_DEPTH: usize = 64;
2300
2301#[cfg(all(feature = "std", feature = "parsing"))]
2303#[derive(Debug, Clone, Default, PartialEq)]
2304pub struct FcSystemConfig {
2305 pub font_dirs: Vec<PathBuf>,
2307 pub render_configs: BTreeMap<String, FcFontRenderConfig>,
2309 pub aliases: BTreeMap<String, Vec<String>>,
2311 pub files: Vec<PathBuf>,
2313}
2314
2315#[cfg(all(feature = "std", feature = "parsing"))]
2316impl FcSystemConfig {
2317 pub fn from_system() -> Option<Self> {
2319 let root = std::env::var("FONTCONFIG_FILE")
2320 .ok()
2321 .filter(|p| !p.is_empty())
2322 .unwrap_or_else(|| "/etc/fonts/fonts.conf".to_string());
2323 let root = PathBuf::from(root);
2324 if !root.is_file() {
2325 return None;
2326 }
2327 Self::parse_tree(&root)
2328 }
2329
2330 pub fn parse_tree(root: &std::path::Path) -> Option<Self> {
2332 use std::collections::VecDeque;
2333
2334 let root_dir = root.parent().map(|d| d.to_path_buf()).unwrap_or_default();
2335 let search_dirs: Vec<PathBuf> = std::env::var_os("FONTCONFIG_PATH")
2336 .map(|v| std::env::split_paths(&v).collect::<Vec<_>>())
2337 .unwrap_or_default()
2338 .into_iter()
2339 .chain(core::iter::once(root_dir))
2340 .collect();
2341
2342 let mut config = Self::default();
2343 let mut visited: alloc::collections::BTreeSet<PathBuf> =
2344 alloc::collections::BTreeSet::new();
2345 let mut queue: VecDeque<(PathBuf, usize)> = VecDeque::new();
2346 queue.push_back((root.to_path_buf(), 0));
2347
2348 while let Some((path, depth)) = queue.pop_front() {
2349 if depth > MAX_INCLUDE_DEPTH {
2350 continue;
2351 }
2352 let identity = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
2353 if !visited.insert(identity) {
2354 continue;
2355 }
2356 let Ok(metadata) = std::fs::metadata(&path) else {
2357 continue;
2358 };
2359
2360 if metadata.is_dir() {
2361 let mut entries: Vec<PathBuf> = std::fs::read_dir(&path)
2364 .ok()?
2365 .filter_map(|entry| entry.ok().map(|e| e.path()))
2366 .filter(|p| std::fs::metadata(p).map(|m| m.is_file()).unwrap_or(false))
2367 .filter(|p| {
2368 p.file_name().map(|n| n.to_string_lossy()).is_some_and(|n| {
2369 n.starts_with(|c: char| c.is_ascii_digit()) && n.ends_with(".conf")
2370 })
2371 })
2372 .collect();
2373 entries.sort();
2374 for (i, entry) in entries.into_iter().enumerate() {
2375 queue.insert(i, (entry, depth));
2376 }
2377 continue;
2378 }
2379 if !metadata.is_file() {
2380 continue;
2381 }
2382
2383 let Ok(xml) = std::fs::read_to_string(&path) else {
2384 continue;
2385 };
2386 let mut includes: Vec<(Option<String>, PathBuf)> = Vec::new();
2387 let mut dirs: Vec<(Option<String>, String)> = Vec::new();
2388 if ParseFontsConf(&xml, &mut includes, &mut dirs).is_none() {
2389 continue;
2390 }
2391 ParseFontsConfRenderConfig(&xml, &mut config.render_configs);
2392 ParseFontsConfAliases(&xml, &mut config.aliases);
2393 config.files.push(path.clone());
2394
2395 let here = path.parent().map(|d| d.to_path_buf()).unwrap_or_default();
2396 for (prefix, dir) in dirs {
2397 let resolved = match prefix.as_deref() {
2398 Some("relative") => Some(here.join(dir)),
2399 _ => process_path(&prefix, PathBuf::from(dir), false),
2400 };
2401 if let Some(dir) = resolved {
2402 if !config.font_dirs.contains(&dir) {
2403 config.font_dirs.push(dir);
2404 }
2405 }
2406 }
2407
2408 let mut position = 0;
2411 for (prefix, include) in includes {
2412 let resolved = match prefix.as_deref() {
2413 Some("relative") => Some(here.join(include)),
2414 Some(_) => process_path(&prefix, include, true),
2415 None => process_path(&None, include, true).map(|expanded| {
2416 if expanded.is_absolute() {
2417 expanded
2418 } else {
2419 search_dirs
2420 .iter()
2421 .map(|dir| dir.join(&expanded))
2422 .find(|candidate| candidate.exists())
2423 .unwrap_or_else(|| {
2424 search_dirs
2425 .last()
2426 .map(|dir| dir.join(&expanded))
2427 .unwrap_or(expanded)
2428 })
2429 }
2430 }),
2431 };
2432 if let Some(resolved) = resolved {
2433 queue.insert(position, (resolved, depth + 1));
2434 position += 1;
2435 }
2436 }
2437 }
2438
2439 if config.files.is_empty() {
2441 return None;
2442 }
2443 Some(config)
2444 }
2445}
2446
2447#[cfg(all(feature = "std", feature = "parsing"))]
2449fn ParseFontsConfAliases(input: &str, aliases: &mut BTreeMap<String, Vec<String>>) {
2450 use xmlparser::Token::*;
2451 use xmlparser::Tokenizer;
2452
2453 #[derive(Clone, Copy, PartialEq)]
2454 enum State {
2455 Idle,
2456 InAlias,
2457 InAliasFamily,
2458 InPrefer,
2459 InPreferFamily,
2460 }
2461
2462 let mut state = State::Idle;
2463 let mut alias_key: Option<String> = None;
2464 let mut preferred: Vec<String> = Vec::new();
2465 let mut text_buf = String::new();
2466
2467 for token_result in Tokenizer::from(input) {
2468 let token = match token_result {
2469 Ok(token) => token,
2470 Err(_) => continue,
2471 };
2472 match token {
2473 ElementStart { local, .. } => match local.as_str() {
2474 "alias" => {
2475 state = State::InAlias;
2476 alias_key = None;
2477 preferred.clear();
2478 }
2479 "family" if state == State::InAlias => {
2480 state = State::InAliasFamily;
2481 text_buf.clear();
2482 }
2483 "prefer" if state == State::InAlias => {
2484 state = State::InPrefer;
2485 }
2486 "family" if state == State::InPrefer => {
2487 state = State::InPreferFamily;
2488 text_buf.clear();
2489 }
2490 _ => {}
2491 },
2492 Text { text } => {
2493 if state == State::InAliasFamily || state == State::InPreferFamily {
2494 text_buf.push_str(text.as_str());
2495 }
2496 }
2497 ElementEnd { end, .. } => {
2498 use xmlparser::ElementEnd;
2499 let closed = match end {
2500 ElementEnd::Close(_, local) => Some(local.as_str().to_owned()),
2501 _ => None,
2502 };
2503 let Some(closed) = closed else { continue };
2504 match closed.as_str() {
2505 "family" => match state {
2506 State::InAliasFamily => {
2507 let t = text_buf.trim();
2508 if !t.is_empty() && alias_key.is_none() {
2509 alias_key = Some(t.to_owned());
2510 }
2511 state = State::InAlias;
2512 }
2513 State::InPreferFamily => {
2514 let t = text_buf.trim();
2515 if !t.is_empty() {
2516 preferred.push(t.to_owned());
2517 }
2518 state = State::InPrefer;
2519 }
2520 _ => {}
2521 },
2522 "prefer" if state == State::InPrefer => {
2523 state = State::InAlias;
2524 }
2525 "alias" => {
2526 if let Some(key) = alias_key.take() {
2527 if !preferred.is_empty() {
2528 let norm = crate::utils::normalize_family_name(&key);
2529 let entry = aliases.entry(norm).or_default();
2530 for fam in preferred.drain(..) {
2531 if !entry.iter().any(|e| e == &fam) {
2532 entry.push(fam);
2533 }
2534 }
2535 }
2536 }
2537 state = State::Idle;
2538 }
2539 _ => {}
2540 }
2541 }
2542 _ => {}
2543 }
2544 }
2545}
2546
2547#[cfg(all(feature = "std", feature = "parsing"))]
2549fn ParseFontsConf(
2550 input: &str,
2551 paths_to_visit: &mut Vec<(Option<String>, PathBuf)>,
2552 font_paths: &mut Vec<(Option<String>, String)>,
2553) -> Option<()> {
2554 use xmlparser::Token::*;
2555 use xmlparser::Tokenizer;
2556
2557 const TAG_INCLUDE: &str = "include";
2558 const TAG_DIR: &str = "dir";
2559 const ATTRIBUTE_PREFIX: &str = "prefix";
2560
2561 let mut current_prefix: Option<&str> = None;
2562 let mut current_path: Option<&str> = None;
2563 let mut is_in_include = false;
2564 let mut is_in_dir = false;
2565
2566 for token_result in Tokenizer::from(input) {
2567 let token = match token_result {
2568 Ok(token) => token,
2569 Err(_) => return None,
2570 };
2571
2572 match token {
2573 ElementStart { local, .. } => {
2574 if is_in_include || is_in_dir {
2575 return None; }
2577
2578 match local.as_str() {
2579 TAG_INCLUDE => {
2580 is_in_include = true;
2581 }
2582 TAG_DIR => {
2583 is_in_dir = true;
2584 }
2585 _ => continue,
2586 }
2587
2588 current_path = None;
2589 }
2590 Text { text, .. } => {
2591 let text = text.as_str().trim();
2592 if text.is_empty() {
2593 continue;
2594 }
2595 if is_in_include || is_in_dir {
2596 current_path = Some(text);
2597 }
2598 }
2599 Attribute { local, value, .. } => {
2600 if !is_in_include && !is_in_dir {
2601 continue;
2602 }
2603 if local.as_str() == ATTRIBUTE_PREFIX {
2605 current_prefix = Some(value.as_str());
2606 }
2607 }
2608 ElementEnd { end, .. } => {
2609 let end_tag = match end {
2610 xmlparser::ElementEnd::Close(_, a) => a,
2611 _ => continue,
2612 };
2613
2614 match end_tag.as_str() {
2615 TAG_INCLUDE => {
2616 if !is_in_include {
2617 continue;
2618 }
2619
2620 if let Some(current_path) = current_path.as_ref() {
2621 paths_to_visit.push((
2622 current_prefix.map(ToOwned::to_owned),
2623 PathBuf::from(*current_path),
2624 ));
2625 }
2626 }
2627 TAG_DIR => {
2628 if !is_in_dir {
2629 continue;
2630 }
2631
2632 if let Some(current_path) = current_path.as_ref() {
2633 font_paths.push((
2634 current_prefix.map(ToOwned::to_owned),
2635 (*current_path).to_owned(),
2636 ));
2637 }
2638 }
2639 _ => continue,
2640 }
2641
2642 is_in_include = false;
2643 is_in_dir = false;
2644 current_path = None;
2645 current_prefix = None;
2646 }
2647 _ => {}
2648 }
2649 }
2650
2651 Some(())
2652}
2653
2654#[cfg(all(feature = "std", feature = "parsing"))]
2656fn ParseFontsConfRenderConfig(input: &str, configs: &mut BTreeMap<String, FcFontRenderConfig>) {
2657 use xmlparser::Token::*;
2658 use xmlparser::Tokenizer;
2659
2660 #[derive(Clone, Copy, PartialEq)]
2662 enum State {
2663 Idle,
2665 InMatchFont,
2667 InTestFamily,
2669 InEdit,
2671 }
2672
2673 let mut state = State::Idle;
2674 let mut match_is_font_target = false;
2675 let mut current_family: Option<String> = None;
2676 let mut current_edit_name: Option<String> = None;
2677 let mut current_value: Option<String> = None;
2678 let mut value_tag: Option<String> = None;
2679 let mut config = FcFontRenderConfig::default();
2680 let mut in_test = false;
2681 let mut test_name: Option<String> = None;
2682
2683 for token_result in Tokenizer::from(input) {
2684 let token = match token_result {
2685 Ok(token) => token,
2686 Err(_) => continue,
2687 };
2688
2689 match token {
2690 ElementStart { local, .. } => {
2691 let tag = local.as_str();
2692 match tag {
2693 "match" => {
2694 match_is_font_target = false;
2696 current_family = None;
2697 config = FcFontRenderConfig::default();
2698 }
2699 "test" if state == State::InMatchFont => {
2700 in_test = true;
2701 test_name = None;
2702 }
2703 "edit" if state == State::InMatchFont => {
2704 current_edit_name = None;
2705 }
2706 "bool" | "double" | "const" | "string" | "int" => {
2707 if state == State::InTestFamily || state == State::InEdit {
2708 value_tag = Some(tag.to_owned());
2709 current_value = None;
2710 }
2711 }
2712 _ => {}
2713 }
2714 }
2715 Attribute { local, value, .. } => {
2716 let attr_name = local.as_str();
2717 let attr_value = value.as_str();
2718
2719 match attr_name {
2720 "target" => {
2721 if attr_value == "font" {
2722 match_is_font_target = true;
2723 }
2724 }
2725 "name" => {
2726 if in_test && state == State::InMatchFont {
2727 test_name = Some(attr_value.to_owned());
2728 } else if state == State::InMatchFont {
2729 current_edit_name = Some(attr_value.to_owned());
2730 }
2731 }
2732 _ => {}
2733 }
2734 }
2735 Text { text, .. } => {
2736 let text = text.as_str().trim();
2737 if !text.is_empty() && (state == State::InTestFamily || state == State::InEdit) {
2738 current_value = Some(text.to_owned());
2739 }
2740 }
2741 ElementEnd { end, .. } => {
2742 match end {
2743 xmlparser::ElementEnd::Open => {
2744 if match_is_font_target && state == State::Idle {
2746 state = State::InMatchFont;
2747 match_is_font_target = false;
2748 } else if in_test {
2749 if test_name.as_deref() == Some("family") {
2750 state = State::InTestFamily;
2751 }
2752 in_test = false;
2753 } else if current_edit_name.is_some() && state == State::InMatchFont {
2754 state = State::InEdit;
2755 }
2756 }
2757 xmlparser::ElementEnd::Close(_, local) => {
2758 let tag = local.as_str();
2759 match tag {
2760 "match" => {
2761 if let Some(family) = current_family.take() {
2763 let empty = FcFontRenderConfig::default();
2764 if config != empty {
2765 configs.insert(family, config.clone());
2766 }
2767 }
2768 state = State::Idle;
2769 config = FcFontRenderConfig::default();
2770 }
2771 "test" => {
2772 if state == State::InTestFamily {
2773 if let Some(ref val) = current_value {
2775 current_family = Some(val.clone());
2776 }
2777 state = State::InMatchFont;
2778 }
2779 current_value = None;
2780 value_tag = None;
2781 }
2782 "edit" => {
2783 if state == State::InEdit {
2784 if let (Some(ref name), Some(ref val)) =
2786 (¤t_edit_name, ¤t_value)
2787 {
2788 apply_edit_value(
2789 &mut config,
2790 name,
2791 val,
2792 value_tag.as_deref(),
2793 );
2794 }
2795 state = State::InMatchFont;
2796 }
2797 current_edit_name = None;
2798 current_value = None;
2799 value_tag = None;
2800 }
2801 "bool" | "double" | "const" | "string" | "int" => {
2802 }
2804 _ => {}
2805 }
2806 }
2807 xmlparser::ElementEnd::Empty => {
2808 }
2810 }
2811 }
2812 _ => {}
2813 }
2814 }
2815}
2816
2817#[cfg(all(feature = "std", feature = "parsing"))]
2819fn apply_edit_value(
2820 config: &mut FcFontRenderConfig,
2821 edit_name: &str,
2822 value: &str,
2823 _value_tag: Option<&str>,
2824) {
2825 match edit_name {
2826 "antialias" => {
2827 config.antialias = parse_bool_value(value);
2828 }
2829 "hinting" => {
2830 config.hinting = parse_bool_value(value);
2831 }
2832 "autohint" => {
2833 config.autohint = parse_bool_value(value);
2834 }
2835 "embeddedbitmap" => {
2836 config.embeddedbitmap = parse_bool_value(value);
2837 }
2838 "embolden" => {
2839 config.embolden = parse_bool_value(value);
2840 }
2841 "minspace" => {
2842 config.minspace = parse_bool_value(value);
2843 }
2844 "hintstyle" => {
2845 config.hintstyle = parse_hintstyle_const(value);
2846 }
2847 "rgba" => {
2848 config.rgba = parse_rgba_const(value);
2849 }
2850 "lcdfilter" => {
2851 config.lcdfilter = parse_lcdfilter_const(value);
2852 }
2853 "dpi" => {
2854 if let Ok(v) = value.parse::<f64>() {
2855 config.dpi = Some(v);
2856 }
2857 }
2858 "scale" => {
2859 if let Ok(v) = value.parse::<f64>() {
2860 config.scale = Some(v);
2861 }
2862 }
2863 _ => {
2864 }
2866 }
2867}
2868
2869#[cfg(all(feature = "std", feature = "parsing"))]
2870fn parse_bool_value(value: &str) -> Option<bool> {
2871 match value {
2872 "true" => Some(true),
2873 "false" => Some(false),
2874 _ => None,
2875 }
2876}
2877
2878#[cfg(all(feature = "std", feature = "parsing"))]
2879fn parse_hintstyle_const(value: &str) -> Option<FcHintStyle> {
2880 match value {
2881 "hintnone" => Some(FcHintStyle::None),
2882 "hintslight" => Some(FcHintStyle::Slight),
2883 "hintmedium" => Some(FcHintStyle::Medium),
2884 "hintfull" => Some(FcHintStyle::Full),
2885 _ => None,
2886 }
2887}
2888
2889#[cfg(all(feature = "std", feature = "parsing"))]
2890fn parse_rgba_const(value: &str) -> Option<FcRgba> {
2891 match value {
2892 "unknown" => Some(FcRgba::Unknown),
2893 "rgb" => Some(FcRgba::Rgb),
2894 "bgr" => Some(FcRgba::Bgr),
2895 "vrgb" => Some(FcRgba::Vrgb),
2896 "vbgr" => Some(FcRgba::Vbgr),
2897 "none" => Some(FcRgba::None),
2898 _ => None,
2899 }
2900}
2901
2902#[cfg(all(feature = "std", feature = "parsing"))]
2903fn parse_lcdfilter_const(value: &str) -> Option<FcLcdFilter> {
2904 match value {
2905 "lcdnone" => Some(FcLcdFilter::None),
2906 "lcddefault" => Some(FcLcdFilter::Default),
2907 "lcdlight" => Some(FcLcdFilter::Light),
2908 "lcdlegacy" => Some(FcLcdFilter::Legacy),
2909 _ => None,
2910 }
2911}
2912
2913#[cfg(all(feature = "std", feature = "parsing"))]
2915struct ParsedFontFace {
2916 pattern: FcPattern,
2917 font_index: usize,
2918}
2919
2920#[cfg(all(feature = "std", feature = "parsing"))]
2922fn parse_font_faces(font_bytes: &[u8]) -> Option<Vec<ParsedFontFace>> {
2923 use allsorts::{
2924 binary::read::ReadScope,
2925 font_data::FontData,
2926 get_name::fontcode_get_name,
2927 post::PostTable,
2928 tables::{os2::Os2, HeadTable, NameTable},
2929 tag,
2930 };
2931 use std::collections::BTreeSet;
2932
2933 const FONT_SPECIFIER_NAME_ID: u16 = 4;
2934 const FONT_SPECIFIER_FAMILY_ID: u16 = 1;
2935
2936 let max_fonts = if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
2937 let num_fonts =
2939 u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
2940 std::cmp::min(num_fonts as usize, 100)
2942 } else {
2943 1
2945 };
2946
2947 let scope = ReadScope::new(font_bytes);
2948 let font_file = scope.read::<FontData<'_>>().ok()?;
2949
2950 let mut results = Vec::new();
2952
2953 for font_index in 0..max_fonts {
2954 let provider = font_file.table_provider(font_index).ok()?;
2955 let head_data = provider.table_data(tag::HEAD).ok()??.into_owned();
2956 let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
2957
2958 let is_bold = head_table.is_bold();
2959 let is_italic = head_table.is_italic();
2960 let mut detected_monospace = None;
2961
2962 let post_data = provider.table_data(tag::POST).ok()??;
2963 if let Ok(post_table) = ReadScope::new(&post_data).read::<PostTable>() {
2964 detected_monospace = Some(post_table.header.is_fixed_pitch != 0);
2966 }
2967
2968 let os2_data = provider.table_data(tag::OS_2).ok().flatten();
2970 let os2_table = os2_data
2971 .as_deref()
2972 .and_then(|data| ReadScope::new(data).read_dep::<Os2>(data.len()).ok());
2973
2974 let is_oblique = os2_table.as_ref().is_some_and(|os2| {
2976 os2.fs_selection
2977 .contains(allsorts::tables::os2::FsSelectionFlag::OBLIQUE)
2978 });
2979 let weight = os2_table.as_ref().map_or(
2982 if is_bold {
2983 FcWeight::Bold
2984 } else {
2985 FcWeight::Normal
2986 },
2987 |os2| FcWeight::from_u16(os2.us_weight_class),
2988 );
2989 let stretch = os2_table.as_ref().map_or(FcStretch::Normal, |os2| {
2990 FcStretch::from_u16(os2.us_width_class)
2991 });
2992
2993 let unicode_ranges = cmap_coverage(&provider).unwrap_or_default();
2996
2997 let is_monospace =
2999 detect_monospace(&provider, os2_table.as_ref(), detected_monospace).unwrap_or(false);
3000
3001 let name_data = provider.table_data(tag::NAME).ok()??.into_owned();
3002 let name_table = ReadScope::new(&name_data).read::<NameTable>().ok()?;
3003
3004 let mut metadata = FcFontMetadata::default();
3006
3007 const NAME_ID_COPYRIGHT: u16 = 0;
3008 const NAME_ID_FAMILY: u16 = 1;
3009 const NAME_ID_SUBFAMILY: u16 = 2;
3010 const NAME_ID_UNIQUE_ID: u16 = 3;
3011 const NAME_ID_FULL_NAME: u16 = 4;
3012 const NAME_ID_VERSION: u16 = 5;
3013 const NAME_ID_POSTSCRIPT_NAME: u16 = 6;
3014 const NAME_ID_TRADEMARK: u16 = 7;
3015 const NAME_ID_MANUFACTURER: u16 = 8;
3016 const NAME_ID_DESIGNER: u16 = 9;
3017 const NAME_ID_DESCRIPTION: u16 = 10;
3018 const NAME_ID_VENDOR_URL: u16 = 11;
3019 const NAME_ID_DESIGNER_URL: u16 = 12;
3020 const NAME_ID_LICENSE: u16 = 13;
3021 const NAME_ID_LICENSE_URL: u16 = 14;
3022 const NAME_ID_PREFERRED_FAMILY: u16 = 16;
3023 const NAME_ID_PREFERRED_SUBFAMILY: u16 = 17;
3024
3025 metadata.copyright = get_name_string(&name_data, NAME_ID_COPYRIGHT);
3026 metadata.font_family = get_name_string(&name_data, NAME_ID_FAMILY);
3027 metadata.font_subfamily = get_name_string(&name_data, NAME_ID_SUBFAMILY);
3028 metadata.full_name = get_name_string(&name_data, NAME_ID_FULL_NAME);
3029 metadata.unique_id = get_name_string(&name_data, NAME_ID_UNIQUE_ID);
3030 metadata.version = get_name_string(&name_data, NAME_ID_VERSION);
3031 metadata.postscript_name = get_name_string(&name_data, NAME_ID_POSTSCRIPT_NAME);
3032 metadata.trademark = get_name_string(&name_data, NAME_ID_TRADEMARK);
3033 metadata.manufacturer = get_name_string(&name_data, NAME_ID_MANUFACTURER);
3034 metadata.designer = get_name_string(&name_data, NAME_ID_DESIGNER);
3035 metadata.id_description = get_name_string(&name_data, NAME_ID_DESCRIPTION);
3036 metadata.designer_url = get_name_string(&name_data, NAME_ID_DESIGNER_URL);
3037 metadata.manufacturer_url = get_name_string(&name_data, NAME_ID_VENDOR_URL);
3038 metadata.license = get_name_string(&name_data, NAME_ID_LICENSE);
3039 metadata.license_url = get_name_string(&name_data, NAME_ID_LICENSE_URL);
3040 metadata.preferred_family = get_name_string(&name_data, NAME_ID_PREFERRED_FAMILY);
3041 metadata.preferred_subfamily = get_name_string(&name_data, NAME_ID_PREFERRED_SUBFAMILY);
3042
3043 let mut f_family = None;
3045
3046 let patterns = name_table
3047 .name_records
3048 .iter()
3049 .filter_map(|name_record| {
3050 let name_id = name_record.name_id;
3051 if name_id == FONT_SPECIFIER_FAMILY_ID {
3052 if let Ok(Some(family)) =
3053 fontcode_get_name(&name_data, FONT_SPECIFIER_FAMILY_ID)
3054 {
3055 f_family = Some(family);
3056 }
3057 None
3058 } else if name_id == FONT_SPECIFIER_NAME_ID {
3059 let family = f_family.as_ref()?;
3060 let name = fontcode_get_name(&name_data, FONT_SPECIFIER_NAME_ID).ok()??;
3061 if name.to_bytes().is_empty() {
3062 None
3063 } else {
3064 let mut name_str = String::from_utf8_lossy(name.to_bytes()).to_string();
3065 let mut family_str = String::from_utf8_lossy(family.as_bytes()).to_string();
3066 if name_str.starts_with('.') {
3067 name_str = name_str[1..].to_string();
3068 }
3069 if family_str.starts_with('.') {
3070 family_str = family_str[1..].to_string();
3071 }
3072 Some((
3073 FcPattern {
3074 name: Some(name_str),
3075 family: Some(family_str),
3076 bold: if is_bold {
3077 PatternMatch::True
3078 } else {
3079 PatternMatch::False
3080 },
3081 italic: if is_italic {
3082 PatternMatch::True
3083 } else {
3084 PatternMatch::False
3085 },
3086 oblique: if is_oblique {
3087 PatternMatch::True
3088 } else {
3089 PatternMatch::False
3090 },
3091 monospace: if is_monospace {
3092 PatternMatch::True
3093 } else {
3094 PatternMatch::False
3095 },
3096 condensed: if stretch <= FcStretch::Condensed {
3097 PatternMatch::True
3098 } else {
3099 PatternMatch::False
3100 },
3101 weight,
3102 stretch,
3103 unicode_ranges: unicode_ranges.clone(),
3104 metadata: metadata.clone(),
3105 render_config: FcFontRenderConfig::default(),
3106 },
3107 font_index,
3108 ))
3109 }
3110 } else {
3111 None
3112 }
3113 })
3114 .collect::<BTreeSet<_>>();
3115
3116 results.extend(patterns.into_iter().map(|(pat, idx)| ParsedFontFace {
3117 pattern: pat,
3118 font_index: idx,
3119 }));
3120 }
3121
3122 if results.is_empty() {
3123 None
3124 } else {
3125 Some(results)
3126 }
3127}
3128
3129#[cfg(all(feature = "std", feature = "parsing"))]
3131pub(crate) fn FcParseFont(filepath: &PathBuf) -> Option<Vec<(FcPattern, FcFontPath)>> {
3132 #[cfg(all(not(target_family = "wasm"), feature = "std"))]
3133 use mmapio::MmapOptions;
3134 use std::fs::File;
3135
3136 let file = File::open(filepath).ok()?;
3138
3139 #[cfg(all(not(target_family = "wasm"), feature = "std"))]
3140 let font_bytes = unsafe { MmapOptions::new().map(&file).ok()? };
3141
3142 #[cfg(not(all(not(target_family = "wasm"), feature = "std")))]
3143 let font_bytes = std::fs::read(filepath).ok()?;
3144
3145 let faces = parse_font_faces(&font_bytes[..])?;
3146 let path_str = filepath.to_string_lossy().to_string();
3147 let bytes_hash = crate::utils::content_dedup_hash_u64(&font_bytes[..]);
3149
3150 Some(
3151 faces
3152 .into_iter()
3153 .map(|face| {
3154 (
3155 face.pattern,
3156 FcFontPath {
3157 path: path_str.clone(),
3158 font_index: face.font_index,
3159 bytes_hash,
3160 },
3161 )
3162 })
3163 .collect(),
3164 )
3165}
3166
3167#[cfg(all(feature = "std", feature = "parsing"))]
3169#[derive(Debug, Clone)]
3170pub struct FastCoverage {
3171 pub pattern: FcPattern,
3173 pub covered: alloc::collections::BTreeSet<char>,
3175 pub is_bold: bool,
3177 pub is_italic: bool,
3179}
3180
3181#[cfg(all(feature = "std", feature = "parsing"))]
3183#[allow(non_snake_case)]
3184pub fn FcParseFontFaceFast(
3185 font_bytes: &[u8],
3186 font_index: usize,
3187 codepoints: &alloc::collections::BTreeSet<char>,
3188) -> Option<FastCoverage> {
3189 use allsorts::{
3190 binary::read::ReadScope,
3191 font_data::FontData,
3192 tables::{
3193 cmap::{Cmap, CmapSubtable},
3194 FontTableProvider, HeadTable,
3195 },
3196 tag,
3197 };
3198
3199 let scope = ReadScope::new(font_bytes);
3200 let font_file = scope.read::<FontData<'_>>().ok()?;
3201 let provider = font_file.table_provider(font_index).ok()?;
3202
3203 let head_data = provider.table_data(tag::HEAD).ok()??;
3205 let head_table = ReadScope::new(&head_data).read::<HeadTable>().ok()?;
3206 let is_bold = head_table.is_bold();
3207 let is_italic = head_table.is_italic();
3208
3209 let cmap_data = provider.table_data(tag::CMAP).ok()??;
3212 let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
3213 let encoding_record = find_best_cmap_subtable(&cmap)?;
3214 let cmap_subtable = ReadScope::new(&cmap_data)
3215 .offset(encoding_record.offset as usize)
3216 .read::<CmapSubtable<'_>>()
3217 .ok()?;
3218
3219 let mut covered: alloc::collections::BTreeSet<char> = alloc::collections::BTreeSet::new();
3220 for ch in codepoints {
3221 if matches!(cmap_subtable.map_glyph(*ch as u32), Ok(Some(gid)) if gid != 0) {
3222 covered.insert(*ch);
3223 }
3224 }
3225 let covered_ranges =
3227 coverage_from_subtable(&cmap_subtable, &cmap_data, encoding_record.offset as usize)
3228 .unwrap_or_default();
3229
3230 let weight = if is_bold {
3231 FcWeight::Bold
3232 } else {
3233 FcWeight::Normal
3234 };
3235 let italic_match = if is_italic {
3236 PatternMatch::True
3237 } else {
3238 PatternMatch::False
3239 };
3240
3241 let pattern = FcPattern {
3242 name: None,
3243 family: None,
3244 weight,
3245 italic: italic_match,
3246 oblique: PatternMatch::DontCare,
3247 monospace: PatternMatch::DontCare,
3248 unicode_ranges: covered_ranges,
3249 ..Default::default()
3250 };
3251
3252 Some(FastCoverage {
3253 pattern,
3254 covered,
3255 is_bold,
3256 is_italic,
3257 })
3258}
3259
3260#[cfg(all(feature = "std", feature = "parsing"))]
3262#[allow(non_snake_case)]
3263pub fn FcCountFontFaces(font_bytes: &[u8]) -> usize {
3264 if font_bytes.len() >= 12 && &font_bytes[0..4] == b"ttcf" {
3265 let num_fonts =
3266 u32::from_be_bytes([font_bytes[8], font_bytes[9], font_bytes[10], font_bytes[11]]);
3267 std::cmp::min(num_fonts as usize, 100).max(1)
3269 } else {
3270 1
3271 }
3272}
3273
3274#[cfg(all(feature = "std", feature = "parsing"))]
3276#[allow(non_snake_case)]
3277pub fn FcParseFontBytes(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
3278 FcParseFontBytesInner(font_bytes, font_id)
3279}
3280
3281#[cfg(all(feature = "std", feature = "parsing"))]
3283fn FcParseFontBytesInner(font_bytes: &[u8], font_id: &str) -> Option<Vec<(FcPattern, FcFont)>> {
3284 let faces = parse_font_faces(font_bytes)?;
3285 let id = font_id.to_string();
3286 let bytes = font_bytes.to_vec();
3287
3288 Some(
3289 faces
3290 .into_iter()
3291 .map(|face| {
3292 (
3293 face.pattern,
3294 FcFont {
3295 bytes: bytes.clone(),
3296 font_index: face.font_index,
3297 id: id.clone(),
3298 },
3299 )
3300 })
3301 .collect(),
3302 )
3303}
3304
3305#[cfg(all(feature = "std", feature = "parsing"))]
3306fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern, FcFontPath)> {
3307 #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
3308 {
3309 use rayon::prelude::*;
3310
3311 paths
3313 .par_iter()
3314 .filter_map(|(prefix, p)| {
3315 process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
3316 })
3317 .flatten()
3318 .collect()
3319 }
3320 #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
3323 {
3324 paths
3325 .iter()
3326 .filter_map(|(prefix, p)| {
3327 process_path(prefix, PathBuf::from(p), false).map(FcScanSingleDirectoryRecursive)
3328 })
3329 .flatten()
3330 .collect()
3331 }
3332}
3333
3334#[cfg(feature = "std")]
3336#[allow(non_snake_case)]
3337fn FcCollectFontFilesRecursive(dir: PathBuf) -> Vec<PathBuf> {
3338 crate::utils::collect_font_files(&dir)
3339}
3340
3341#[cfg(all(feature = "std", feature = "parsing"))]
3342fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
3343 let files = FcCollectFontFilesRecursive(dir);
3344 FcParseFontFiles(&files)
3345}
3346
3347#[cfg(all(feature = "std", feature = "parsing"))]
3348fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)> {
3349 let result = {
3350 #[cfg(all(feature = "multithreading", not(target_family = "wasm")))]
3351 {
3352 use rayon::prelude::*;
3353
3354 files_to_parse
3355 .par_iter()
3356 .filter_map(|file| FcParseFont(file))
3357 .collect::<Vec<Vec<_>>>()
3358 }
3359 #[cfg(not(all(feature = "multithreading", not(target_family = "wasm"))))]
3360 {
3361 files_to_parse
3362 .iter()
3363 .filter_map(|file| FcParseFont(file))
3364 .collect::<Vec<Vec<_>>>()
3365 }
3366 };
3367
3368 result.into_iter().flat_map(|f| f.into_iter()).collect()
3369}
3370
3371#[cfg(all(feature = "std", feature = "parsing"))]
3372fn process_path(
3374 prefix: &Option<String>,
3375 mut path: PathBuf,
3376 is_include_path: bool,
3377) -> Option<PathBuf> {
3378 use std::env::var;
3379
3380 const HOME_SHORTCUT: &str = "~";
3381 const CWD_PATH: &str = ".";
3382
3383 const HOME_ENV_VAR: &str = "HOME";
3384 const XDG_CONFIG_HOME_ENV_VAR: &str = "XDG_CONFIG_HOME";
3385 const XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX: &str = ".config";
3386 const XDG_DATA_HOME_ENV_VAR: &str = "XDG_DATA_HOME";
3387 const XDG_DATA_HOME_DEFAULT_PATH_SUFFIX: &str = ".local/share";
3388
3389 const PREFIX_CWD: &str = "cwd";
3390 const PREFIX_DEFAULT: &str = "default";
3391 const PREFIX_XDG: &str = "xdg";
3392
3393 fn get_home_value() -> Option<PathBuf> {
3395 var(HOME_ENV_VAR).ok().map(PathBuf::from)
3396 }
3397 fn get_xdg_config_home_value() -> Option<PathBuf> {
3398 var(XDG_CONFIG_HOME_ENV_VAR)
3399 .ok()
3400 .map(PathBuf::from)
3401 .or_else(|| {
3402 get_home_value()
3403 .map(|home_path| home_path.join(XDG_CONFIG_HOME_DEFAULT_PATH_SUFFIX))
3404 })
3405 }
3406 fn get_xdg_data_home_value() -> Option<PathBuf> {
3407 var(XDG_DATA_HOME_ENV_VAR)
3408 .ok()
3409 .map(PathBuf::from)
3410 .or_else(|| {
3411 get_home_value().map(|home_path| home_path.join(XDG_DATA_HOME_DEFAULT_PATH_SUFFIX))
3412 })
3413 }
3414
3415 if path.starts_with(HOME_SHORTCUT) {
3417 if let Some(home_path) = get_home_value() {
3418 path = home_path.join(
3419 path.strip_prefix(HOME_SHORTCUT)
3420 .expect("already checked that it starts with the prefix"),
3421 );
3422 } else {
3423 return None;
3424 }
3425 }
3426
3427 match prefix {
3429 Some(prefix) => match prefix.as_str() {
3430 PREFIX_CWD | PREFIX_DEFAULT => {
3431 let mut new_path = PathBuf::from(CWD_PATH);
3432 new_path.push(path);
3433
3434 Some(new_path)
3435 }
3436 PREFIX_XDG => {
3437 if is_include_path {
3438 get_xdg_config_home_value()
3439 .map(|xdg_config_home_path| xdg_config_home_path.join(path))
3440 } else {
3441 get_xdg_data_home_value()
3442 .map(|xdg_data_home_path| xdg_data_home_path.join(path))
3443 }
3444 }
3445 _ => None, },
3447 None => Some(path),
3448 }
3449}
3450
3451#[cfg(all(feature = "std", feature = "parsing"))]
3453fn get_name_string(name_data: &[u8], name_id: u16) -> Option<String> {
3454 fontcode_get_name(name_data, name_id)
3455 .ok()
3456 .flatten()
3457 .map(|name| String::from_utf8_lossy(name.to_bytes()).to_string())
3458}
3459
3460#[cfg(all(feature = "std", feature = "parsing"))]
3462fn find_best_cmap_subtable<'a>(
3463 cmap: &allsorts::tables::cmap::Cmap<'a>,
3464) -> Option<allsorts::tables::cmap::EncodingRecord> {
3465 use allsorts::tables::cmap::{EncodingId, PlatformId};
3466
3467 cmap.find_subtable(PlatformId::UNICODE, EncodingId(4))
3469 .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(10)))
3470 .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(3)))
3471 .or_else(|| cmap.find_subtable(PlatformId::WINDOWS, EncodingId(1)))
3472 .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(0)))
3473 .or_else(|| cmap.find_subtable(PlatformId::UNICODE, EncodingId(1)))
3474}
3475
3476#[cfg(all(feature = "std", feature = "parsing"))]
3478fn cmap_coverage(provider: &impl FontTableProvider) -> Option<Vec<UnicodeRange>> {
3479 use allsorts::binary::read::ReadScope;
3480 use allsorts::tables::cmap::{Cmap, CmapSubtable};
3481
3482 let cmap_data = provider.table_data(tag::CMAP).ok()??;
3483 let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
3484 let record = find_best_cmap_subtable(&cmap)?;
3485 let subtable = ReadScope::new(&cmap_data)
3486 .offset(record.offset as usize)
3487 .read::<CmapSubtable<'_>>()
3488 .ok()?;
3489 coverage_from_subtable(&subtable, &cmap_data, record.offset as usize)
3490}
3491
3492#[cfg(all(feature = "std", feature = "parsing"))]
3494fn coverage_from_subtable(
3495 subtable: &allsorts::tables::cmap::CmapSubtable<'_>,
3496 cmap_data: &[u8],
3497 offset: usize,
3498) -> Option<Vec<UnicodeRange>> {
3499 use allsorts::tables::cmap::CmapSubtable;
3500
3501 let mut ranges: Vec<UnicodeRange> = Vec::new();
3502 let mut push = |start: u32, end: u32| {
3503 if start > end {
3504 return;
3505 }
3506 match ranges.last_mut() {
3507 Some(last) if start <= last.end.saturating_add(1) => last.end = last.end.max(end),
3508 _ => ranges.push(UnicodeRange { start, end }),
3509 }
3510 };
3511
3512 match subtable {
3513 CmapSubtable::Format4(f4) => {
3514 let seg_count = f4.start_codes.len();
3515 let glyph_ids: Vec<u16> = f4.glyph_id_array.iter().collect();
3516 let segments = f4
3517 .start_codes
3518 .iter()
3519 .zip(f4.end_codes.iter())
3520 .zip(f4.id_deltas.iter())
3521 .zip(f4.id_range_offsets.iter())
3522 .enumerate();
3523 for (i, (((start, end), delta), range_offset)) in segments {
3524 if start == 0xFFFF {
3525 continue; }
3527 let (start, end) = (start as u32, end as u32);
3528 if range_offset == 0 {
3529 let zero_code = (delta as u16).wrapping_neg() as u32;
3532 if zero_code >= start && zero_code <= end {
3533 if zero_code > start {
3534 push(start, zero_code - 1);
3535 }
3536 if zero_code < end {
3537 push(zero_code + 1, end);
3538 }
3539 } else {
3540 push(start, end);
3541 }
3542 } else {
3543 let base = (range_offset as usize / 2).wrapping_sub(seg_count - i);
3545 for code in start..=end {
3546 let index = base.wrapping_add((code - start) as usize);
3547 let Some(&value) = glyph_ids.get(index) else {
3548 continue;
3549 };
3550 if value != 0 && value.wrapping_add(delta as u16) != 0 {
3551 push(code, code);
3552 }
3553 }
3554 }
3555 }
3556 }
3557 CmapSubtable::Format12 { .. } => {
3558 for (start, end, start_gid) in format12_groups(cmap_data, offset)? {
3559 let first = if start_gid == 0 {
3562 start.saturating_add(1)
3563 } else {
3564 start
3565 };
3566 push(first, end.min(0x10FFFF));
3567 }
3568 }
3569 CmapSubtable::Format0 { glyph_id_array, .. } => {
3570 for (code, gid) in glyph_id_array.iter().enumerate() {
3571 if gid != 0 {
3572 push(code as u32, code as u32);
3573 }
3574 }
3575 }
3576 CmapSubtable::Format6 {
3577 first_code,
3578 glyph_id_array,
3579 ..
3580 } => {
3581 for (i, gid) in glyph_id_array.iter().enumerate() {
3582 if gid != 0 {
3583 let code = *first_code as u32 + i as u32;
3584 push(code, code);
3585 }
3586 }
3587 }
3588 CmapSubtable::Format10 {
3589 start_char_code,
3590 glyph_id_array,
3591 ..
3592 } => {
3593 for (i, gid) in glyph_id_array.iter().enumerate() {
3594 if gid != 0 {
3595 let code = *start_char_code + i as u32;
3596 push(code, code);
3597 }
3598 }
3599 }
3600 CmapSubtable::Format2 { .. } => {
3601 let mut codes: Vec<u32> = Vec::new();
3604 subtable
3605 .mappings_fn(|code, gid| {
3606 if gid != 0 {
3607 codes.push(code);
3608 }
3609 })
3610 .ok()?;
3611 codes.sort_unstable();
3612 for code in codes {
3613 push(code, code);
3614 }
3615 }
3616 }
3617
3618 if ranges.is_empty() {
3619 None
3620 } else {
3621 Some(FcFontCache::normalize_unicode_ranges(ranges))
3622 }
3623}
3624
3625#[cfg(all(feature = "std", feature = "parsing"))]
3627fn format12_groups(cmap_data: &[u8], offset: usize) -> Option<Vec<(u32, u32, u32)>> {
3628 let table = cmap_data.get(offset..)?;
3629 let u16_at = |at: usize| {
3630 table
3631 .get(at..at + 2)
3632 .map(|b| u16::from_be_bytes([b[0], b[1]]))
3633 };
3634 let u32_at = |at: usize| {
3635 table
3636 .get(at..at + 4)
3637 .map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
3638 };
3639 if u16_at(0)? != 12 {
3640 return None;
3641 }
3642 let num_groups = u32_at(12)? as usize;
3643 let mut groups = Vec::with_capacity(num_groups.min(1 << 16));
3644 for i in 0..num_groups {
3645 let at = 16 + i * 12;
3646 groups.push((u32_at(at)?, u32_at(at + 4)?, u32_at(at + 8)?));
3647 }
3648 Some(groups)
3649}
3650
3651#[cfg(all(feature = "std", feature = "parsing"))]
3653fn detect_monospace(
3654 provider: &impl FontTableProvider,
3655 os2_table: Option<&Os2>,
3656 detected_monospace: Option<bool>,
3657) -> Option<bool> {
3658 if let Some(is_monospace) = detected_monospace {
3659 return Some(is_monospace);
3660 }
3661
3662 if let Some(os2_table) = os2_table {
3665 if os2_table.panose[0] == 2 {
3666 return Some(os2_table.panose[3] == 9); }
3669 }
3670
3671 let hhea_data = provider.table_data(tag::HHEA).ok()??;
3673 let hhea_table = ReadScope::new(&hhea_data).read::<HheaTable>().ok()?;
3674 let maxp_data = provider.table_data(tag::MAXP).ok()??;
3675 let maxp_table = ReadScope::new(&maxp_data).read::<MaxpTable>().ok()?;
3676 let hmtx_data = provider.table_data(tag::HMTX).ok()??;
3677 let hmtx_table = ReadScope::new(&hmtx_data)
3678 .read_dep::<HmtxTable<'_>>((
3679 usize::from(maxp_table.num_glyphs),
3680 usize::from(hhea_table.num_h_metrics),
3681 ))
3682 .ok()?;
3683
3684 let mut monospace = true;
3685 let mut last_advance = 0;
3686
3687 for i in 0..hhea_table.num_h_metrics as usize {
3689 let advance = hmtx_table.h_metrics.read_item(i).ok()?.advance_width;
3690 if i > 0 && advance != last_advance {
3691 monospace = false;
3692 break;
3693 }
3694 last_advance = advance;
3695 }
3696
3697 Some(monospace)
3698}
3699
3700#[cfg(all(feature = "std", not(feature = "parsing")))]
3702fn pattern_from_filename(path: &std::path::Path) -> Option<FcPattern> {
3703 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
3704 match ext.as_str() {
3705 "ttf" | "otf" | "ttc" | "woff" | "woff2" => {}
3706 _ => return None,
3707 }
3708
3709 let stem = path.file_stem()?.to_str()?;
3710 let all_tokens = crate::config::tokenize_lowercase(stem);
3711
3712 let has_token = |kw: &str| all_tokens.iter().any(|t| t == kw);
3714 let is_bold = has_token("bold") || has_token("heavy");
3715 let is_italic = has_token("italic");
3716 let is_oblique = has_token("oblique");
3717 let is_mono = has_token("mono") || has_token("monospace");
3718 let is_condensed = has_token("condensed");
3719
3720 let family_tokens = crate::config::tokenize_font_stem(stem);
3722 if family_tokens.is_empty() {
3723 return None;
3724 }
3725 let family = family_tokens.join(" ");
3726
3727 Some(FcPattern {
3728 name: Some(stem.to_string()),
3729 family: Some(family),
3730 bold: if is_bold {
3731 PatternMatch::True
3732 } else {
3733 PatternMatch::False
3734 },
3735 italic: if is_italic {
3736 PatternMatch::True
3737 } else {
3738 PatternMatch::False
3739 },
3740 oblique: if is_oblique {
3741 PatternMatch::True
3742 } else {
3743 PatternMatch::DontCare
3744 },
3745 monospace: if is_mono {
3746 PatternMatch::True
3747 } else {
3748 PatternMatch::DontCare
3749 },
3750 condensed: if is_condensed {
3751 PatternMatch::True
3752 } else {
3753 PatternMatch::DontCare
3754 },
3755 weight: if is_bold {
3756 FcWeight::Bold
3757 } else {
3758 FcWeight::Normal
3759 },
3760 stretch: if is_condensed {
3761 FcStretch::Condensed
3762 } else {
3763 FcStretch::Normal
3764 },
3765 unicode_ranges: Vec::new(),
3766 metadata: FcFontMetadata::default(),
3767 render_config: FcFontRenderConfig::default(),
3768 })
3769}
3770
3771#[cfg(all(test, feature = "std", feature = "parsing"))]
3772mod system_alias_tests {
3773 use super::*;
3774
3775 const SAMPLE: &str = r#"<?xml version="1.0"?>
3776<fontconfig>
3777 <alias>
3778 <family>sans-serif</family>
3779 <prefer>
3780 <family>Noto Sans</family>
3781 <family>DejaVu Sans</family>
3782 </prefer>
3783 </alias>
3784 <alias>
3785 <family>Arial</family>
3786 <prefer><family>Liberation Sans</family></prefer>
3787 </alias>
3788 <alias binding="same">
3789 <family>monospace</family>
3790 <prefer><family>Noto Sans Mono</family></prefer>
3791 </alias>
3792</fontconfig>"#;
3793
3794 const SECOND_FILE: &str = r#"<fontconfig>
3795 <alias>
3796 <family>sans-serif</family>
3797 <prefer>
3798 <family>Ubuntu</family>
3799 <family>Noto Sans</family>
3800 </prefer>
3801 </alias>
3802</fontconfig>"#;
3803
3804 #[test]
3805 fn alias_blocks_parse_with_order_and_dedup_across_files() {
3806 let mut aliases = BTreeMap::new();
3807 ParseFontsConfAliases(SAMPLE, &mut aliases);
3808 ParseFontsConfAliases(SECOND_FILE, &mut aliases);
3809 let key = crate::utils::normalize_family_name("sans-serif");
3810 assert_eq!(
3811 aliases.get(&key).map(Vec::as_slice),
3812 Some(
3813 &[
3814 "Noto Sans".to_string(),
3815 "DejaVu Sans".to_string(),
3816 "Ubuntu".to_string()
3817 ][..]
3818 ),
3819 "prefer entries append across files in include order, deduplicated"
3820 );
3821 assert_eq!(
3822 aliases.get("arial").map(Vec::as_slice),
3823 Some(&["Liberation Sans".to_string()][..]),
3824 "named-family aliases parse too (key normalized)"
3825 );
3826 assert_eq!(
3827 aliases.get("monospace").map(Vec::as_slice),
3828 Some(&["Noto Sans Mono".to_string()][..]),
3829 "alias attributes (binding=...) do not confuse the parser"
3830 );
3831 }
3832
3833 #[test]
3834 fn config_first_expansion_beats_the_builtin_lists() {
3835 let mut aliases = BTreeMap::new();
3838 ParseFontsConfAliases(SAMPLE, &mut aliases);
3839 let mut config = FcFallbackConfig::default();
3840 config.absorb_system_aliases(aliases);
3841 config.merge_defaults(&FcFallbackConfig::os_defaults(OperatingSystem::Linux));
3842
3843 let cache = FcFontCache::default().with_fallback_config(config);
3844 let out = cache
3845 .fallback_config()
3846 .candidate_families(&["Arial".to_string(), "sans-serif".to_string()], &[]);
3847 assert_eq!(
3848 out,
3849 vec![
3850 "Arial".to_string(), "Liberation Sans".to_string(), "Noto Sans".to_string(), "DejaVu Sans".to_string(),
3854 ],
3855 "configured preferences resolve the stack; no built-in list entries leak in"
3856 );
3857 }
3858
3859 #[test]
3860 fn generic_family_without_config_falls_back_to_builtin_lists() {
3861 let mut config = FcFallbackConfig::default();
3862 config.merge_defaults(&FcFallbackConfig::os_defaults(OperatingSystem::Linux));
3863 let out = config.candidate_families(&["sans-serif".to_string()], &[]);
3864 assert!(
3865 !out.is_empty() && out.iter().any(|f| f == "DejaVu Sans"),
3866 "no configuration parsed -> the built-in candidates are the last resort: {out:?}"
3867 );
3868 }
3869}
3870
3871#[cfg(all(test, feature = "std", feature = "parsing"))]
3872mod coverage_tests {
3873 use super::*;
3874 use allsorts::binary::read::ReadScope;
3875 use allsorts::font_data::FontData;
3876 use allsorts::tables::cmap::{Cmap, CmapSubtable};
3877 use allsorts::tables::FontTableProvider;
3878
3879 const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/InstrumentSerif-Regular.ttf");
3880
3881 fn brute_force(bytes: &[u8], face: usize, max: u32) -> Vec<UnicodeRange> {
3883 let font = ReadScope::new(bytes)
3884 .read::<FontData<'_>>()
3885 .expect("font data");
3886 let provider = font.table_provider(face).expect("face");
3887 let cmap_data = provider
3888 .table_data(tag::CMAP)
3889 .expect("cmap")
3890 .expect("cmap present");
3891 let cmap = ReadScope::new(&cmap_data)
3892 .read::<Cmap<'_>>()
3893 .expect("cmap header");
3894 let record = find_best_cmap_subtable(&cmap).expect("a Unicode subtable");
3895 let subtable = ReadScope::new(&cmap_data)
3896 .offset(record.offset as usize)
3897 .read::<CmapSubtable<'_>>()
3898 .expect("subtable");
3899 let mut codes: Vec<u32> = Vec::new();
3902 if matches!(subtable, CmapSubtable::Format12 { .. }) {
3903 subtable
3904 .mappings_fn(|cp, gid| {
3905 if gid != 0 && cp <= max {
3906 codes.push(cp);
3907 }
3908 })
3909 .expect("format-12 mappings");
3910 codes.sort_unstable();
3911 codes.dedup();
3912 } else {
3913 for cp in 0..=max {
3914 if (0xD800..=0xDFFF).contains(&cp) {
3915 continue;
3916 }
3917 if matches!(subtable.map_glyph(cp), Ok(Some(gid)) if gid != 0) {
3918 codes.push(cp);
3919 }
3920 }
3921 }
3922 let mut out: Vec<UnicodeRange> = Vec::new();
3923 for cp in codes {
3924 match out.last_mut() {
3925 Some(last) if last.end + 1 == cp => last.end = cp,
3926 _ => out.push(UnicodeRange { start: cp, end: cp }),
3927 }
3928 }
3929 out
3930 }
3931
3932 fn clipped(ranges: &[UnicodeRange], max: u32) -> Vec<UnicodeRange> {
3933 ranges
3934 .iter()
3935 .filter(|r| r.start <= max)
3936 .map(|r| UnicodeRange {
3937 start: r.start,
3938 end: r.end.min(max),
3939 })
3940 .collect()
3941 }
3942
3943 #[test]
3944 fn format12_groups_are_read_from_the_raw_table() {
3945 let mut table = Vec::new();
3946 table.extend_from_slice(&12u16.to_be_bytes()); table.extend_from_slice(&0u16.to_be_bytes()); table.extend_from_slice(&(16u32 + 2 * 12).to_be_bytes()); table.extend_from_slice(&0u32.to_be_bytes()); table.extend_from_slice(&2u32.to_be_bytes()); for (start, end, gid) in [(0x20u32, 0x7Eu32, 3u32), (0x1F600, 0x1F64F, 200)] {
3952 table.extend_from_slice(&start.to_be_bytes());
3953 table.extend_from_slice(&end.to_be_bytes());
3954 table.extend_from_slice(&gid.to_be_bytes());
3955 }
3956 let mut cmap = vec![0u8; 40];
3958 cmap.extend_from_slice(&table);
3959
3960 assert_eq!(
3961 format12_groups(&cmap, 40),
3962 Some(vec![(0x20, 0x7E, 3), (0x1F600, 0x1F64F, 200)])
3963 );
3964 assert_eq!(
3965 format12_groups(&cmap, 0),
3966 None,
3967 "offset 0 is not a format-12 subtable"
3968 );
3969 assert_eq!(
3970 format12_groups(&cmap[..50], 40),
3971 None,
3972 "a truncated table is rejected"
3973 );
3974 }
3975
3976 #[test]
3978 fn fixture_coverage_equals_the_cmap_exactly() {
3979 let faces = FcParseFontBytes(FIXTURE, "fixture").expect("the fixture parses");
3980 let parsed = &faces[0].0.unicode_ranges;
3981 assert!(!parsed.is_empty());
3982 assert_eq!(
3983 *parsed,
3984 FcFontCache::normalize_unicode_ranges(parsed.clone()),
3985 "stored coverage is normalized"
3986 );
3987
3988 let exact = brute_force(FIXTURE, 0, 0x10FFFF);
3989 assert_eq!(
3990 *parsed, exact,
3991 "segment walk and per-codepoint lookup disagree"
3992 );
3993
3994 assert!(crate::fallback::covers(parsed, 'A' as u32));
3996 assert!(!crate::fallback::covers(parsed, 0x4E00));
3997 let latin_ext_a = UnicodeRange {
3998 start: 0x0100,
3999 end: 0x017F,
4000 };
4001 let overlap = crate::fallback::overlap_size(parsed, &latin_ext_a);
4002 assert!(
4003 overlap > 0 && overlap < 128,
4004 "the fixture covers part of Latin Extended-A ({overlap} of 128); a block-rounded \
4005 coverage would report all or nothing"
4006 );
4007 }
4008
4009 fn with_best_subtable<R>(
4010 bytes: &[u8],
4011 face: usize,
4012 f: impl FnOnce(&CmapSubtable<'_>, &[u8], usize) -> R,
4013 ) -> Option<R> {
4014 let font = ReadScope::new(bytes).read::<FontData<'_>>().ok()?;
4015 let provider = font.table_provider(face).ok()?;
4016 let cmap_data = provider.table_data(tag::CMAP).ok()??;
4017 let cmap = ReadScope::new(&cmap_data).read::<Cmap<'_>>().ok()?;
4018 let record = find_best_cmap_subtable(&cmap)?;
4019 let subtable = ReadScope::new(&cmap_data)
4020 .offset(record.offset as usize)
4021 .read::<CmapSubtable<'_>>()
4022 .ok()?;
4023 Some(f(&subtable, &cmap_data, record.offset as usize))
4024 }
4025
4026 #[test]
4028 #[ignore]
4029 fn every_installed_font_coverage_matches_its_cmap_at_every_boundary() {
4030 fn walk(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
4031 let Ok(entries) = std::fs::read_dir(dir) else {
4032 return;
4033 };
4034 for entry in entries.flatten() {
4035 let path = entry.path();
4036 if path.is_dir() {
4037 walk(&path, out);
4038 } else if crate::utils::is_font_file(&path) {
4039 out.push(path);
4040 }
4041 }
4042 }
4043 let mut files = Vec::new();
4044 for dir in crate::config::font_directories(OperatingSystem::current()) {
4045 walk(&dir, &mut files);
4046 }
4047 if std::env::var_os("RFC_COVERAGE_CHECK_ALL").is_none() {
4049 files.truncate(400);
4050 }
4051 let surrogate = |cp: u32| (0xD800..=0xDFFF).contains(&cp);
4052 let (mut faces_checked, mut skipped) = (0usize, 0usize);
4053 for path in &files {
4054 let Ok(bytes) = std::fs::read(path) else {
4055 continue;
4056 };
4057 let Some(faces) = FcParseFontBytes(&bytes, &path.to_string_lossy()) else {
4058 skipped += 1;
4059 continue;
4060 };
4061 let mut seen_faces = alloc::collections::BTreeSet::new();
4062 for (pattern, font) in &faces {
4063 if !seen_faces.insert(font.font_index) {
4064 continue;
4065 }
4066 let where_ = format!("{}#{}", path.display(), font.font_index);
4067 let checked =
4068 with_best_subtable(&bytes, font.font_index, |subtable, cmap_data, offset| {
4069 let groups = match subtable {
4072 CmapSubtable::Format12 { .. } => format12_groups(cmap_data, offset),
4073 _ => None,
4074 };
4075 let mapped = |cp: u32| match &groups {
4076 Some(groups) => {
4077 let i = groups.partition_point(|g| g.1 < cp);
4078 groups.get(i).is_some_and(|&(start, end, gid)| {
4079 start <= cp && cp <= end && (gid != 0 || cp != start)
4080 })
4081 }
4082 None => matches!(subtable.map_glyph(cp), Ok(Some(gid)) if gid != 0),
4083 };
4084 for r in &pattern.unicode_ranges {
4085 assert!(
4086 mapped(r.start) && mapped(r.end),
4087 "{where_}: {r:?} does not end on mapped codepoints"
4088 );
4089 if r.start > 0 && !surrogate(r.start - 1) {
4090 assert!(!mapped(r.start - 1), "{where_}: {r:?} starts late");
4091 }
4092 if r.end < 0x10FFFF && !surrogate(r.end + 1) {
4093 assert!(!mapped(r.end + 1), "{where_}: {r:?} ends early");
4094 }
4095 }
4096 });
4097 if checked.is_some() {
4098 faces_checked += 1;
4099 }
4100 }
4101 }
4102 println!(
4103 "checked {faces_checked} faces in {} files ({skipped} unparsable)",
4104 files.len()
4105 );
4106 assert!(faces_checked > 0, "no fonts found to check");
4107 }
4108}