Skip to main content

fret_render_text/
parley_font_db.rs

1use fret_core::{TextSlant, TextStyle};
2use parley::FontContext;
3use parley::fontique::{FamilyId, GenericFamily};
4use read_fonts::{FontRef, TableProvider as _};
5use std::collections::{HashMap, VecDeque};
6use std::hash::{Hash as _, Hasher as _};
7
8use crate::FontCatalogEntryMetadata;
9use crate::FontVariableAxisMetadata;
10
11fn canonical_family_names(fcx: &mut FontContext) -> Vec<String> {
12    let mut by_lower: HashMap<String, String> = HashMap::new();
13    for name in fcx.collection.family_names() {
14        let key = name.to_ascii_lowercase();
15        by_lower.entry(key).or_insert_with(|| name.to_string());
16    }
17
18    let mut names: Vec<String> = by_lower.into_values().collect();
19    names.sort_unstable_by(|a, b| {
20        a.to_ascii_lowercase()
21            .cmp(&b.to_ascii_lowercase())
22            .then(a.cmp(b))
23    });
24    names
25}
26
27#[derive(Debug, Default, Clone, Copy)]
28pub struct ParleyShaperFontDbDiagnosticsSnapshot {
29    registered_font_blobs_count: u64,
30    registered_font_blobs_total_bytes: u64,
31    family_id_cache_entries: u64,
32    baseline_metrics_cache_entries: u64,
33    catalog_entries_build_count: u64,
34    all_font_names_cache_present: bool,
35    all_font_catalog_entries_cache_present: bool,
36}
37
38impl ParleyShaperFontDbDiagnosticsSnapshot {
39    pub fn registered_font_blobs_count(&self) -> u64 {
40        self.registered_font_blobs_count
41    }
42
43    pub fn registered_font_blobs_total_bytes(&self) -> u64 {
44        self.registered_font_blobs_total_bytes
45    }
46
47    pub fn family_id_cache_entries(&self) -> u64 {
48        self.family_id_cache_entries
49    }
50
51    pub fn baseline_metrics_cache_entries(&self) -> u64 {
52        self.baseline_metrics_cache_entries
53    }
54
55    pub fn catalog_entries_build_count(&self) -> u64 {
56        self.catalog_entries_build_count
57    }
58
59    pub fn all_font_names_cache_present(&self) -> bool {
60        self.all_font_names_cache_present
61    }
62
63    pub fn all_font_catalog_entries_cache_present(&self) -> bool {
64        self.all_font_catalog_entries_cache_present
65    }
66}
67
68pub(crate) struct ParleyFontDbState {
69    system_fonts_enabled: bool,
70    registered_font_blobs: VecDeque<RegisteredFontBlob>,
71    registered_font_blobs_total_bytes: usize,
72    family_id_cache_lower: HashMap<String, FamilyId>,
73    all_font_names_cache: Option<Vec<String>>,
74    all_font_catalog_entries_cache: Option<Vec<FontCatalogEntryMetadata>>,
75    base_line_metrics_cache: HashMap<u64, (f32, f32)>,
76    catalog_entries_build_count: u64,
77}
78
79#[derive(Debug, Clone)]
80struct RegisteredFontBlob {
81    hash: u64,
82    len: usize,
83    blob: parley::fontique::Blob<u8>,
84}
85
86impl Default for ParleyFontDbState {
87    fn default() -> Self {
88        Self {
89            system_fonts_enabled: true,
90            registered_font_blobs: VecDeque::new(),
91            registered_font_blobs_total_bytes: 0,
92            family_id_cache_lower: HashMap::new(),
93            all_font_names_cache: None,
94            all_font_catalog_entries_cache: None,
95            base_line_metrics_cache: HashMap::new(),
96            catalog_entries_build_count: 0,
97        }
98    }
99}
100
101fn env_disables_font_catalog_monospace_probe() -> bool {
102    let Ok(raw) = std::env::var("FRET_TEXT_FONT_CATALOG_MONOSPACE_PROBE") else {
103        return false;
104    };
105    let v = raw.trim().to_ascii_lowercase();
106    matches!(v.as_str(), "0" | "false" | "no" | "off")
107}
108
109fn registered_font_blobs_max_count() -> usize {
110    // Keep enough space for apps that load multiple font families (UI, mono, icons, etc) while
111    // preventing unbounded growth when hot-reloading or repeatedly injecting fonts.
112    std::env::var("FRET_TEXT_REGISTERED_FONT_BLOBS_MAX_COUNT")
113        .ok()
114        .and_then(|v| v.parse::<usize>().ok())
115        .unwrap_or(256)
116        .min(4096)
117}
118
119fn registered_font_blobs_max_bytes() -> usize {
120    // Safety valve for memory-backed font injection. This is a soft cap: we evict the oldest
121    // entries until we are within budget.
122    std::env::var("FRET_TEXT_REGISTERED_FONT_BLOBS_MAX_BYTES")
123        .ok()
124        .and_then(|v| v.parse::<usize>().ok())
125        .unwrap_or(256 * 1024 * 1024)
126        .min(2 * 1024 * 1024 * 1024)
127}
128
129fn hash_bytes(bytes: &[u8]) -> u64 {
130    let mut hasher = std::collections::hash_map::DefaultHasher::new();
131    hasher.write(bytes);
132    hasher.finish()
133}
134
135pub(crate) fn font_environment_fingerprint(
136    all_font_names: &[String],
137    all_font_catalog_entries: &[FontCatalogEntryMetadata],
138) -> u64 {
139    let mut hasher = std::collections::hash_map::DefaultHasher::new();
140    "fret.text.font_environment.v1".hash(&mut hasher);
141    all_font_names.hash(&mut hasher);
142    all_font_catalog_entries.hash(&mut hasher);
143    hasher.finish()
144}
145
146impl ParleyFontDbState {
147    pub(crate) fn diagnostics_snapshot(&self) -> ParleyShaperFontDbDiagnosticsSnapshot {
148        ParleyShaperFontDbDiagnosticsSnapshot {
149            registered_font_blobs_count: self.registered_font_blobs.len() as u64,
150            registered_font_blobs_total_bytes: self.registered_font_blobs_total_bytes as u64,
151            family_id_cache_entries: self.family_id_cache_lower.len() as u64,
152            baseline_metrics_cache_entries: self.base_line_metrics_cache.len() as u64,
153            catalog_entries_build_count: self.catalog_entries_build_count,
154            all_font_names_cache_present: self.all_font_names_cache.is_some(),
155            all_font_catalog_entries_cache_present: self.all_font_catalog_entries_cache.is_some(),
156        }
157    }
158
159    pub(crate) fn system_fonts_enabled(&self) -> bool {
160        self.system_fonts_enabled
161    }
162
163    pub(crate) fn disable_system_fonts(&mut self) {
164        self.system_fonts_enabled = false;
165        self.invalidate_catalog_caches();
166    }
167
168    #[cfg(test)]
169    pub(crate) fn record_registered_font_blob_bytes_for_tests(&mut self, bytes: Vec<u8>) {
170        let blob = parley::fontique::Blob::<u8>::from(bytes);
171        self.record_registered_font_blob(blob);
172    }
173
174    #[cfg(test)]
175    pub(crate) fn registered_font_blob_lengths_for_tests(&self) -> Vec<usize> {
176        self.registered_font_blobs.iter().map(|b| b.len).collect()
177    }
178
179    #[cfg(test)]
180    pub(crate) fn registered_font_blob_total_bytes_for_tests(&self) -> usize {
181        self.registered_font_blobs_total_bytes
182    }
183
184    fn invalidate_catalog_caches(&mut self) {
185        self.family_id_cache_lower.clear();
186        self.all_font_names_cache = None;
187        self.all_font_catalog_entries_cache = None;
188    }
189
190    fn record_registered_font_blob(&mut self, blob: parley::fontique::Blob<u8>) {
191        let bytes = blob.as_ref();
192        let len = bytes.len();
193        let hash = hash_bytes(bytes);
194
195        if let Some(ix) = self
196            .registered_font_blobs
197            .iter()
198            .position(|v| v.hash == hash && v.len == len && v.blob.as_ref() == bytes)
199        {
200            // LRU: keep the most recently injected fonts near the back.
201            let entry = self.registered_font_blobs.remove(ix);
202            if let Some(entry) = entry {
203                self.registered_font_blobs.push_back(entry);
204            }
205            return;
206        }
207
208        self.registered_font_blobs_total_bytes =
209            self.registered_font_blobs_total_bytes.saturating_add(len);
210        self.registered_font_blobs
211            .push_back(RegisteredFontBlob { hash, len, blob });
212
213        let max_count = registered_font_blobs_max_count();
214        let max_bytes = registered_font_blobs_max_bytes();
215        while self.registered_font_blobs.len() > max_count
216            || self.registered_font_blobs_total_bytes > max_bytes
217        {
218            let Some(evicted) = self.registered_font_blobs.pop_front() else {
219                break;
220            };
221            self.registered_font_blobs_total_bytes = self
222                .registered_font_blobs_total_bytes
223                .saturating_sub(evicted.len);
224        }
225    }
226
227    pub(crate) fn all_font_names(&mut self, fcx: &mut FontContext) -> Vec<String> {
228        if let Some(cache) = self.all_font_names_cache.as_ref() {
229            return cache.clone();
230        }
231
232        let names = canonical_family_names(fcx);
233        self.all_font_names_cache = Some(names.clone());
234        names
235    }
236
237    pub(crate) fn all_font_catalog_entries(
238        &mut self,
239        fcx: &mut FontContext,
240    ) -> Vec<FontCatalogEntryMetadata> {
241        if let Some(cache) = self.all_font_catalog_entries_cache.as_ref() {
242            return cache.clone();
243        }
244        self.catalog_entries_build_count = self.catalog_entries_build_count.saturating_add(1);
245
246        fn axis_tag_string(tag_be_bytes: [u8; 4]) -> String {
247            String::from_utf8_lossy(&tag_be_bytes).to_string()
248        }
249
250        let names = canonical_family_names(fcx);
251        let mut out: Vec<FontCatalogEntryMetadata> = Vec::with_capacity(names.len());
252        for family in names {
253            let Some(id) = fcx.collection.family_id(&family) else {
254                continue;
255            };
256            let Some(info) = fcx.collection.family(id) else {
257                continue;
258            };
259
260            let mut has_variable_axes = false;
261            let mut has_wght = false;
262            let mut has_wdth = false;
263            let mut has_slnt = false;
264            let mut has_ital = false;
265            let mut has_opsz = false;
266
267            for font in info.fonts() {
268                has_variable_axes |= !font.axes().is_empty();
269                has_wght |= font.has_weight_axis();
270                has_wdth |= font.has_width_axis();
271                has_slnt |= font.has_slant_axis();
272                has_ital |= font.has_italic_axis();
273                has_opsz |= font.has_optical_size_axis();
274            }
275
276            let mut known_variable_axes: Vec<String> = Vec::new();
277            if has_wght {
278                known_variable_axes.push("wght".to_string());
279            }
280            if has_wdth {
281                known_variable_axes.push("wdth".to_string());
282            }
283            if has_slnt {
284                known_variable_axes.push("slnt".to_string());
285            }
286            if has_ital {
287                known_variable_axes.push("ital".to_string());
288            }
289            if has_opsz {
290                known_variable_axes.push("opsz".to_string());
291            }
292
293            let variable_axes = info
294                .default_font()
295                .map(|font| {
296                    font.axes()
297                        .iter()
298                        .take(64)
299                        .map(|axis| {
300                            FontVariableAxisMetadata::new(
301                                axis_tag_string(axis.tag.to_be_bytes()),
302                                axis.min.to_bits(),
303                                axis.max.to_bits(),
304                                axis.default.to_bits(),
305                            )
306                        })
307                        .collect::<Vec<_>>()
308                })
309                .unwrap_or_default();
310
311            let is_monospace_candidate = if env_disables_font_catalog_monospace_probe() {
312                false
313            } else {
314                info.default_font()
315                    .and_then(|font| {
316                        let blob = font.load(Some(&mut fcx.source_cache))?;
317                        let face = FontRef::from_index(blob.as_ref(), font.index()).ok()?;
318                        let post = face.post().ok()?;
319                        Some(post.is_fixed_pitch() != 0)
320                    })
321                    .unwrap_or(false)
322            };
323
324            out.push(FontCatalogEntryMetadata::new(
325                family,
326                has_variable_axes,
327                known_variable_axes,
328                variable_axes,
329                is_monospace_candidate,
330            ));
331        }
332
333        self.all_font_catalog_entries_cache = Some(out.clone());
334        out
335    }
336
337    pub(crate) fn family_name_for_id(
338        &mut self,
339        fcx: &mut FontContext,
340        id: FamilyId,
341    ) -> Option<String> {
342        fcx.collection.family_name(id).map(|name| name.to_string())
343    }
344
345    pub(crate) fn for_each_font_environment_blob(
346        &mut self,
347        fcx: &mut FontContext,
348        mut f: impl FnMut(crate::FontEnvironmentBlobRef<'_>),
349    ) {
350        let names = canonical_family_names(fcx);
351        let mut seen: Vec<RegisteredFontBlob> = Vec::new();
352
353        for family in names {
354            let Some(id) = fcx.collection.family_id(&family) else {
355                continue;
356            };
357            let Some(info) = fcx.collection.family(id) else {
358                continue;
359            };
360
361            for font in info.fonts() {
362                let Some(blob) = font.load(Some(&mut fcx.source_cache)) else {
363                    continue;
364                };
365
366                let bytes = blob.as_ref();
367                let len = bytes.len();
368                let hash = hash_bytes(bytes);
369                if seen.iter().any(|existing| {
370                    existing.hash == hash && existing.len == len && existing.blob.as_ref() == bytes
371                }) {
372                    continue;
373                }
374
375                seen.push(RegisteredFontBlob {
376                    hash,
377                    len,
378                    blob: blob.clone(),
379                });
380                f(crate::FontEnvironmentBlobRef::new(hash, bytes));
381            }
382        }
383    }
384
385    pub(crate) fn resolve_family_id(
386        &mut self,
387        fcx: &mut FontContext,
388        name: &str,
389    ) -> Option<FamilyId> {
390        let name = name.trim();
391        if name.is_empty() {
392            return None;
393        }
394
395        if let Some(id) = fcx.collection.family_id(name) {
396            return Some(id);
397        }
398
399        let target = name.to_ascii_lowercase();
400        if let Some(id) = self.family_id_cache_lower.get(&target).copied() {
401            return Some(id);
402        }
403
404        let mut resolved_name: Option<String> = None;
405        for candidate in fcx.collection.family_names() {
406            if candidate.to_ascii_lowercase() != target {
407                continue;
408            }
409            resolved_name = Some(candidate.to_string());
410            break;
411        }
412
413        let resolved = resolved_name
414            .as_deref()
415            .and_then(|name| fcx.collection.family_id(name));
416
417        if let Some(id) = resolved {
418            self.family_id_cache_lower.insert(target, id);
419        }
420        resolved
421    }
422
423    pub(crate) fn generic_family_ids(
424        &self,
425        fcx: &mut FontContext,
426        generic: GenericFamily,
427    ) -> Vec<FamilyId> {
428        fcx.collection.generic_families(generic).collect()
429    }
430
431    pub(crate) fn set_generic_family_ids(
432        &mut self,
433        fcx: &mut FontContext,
434        generic: GenericFamily,
435        ids: &[FamilyId],
436    ) -> bool {
437        let before = self.generic_family_ids(fcx, generic);
438        if before == ids {
439            return false;
440        }
441        fcx.collection
442            .set_generic_families(generic, ids.iter().copied());
443        true
444    }
445
446    pub(crate) fn add_fonts(
447        &mut self,
448        fcx: &mut FontContext,
449        fonts: impl IntoIterator<Item = Vec<u8>>,
450    ) -> usize {
451        let mut added = 0usize;
452        for data in fonts {
453            let blob = parley::fontique::Blob::<u8>::from(data);
454            self.record_registered_font_blob(blob.clone());
455            let families = fcx.collection.register_fonts(blob, None);
456            added = added.saturating_add(families.iter().map(|(_, fonts)| fonts.len()).sum());
457        }
458        if added > 0 {
459            self.invalidate_catalog_caches();
460        }
461        added
462    }
463
464    pub(crate) fn system_font_rescan_seed(&self) -> Option<crate::SystemFontRescanSeed> {
465        if !self.system_fonts_enabled {
466            return None;
467        }
468
469        Some(crate::SystemFontRescanSeed {
470            registered_font_blobs: self
471                .registered_font_blobs
472                .iter()
473                .map(|b| b.blob.clone())
474                .collect(),
475        })
476    }
477
478    pub(crate) fn apply_system_font_rescan_result(
479        &mut self,
480        fcx: &mut FontContext,
481        result: crate::SystemFontRescanResult,
482    ) -> bool {
483        if !self.system_fonts_enabled {
484            return false;
485        }
486
487        let crate::SystemFontRescanResult {
488            collection,
489            all_font_names,
490            all_font_catalog_entries,
491            environment_fingerprint,
492        } = result;
493
494        if self.current_font_environment_fingerprint(fcx) == environment_fingerprint {
495            return false;
496        }
497
498        fcx.collection = collection;
499        self.invalidate_catalog_caches();
500        self.all_font_names_cache = Some(all_font_names);
501        self.all_font_catalog_entries_cache = Some(all_font_catalog_entries);
502        true
503    }
504
505    pub(crate) fn current_font_environment_fingerprint(&mut self, fcx: &mut FontContext) -> u64 {
506        let all_font_names = self.all_font_names(fcx);
507        let all_font_catalog_entries = self.all_font_catalog_entries(fcx);
508        font_environment_fingerprint(&all_font_names, &all_font_catalog_entries)
509    }
510
511    pub(crate) fn base_line_metrics_cache_key(
512        &self,
513        default_locale: Option<&str>,
514        common_fallback_stack_suffix: &str,
515        style: &TextStyle,
516        scale: f32,
517    ) -> u64 {
518        let mut hasher = std::collections::hash_map::DefaultHasher::new();
519        "fret.text.base_line_metrics.v1".hash(&mut hasher);
520        style.font.hash(&mut hasher);
521        style.size.0.to_bits().hash(&mut hasher);
522        style.weight.0.hash(&mut hasher);
523        match style.slant {
524            TextSlant::Normal => 0u8,
525            TextSlant::Italic => 1u8,
526            TextSlant::Oblique => 2u8,
527        }
528        .hash(&mut hasher);
529        style
530            .letter_spacing_em
531            .map(|v| v.to_bits())
532            .unwrap_or(0)
533            .hash(&mut hasher);
534        default_locale.hash(&mut hasher);
535        common_fallback_stack_suffix.hash(&mut hasher);
536        scale.to_bits().hash(&mut hasher);
537        hasher.finish()
538    }
539
540    pub(crate) fn base_line_metrics(&self, key: u64) -> Option<(f32, f32)> {
541        self.base_line_metrics_cache.get(&key).copied()
542    }
543
544    pub(crate) fn insert_base_line_metrics(&mut self, key: u64, metrics: (f32, f32)) {
545        self.base_line_metrics_cache.insert(key, metrics);
546    }
547}
548
549pub(crate) fn run_system_font_rescan(
550    seed: crate::SystemFontRescanSeed,
551) -> crate::SystemFontRescanResult {
552    let mut fcx = FontContext {
553        collection: parley::fontique::Collection::new(parley::fontique::CollectionOptions {
554            shared: false,
555            system_fonts: true,
556        }),
557        source_cache: parley::fontique::SourceCache::default(),
558    };
559
560    for blob in seed.registered_font_blobs {
561        let _ = fcx.collection.register_fonts(blob, None);
562    }
563
564    let mut font_db = ParleyFontDbState::default();
565    let all_font_names = font_db.all_font_names(&mut fcx);
566    let all_font_catalog_entries = font_db.all_font_catalog_entries(&mut fcx);
567    let environment_fingerprint =
568        font_environment_fingerprint(&all_font_names, &all_font_catalog_entries);
569    crate::SystemFontRescanResult {
570        collection: fcx.collection,
571        all_font_names,
572        all_font_catalog_entries,
573        environment_fingerprint,
574    }
575}