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 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 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 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}