1use std::{
21 io::Read,
22 path::{Path, PathBuf},
23 sync::Arc,
24};
25
26use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
27
28use crate::software_text_raster::{
29 FontFamilyKey, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
30 default_software_text_font,
31};
32
33pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
35
36pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
39 &[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
40
41#[derive(Debug, thiserror::Error)]
47pub enum FontLoadError {
48 #[error("font family declares no faces")]
49 EmptyFamily,
50 #[error("font family is not backed by files, so it has nothing to load")]
51 NotFileBacked,
52 #[error("no system font file for this family under {directory}")]
53 NoSystemFontFile { directory: PathBuf },
54 #[error("failed to read font file {path}: {source}")]
55 Read {
56 path: PathBuf,
57 #[source]
58 source: std::io::Error,
59 },
60 #[error("failed to parse font file {path}: {source}")]
61 Parse {
62 path: PathBuf,
63 #[source]
64 source: SoftwareTextFontError,
65 },
66 #[error("failed to parse font bytes: {source}")]
67 ParseBytes {
68 #[source]
69 source: SoftwareTextFontError,
70 },
71}
72
73#[derive(Clone, Default)]
79pub struct SoftwareTextFontRegistry {
80 faces: Vec<SoftwareTextFont>,
81 system_faces: Vec<(FontFamilyKey, FontWeight, FontStyle)>,
82}
83
84#[derive(Default)]
85struct TolerantLoad {
86 first_error: Option<FontLoadError>,
87 loaded: usize,
88}
89
90impl TolerantLoad {
91 fn record(&mut self, result: Result<(), FontLoadError>) {
92 match result {
93 Ok(()) => self.loaded += 1,
94 Err(error) => self.first_error = self.first_error.take().or(Some(error)),
95 }
96 }
97
98 fn finish(self) -> Result<(), FontLoadError> {
99 match self.first_error {
100 Some(error) if self.loaded == 0 => Err(error),
101 _ => Ok(()),
102 }
103 }
104}
105
106impl SoftwareTextFontRegistry {
107 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
121 let files = font_files_for(family)?;
122 if files.is_empty() {
123 return Err(FontLoadError::EmptyFamily);
124 }
125
126 let mut reads = FontFileReads::default();
127 let mut load = TolerantLoad::default();
128 for file in &files {
129 load.record(self.register_read_face(
130 &mut reads,
131 family,
132 file.weight,
133 file.style,
134 Path::new(&file.path),
135 &[],
136 ));
137 }
138 load.finish()
139 }
140
141 fn register_read_face(
142 &mut self,
143 reads: &mut FontFileReads,
144 family: &FontFamily,
145 weight: FontWeight,
146 style: FontStyle,
147 path: &Path,
148 variations: &[([u8; 4], f32)],
149 ) -> Result<(), FontLoadError> {
150 let bytes = reads.read(path)?;
151 let face = SoftwareTextFont::from_registered_bytes_with_variations(
152 family,
153 weight,
154 style,
155 bytes.to_vec(),
156 variations,
157 )
158 .map_err(|source| FontLoadError::Parse {
159 path: path.to_path_buf(),
160 source,
161 })?;
162 self.faces.push(face);
163 Ok(())
164 }
165
166 pub fn register_face_reader(
173 &mut self,
174 family: &FontFamily,
175 weight: FontWeight,
176 style: FontStyle,
177 reader: &mut impl Read,
178 ) -> Result<(), FontLoadError> {
179 let mut bytes = Vec::new();
180 reader
181 .read_to_end(&mut bytes)
182 .map_err(|source| FontLoadError::Read {
183 path: PathBuf::new(),
184 source,
185 })?;
186 self.register_face_bytes(family, weight, style, bytes)
187 }
188
189 pub fn register_face_bytes(
191 &mut self,
192 family: &FontFamily,
193 weight: FontWeight,
194 style: FontStyle,
195 bytes: impl Into<Vec<u8>>,
196 ) -> Result<(), FontLoadError> {
197 self.register_face_bytes_with_variations(family, weight, style, bytes, &[])
198 }
199
200 pub fn register_face_bytes_with_variations(
203 &mut self,
204 family: &FontFamily,
205 weight: FontWeight,
206 style: FontStyle,
207 bytes: impl Into<Vec<u8>>,
208 variations: &[([u8; 4], f32)],
209 ) -> Result<(), FontLoadError> {
210 let face = SoftwareTextFont::from_registered_bytes_with_variations(
211 family, weight, style, bytes, variations,
212 )
213 .map_err(|source| FontLoadError::ParseBytes { source })?;
214 self.faces.push(face);
215 Ok(())
216 }
217
218 pub fn register_fallback_bytes(
223 &mut self,
224 bytes: impl Into<Vec<u8>>,
225 ) -> Result<(), FontLoadError> {
226 let face = SoftwareTextFont::from_bytes(bytes)
227 .map_err(|source| FontLoadError::ParseBytes { source })?;
228 self.faces.push(face);
229 Ok(())
230 }
231
232 pub fn register_system_family(
249 &mut self,
250 directory: impl AsRef<Path>,
251 family: &FontFamily,
252 weights: &[FontWeight],
253 ) -> Result<(), FontLoadError> {
254 let directory = directory.as_ref();
255 let mut reads = FontFileReads::default();
256 let mut load = TolerantLoad::default();
257 for weight in weights {
258 load.record(self.register_read_system_face(
259 &mut reads,
260 directory,
261 family,
262 *weight,
263 FontStyle::Normal,
264 &[],
265 ));
266 }
267 load.finish()
268 }
269
270 pub fn register_system_face(
278 &mut self,
279 directory: impl AsRef<Path>,
280 family: &FontFamily,
281 weight: FontWeight,
282 style: FontStyle,
283 ) -> Result<(), FontLoadError> {
284 self.register_system_face_with_variations(directory, family, weight, style, &[])
285 }
286
287 pub fn register_system_face_with_variations(
291 &mut self,
292 directory: impl AsRef<Path>,
293 family: &FontFamily,
294 weight: FontWeight,
295 style: FontStyle,
296 variations: &[([u8; 4], f32)],
297 ) -> Result<(), FontLoadError> {
298 self.register_read_system_face(
299 &mut FontFileReads::default(),
300 directory.as_ref(),
301 family,
302 weight,
303 style,
304 variations,
305 )
306 }
307
308 fn register_read_system_face(
309 &mut self,
310 reads: &mut FontFileReads,
311 directory: &Path,
312 family: &FontFamily,
313 weight: FontWeight,
314 style: FontStyle,
315 variations: &[([u8; 4], f32)],
316 ) -> Result<(), FontLoadError> {
317 let weight = system_declared_weight(family, weight);
318 if self.has_system_face(family, weight, style) {
319 return Ok(());
320 }
321 let path = system_font_file(directory, family, weight).ok_or_else(|| {
322 FontLoadError::NoSystemFontFile {
323 directory: directory.to_path_buf(),
324 }
325 })?;
326 self.register_read_face(reads, family, weight, style, &path, variations)?;
327 self.system_faces
328 .push((FontFamilyKey::of(family), weight, style));
329 Ok(())
330 }
331
332 fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
333 self.system_faces
334 .contains(&(FontFamilyKey::of(family), weight, style))
335 }
336
337 pub fn faces(&self) -> &[SoftwareTextFont] {
339 &self.faces
340 }
341
342 pub fn is_empty(&self) -> bool {
343 self.faces.is_empty()
344 }
345
346 pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
354 for bytes in fonts {
355 let _ = self.register_fallback_bytes((*bytes).to_vec());
356 }
357 if self.faces.is_empty()
358 && let Some(default_font) = default_software_text_font()
359 {
360 self.faces.push(default_font);
361 }
362 SoftwareTextFontSet::from_faces(self.faces)
363 }
364}
365
366#[derive(Default)]
367struct FontFileReads {
368 entries: Vec<(PathBuf, Arc<[u8]>)>,
369}
370
371impl FontFileReads {
372 fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
373 if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
374 return Ok(Arc::clone(bytes));
375 }
376 let bytes: Arc<[u8]> = std::fs::read(path)
377 .map_err(|source| FontLoadError::Read {
378 path: path.to_path_buf(),
379 source,
380 })?
381 .into();
382 self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
383 Ok(bytes)
384 }
385}
386
387pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
431 let Some(files) = system_family_files(family) else {
432 return weight;
433 };
434 closest_declared_weight(files.declared, weight).unwrap_or(weight)
435}
436
437fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
438 let mut best: Option<(u16, u16)> = None;
439 for candidate in declared {
440 let score = weight_match_score(*candidate, requested.value());
441 if best.is_none_or(|(_, best_score)| score < best_score) {
442 best = Some((*candidate, score));
443 }
444 }
445 best.map(|(candidate, _)| FontWeight(candidate))
446}
447
448fn weight_match_score(declared: u16, requested: u16) -> u16 {
449 (declared / 100).abs_diff(requested / 100)
450}
451
452pub fn system_font_file(
459 directory: &Path,
460 family: &FontFamily,
461 weight: FontWeight,
462) -> Option<PathBuf> {
463 let files = system_family_files(family)?;
464 files
465 .weighted
466 .iter()
467 .filter(|(candidate_weight, _)| *candidate_weight == weight.value())
468 .map(|(_, name)| directory.join(name))
469 .chain(files.regular.iter().map(|name| directory.join(name)))
470 .find(|path| path.is_file())
471}
472
473struct SystemFamilyFiles {
474 regular: &'static [&'static str],
475 weighted: &'static [(u16, &'static str)],
476 declared: &'static [u16],
477}
478
479const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
480
481fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
482 match family {
483 FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
484 regular: &[
485 "Roboto-Regular.ttf",
486 "RobotoStatic-Regular.ttf",
487 "NotoSans-Regular.ttf",
488 "DroidSans.ttf",
489 "Core/SFUI.ttf",
490 "SFNS.ttf",
491 ],
492 weighted: &[
493 (300, "Roboto-Light.ttf"),
494 (500, "Roboto-Medium.ttf"),
495 (700, "Roboto-Bold.ttf"),
496 (900, "Roboto-Black.ttf"),
497 ],
498 declared: DECLARED_HUNDREDS,
499 }),
500 FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
501 regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
502 weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
503 declared: &[400, 700],
504 }),
505 FontFamily::Monospace => Some(SystemFamilyFiles {
506 regular: &[
507 "DroidSansMono.ttf",
508 "RobotoMono-Regular.ttf",
509 "CutiveMono-Regular.ttf",
510 ],
511 weighted: &[(700, "RobotoMono-Bold.ttf")],
512 declared: &[400, 700],
513 }),
514 FontFamily::Cursive => Some(SystemFamilyFiles {
515 regular: &["DancingScript-Regular.ttf"],
516 weighted: &[(700, "DancingScript-Bold.ttf")],
517 declared: &[400, 700],
518 }),
519 FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
520 }
521}
522
523fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
524 match family {
525 FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
526 FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
527 _ => Err(FontLoadError::NotFileBacked),
528 }
529}
530
531#[cfg(test)]
532#[path = "tests/font_source_tests.rs"]
533mod tests;