1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::error::{LayoutError, Result};
10use crate::output::FontId;
11
12#[derive(Debug, Clone)]
14pub struct FontFile {
15 pub family: String,
17 pub data: Vec<u8>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23struct FontKey {
24 family: String,
25 bold: bool,
26 italic: bool,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct FontMetrics {
32 pub ascent: f64,
34 pub descent: f64,
36 pub line_gap: f64,
38 pub units_per_em: u16,
40}
41
42#[derive(Debug, Clone)]
44pub struct ShapedText {
45 pub glyph_ids: Vec<u16>,
47 pub advances: Vec<f64>,
49 pub width: f64,
51}
52
53struct LoadedFont {
55 id: FontId,
56 family: String,
57 bold: bool,
58 italic: bool,
59 data: Arc<Vec<u8>>,
60 face_index: u32,
61 units_per_em: u16,
62 ascender: i16,
64 descender: i16,
65 line_gap: i16,
66 shaper_data: harfrust::ShaperData,
69}
70
71pub struct FontManager {
73 db: fontdb::Database,
74 cache: HashMap<FontKey, usize>,
76 fonts: Vec<LoadedFont>,
78 next_id: u32,
80 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
88 coverage_misses: HashSet<char>,
91}
92
93const BROAD_COVERAGE_FAMILIES: &[&str] = &[
98 "Noto Sans CJK SC",
100 "Noto Sans CJK JP",
101 "Noto Sans CJK KR",
102 "Noto Sans CJK TC",
103 "Noto Serif CJK SC",
104 "Source Han Sans SC",
105 "WenQuanYi Zen Hei",
106 "WenQuanYi Micro Hei",
107 "PingFang SC",
109 "PingFang TC",
110 "Hiragino Sans",
111 "Hiragino Kaku Gothic ProN",
112 "Apple SD Gothic Neo",
113 "Songti SC",
114 "STHeiti",
115 "Microsoft YaHei",
117 "Microsoft JhengHei",
118 "SimSun",
119 "SimHei",
120 "NSimSun",
121 "Yu Gothic",
122 "MS Gothic",
123 "Meiryo",
124 "Malgun Gothic",
125 "Arial Unicode MS",
127 "DejaVu Sans",
128];
129
130impl Default for FontManager {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl FontManager {
137 pub fn new() -> Self {
142 let mut db = fontdb::Database::new();
143
144 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
146 db.load_font_data(data.to_vec());
147 }
148
149 #[cfg(feature = "system-fonts")]
151 db.load_system_fonts();
152
153 FontManager {
154 db,
155 cache: HashMap::new(),
156 fonts: Vec::new(),
157 next_id: 0,
158 coverage_fallbacks: HashMap::new(),
159 coverage_misses: HashSet::new(),
160 }
161 }
162
163 pub fn new_deterministic() -> Result<Self> {
168 let mut db = fontdb::Database::new();
169 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
170 db.load_font_data(data.to_vec());
171 }
172
173 Ok(FontManager {
174 db,
175 cache: HashMap::new(),
176 fonts: Vec::new(),
177 next_id: 0,
178 coverage_fallbacks: HashMap::new(),
179 coverage_misses: HashSet::new(),
180 })
181 }
182
183 pub fn load_additional_fonts(&mut self, font_files: &[FontFile]) {
188 for font_file in font_files {
189 self.db.load_font_data(font_file.data.clone());
190 }
191 self.cache.clear();
193 }
194
195 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
200 let mut db = fontdb::Database::new();
201 for (_name, data) in &fonts {
202 db.load_font_data(data.clone());
203 }
204 FontManager {
205 db,
206 cache: HashMap::new(),
207 fonts: Vec::new(),
208 next_id: 0,
209 coverage_fallbacks: HashMap::new(),
210 coverage_misses: HashSet::new(),
211 }
212 }
213
214 pub fn resolve_font_for_text(
231 &mut self,
232 family: Option<&str>,
233 bold: bool,
234 italic: bool,
235 text: &str,
236 ) -> Result<FontId> {
237 let primary = self.resolve_font(family, bold, italic)?;
238
239 let Some(idx) = self.index_of(primary) else {
240 return Ok(primary);
241 };
242 let missing = self.uncovered(idx, text);
243 if missing.is_empty() {
244 return Ok(primary);
245 }
246
247 match self.font_covering(&missing, bold, italic) {
248 None => Ok(primary),
251 Some(id) => Ok(id),
252 }
253 }
254
255 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
260 let font = &self.fonts[idx];
261 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
262 return Vec::new();
263 };
264 let mut seen = HashSet::new();
265 text.chars()
266 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
267 .filter(|&ch| face.glyph_index(ch).is_none())
268 .filter(|&ch| seen.insert(ch))
269 .collect()
270 }
271
272 fn covers(&self, idx: usize, ch: char) -> bool {
274 let font = &self.fonts[idx];
275 ttf_parser::Face::parse(&font.data, font.face_index)
276 .map(|face| face.glyph_index(ch).is_some())
277 .unwrap_or(false)
278 }
279
280 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
289 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
290 return None;
291 }
292
293 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
295 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
296 if covered == 0 {
297 return false;
298 }
299 if best.map(|(n, _)| covered > n).unwrap_or(true) {
300 *best = Some((covered, idx));
301 }
302 covered == missing.len()
303 };
304
305 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
308 for idx in known {
309 if consider(self, idx, &mut best) {
310 return Some(self.fonts[idx].id);
311 }
312 }
313 }
314
315 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
319 .iter()
320 .map(|s| s.to_string())
321 .chain(
322 self.db
323 .faces()
324 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
325 )
326 .collect();
327
328 for name in candidates {
329 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
330 continue;
331 };
332 let Some(idx) = self.index_of(id) else {
333 continue;
334 };
335 let complete = consider(self, idx, &mut best);
336 if complete {
337 self.coverage_fallbacks
338 .entry((bold, italic))
339 .or_default()
340 .push(idx);
341 return Some(id);
342 }
343 }
344
345 match best {
346 Some((_, idx)) => {
347 self.coverage_fallbacks
348 .entry((bold, italic))
349 .or_default()
350 .push(idx);
351 Some(self.fonts[idx].id)
352 }
353 None => {
354 for &ch in missing {
355 self.coverage_misses.insert(ch);
356 }
357 None
358 }
359 }
360 }
361
362 fn index_of(&self, id: FontId) -> Option<usize> {
364 self.fonts.iter().position(|f| f.id == id)
365 }
366
367 pub fn resolve_font(
370 &mut self,
371 family: Option<&str>,
372 bold: bool,
373 italic: bool,
374 ) -> Result<FontId> {
375 let family_name = family.unwrap_or("Arial");
376
377 let key = FontKey {
378 family: family_name.to_string(),
379 bold,
380 italic,
381 };
382
383 if let Some(&idx) = self.cache.get(&key) {
384 return Ok(self.fonts[idx].id);
385 }
386
387 let mapped = map_font_name(family_name);
389
390 let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
392 fallbacks.push(family_name);
393 for alt in mapped {
394 if *alt != family_name {
395 fallbacks.push(alt);
396 }
397 }
398 for generic in &[
399 "Carlito",
400 "Arial",
401 "Liberation Sans",
402 "Helvetica",
403 "DejaVu Sans",
404 "Noto Sans",
405 ] {
406 if !fallbacks.contains(generic) {
407 fallbacks.push(generic);
408 }
409 }
410
411 let style = if italic {
412 fontdb::Style::Italic
413 } else {
414 fontdb::Style::Normal
415 };
416 let weight = if bold {
417 fontdb::Weight::BOLD
418 } else {
419 fontdb::Weight::NORMAL
420 };
421
422 let mut found_id = None;
423 for fallback in &fallbacks {
424 let query = fontdb::Query {
425 families: &[fontdb::Family::Name(fallback)],
426 weight,
427 style,
428 stretch: fontdb::Stretch::Normal,
429 };
430
431 if let Some(id) = self.db.query(&query) {
432 found_id = Some(id);
433 break;
434 }
435 }
436
437 if found_id.is_none() {
439 for generic_family in &[
440 fontdb::Family::SansSerif,
441 fontdb::Family::Serif,
442 fontdb::Family::Monospace,
443 ] {
444 let query = fontdb::Query {
445 families: &[*generic_family],
446 weight,
447 style,
448 stretch: fontdb::Stretch::Normal,
449 };
450 if let Some(id) = self.db.query(&query) {
451 found_id = Some(id);
452 break;
453 }
454 }
455 }
456
457 let db_id = found_id.ok_or_else(|| {
458 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
459 })?;
460
461 let font_id = FontId(self.next_id);
462 self.next_id += 1;
463
464 let (data, face_index) = self
466 .db
467 .with_face_data(db_id, |data, idx| (Arc::new(data.to_vec()), idx))
468 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
469
470 let (units_per_em, ascender, descender, line_gap) = {
471 let face = ttf_parser::Face::parse(&data, face_index)
472 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
473 (
474 face.units_per_em(),
475 face.ascender(),
476 face.descender(),
477 face.line_gap(),
478 )
479 };
480
481 if units_per_em == 0 {
484 return Err(LayoutError::FontParse(format!(
485 "font '{family_name}' declares zero units per em"
486 )));
487 }
488
489 let shaper_data = {
490 let face = harfrust::FontRef::from_index(&data, face_index)
491 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
492 harfrust::ShaperData::new(&face)
493 };
494
495 let actual_family = self
496 .db
497 .face(db_id)
498 .map(|f| {
499 f.families
500 .first()
501 .map(|(name, _)| name.clone())
502 .unwrap_or_else(|| family_name.to_string())
503 })
504 .unwrap_or_else(|| family_name.to_string());
505
506 let idx = self.fonts.len();
507 self.fonts.push(LoadedFont {
508 id: font_id,
509 family: actual_family,
510 bold,
511 italic,
512 data,
513 face_index,
514 units_per_em,
515 ascender,
516 descender,
517 line_gap,
518 shaper_data,
519 });
520 self.cache.insert(key, idx);
521
522 Ok(font_id)
523 }
524
525 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
527 let font = self.get_font(font_id)?;
528 let scale = size_pt / font.units_per_em as f64;
529
530 Ok(FontMetrics {
531 ascent: font.ascender as f64 * scale,
532 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
534 units_per_em: font.units_per_em,
535 })
536 }
537
538 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
540 if text.is_empty() {
543 return Ok(ShapedText {
544 glyph_ids: Vec::new(),
545 advances: Vec::new(),
546 width: 0.0,
547 });
548 }
549
550 let font = self.get_font(font_id)?;
551
552 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
553 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
554
555 let shaper = font.shaper_data.shaper(&face).build();
556
557 let mut buffer = harfrust::UnicodeBuffer::new();
558 buffer.push_str(text);
559 buffer.guess_segment_properties();
562
563 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
564 let infos = output.glyph_infos();
565 let positions = output.glyph_positions();
566
567 let upem = font.units_per_em as f64;
568 let scale = size_pt / upem;
569
570 let mut glyph_ids = Vec::with_capacity(infos.len());
571 let mut advances = Vec::with_capacity(positions.len());
572 let mut total_width = 0.0;
573
574 for (info, pos) in infos.iter().zip(positions.iter()) {
575 glyph_ids.push(info.glyph_id as u16);
576 let advance = pos.x_advance as f64 * scale;
577 advances.push(advance);
578 total_width += advance;
579 }
580
581 Ok(ShapedText {
582 glyph_ids,
583 advances,
584 width: total_width,
585 })
586 }
587
588 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
590 let font = self.get_font(font_id)?;
591 Ok(crate::output::FontData {
592 id: font.id,
593 family: font.family.clone(),
594 data: (*font.data).clone(),
595 face_index: font.face_index,
596 bold: font.bold,
597 italic: font.italic,
598 })
599 }
600
601 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
603 self.fonts
604 .iter()
605 .map(|f| crate::output::FontData {
606 id: f.id,
607 family: f.family.clone(),
608 data: (*f.data).clone(),
609 face_index: f.face_index,
610 bold: f.bold,
611 italic: f.italic,
612 })
613 .collect()
614 }
615
616 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
617 self.fonts
618 .iter()
619 .find(|f| f.id == font_id)
620 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
621 }
622}
623
624fn map_font_name(name: &str) -> &[&str] {
631 match name {
632 "Calibri" => &["Calibri", "Carlito"],
633 "Calibri Light" => &["Calibri Light", "Carlito"],
634 "Cambria" => &["Cambria", "Caladea"],
635 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
636 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
637 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
638 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
639 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
640 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
641 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
642 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
643 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
644 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
645 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
646 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
647 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
648 "Impact" => &["Impact", "Liberation Sans", "Arial"],
649 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
650 "Symbol" => &["Symbol", "DejaVu Sans"],
651 "Wingdings" => &["Wingdings", "Symbol"],
652 _ => &[],
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::bundled_fonts::bundled_font_data;
660
661 #[test]
662 fn deterministic_font_manager_uses_only_bundled_fonts() {
663 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
664
665 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
666 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
667 }
668
669 #[cfg(not(feature = "system-fonts"))]
670 #[test]
671 fn no_default_features_omits_system_font_discovery() {
672 let fm = FontManager::new();
673 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
674 }
675
676 #[test]
677 fn font_manager_with_no_fonts_returns_an_error() {
678 let mut fm = FontManager::new_with_fonts(Vec::new());
679 assert!(matches!(
680 fm.resolve_font(None, false, false),
681 Err(LayoutError::FontNotFound(_))
682 ));
683 }
684
685 #[test]
686 fn load_system_font() {
687 let mut fm = FontManager::new();
688 let result = fm.resolve_font(None, false, false);
690 if let Ok(id) = result {
692 assert_eq!(id.0, 0);
693 }
694 }
695
696 #[test]
697 fn font_metrics_positive() {
698 let mut fm = FontManager::new();
699 if let Ok(id) = fm.resolve_font(None, false, false) {
700 let metrics = fm.metrics(id, 12.0).unwrap();
701 assert!(metrics.ascent > 0.0);
702 assert!(metrics.descent > 0.0);
703 assert!(metrics.units_per_em > 0);
704 }
705 }
706
707 #[test]
708 fn shape_hello_world() {
709 let mut fm = FontManager::new();
710 if let Ok(id) = fm.resolve_font(None, false, false) {
711 let shaped = fm.shape_text(id, "Hello World", 12.0).unwrap();
712 assert!(!shaped.glyph_ids.is_empty());
713 assert_eq!(shaped.glyph_ids.len(), shaped.advances.len());
714 assert!(shaped.width > 0.0);
715 }
716 }
717
718 #[test]
719 fn font_caching() {
720 let mut fm = FontManager::new();
721 if let Ok(id1) = fm.resolve_font(Some("Arial"), false, false) {
722 let id2 = fm.resolve_font(Some("Arial"), false, false).unwrap();
723 assert_eq!(id1, id2);
724 }
725 }
726
727 #[test]
728 fn bold_italic_variants() {
729 let mut fm = FontManager::new();
730 let regular = fm.resolve_font(None, false, false);
731 let bold = fm.resolve_font(None, true, false);
732 if let (Ok(r), Ok(b)) = (regular, bold) {
733 assert_ne!(r, b);
735 }
736 }
737
738 #[test]
741 fn latin_text_resolves_the_same_as_by_name() {
742 let mut fm = FontManager::new();
743 let Ok(by_name) = fm.resolve_font(Some("Arial"), false, false) else {
744 return;
745 };
746 let for_text = fm
747 .resolve_font_for_text(Some("Arial"), false, false, "Hello world")
748 .unwrap();
749 assert_eq!(by_name, for_text);
750 }
751
752 #[test]
758 fn text_no_font_can_draw_keeps_the_requested_font() {
759 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
760 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
761 let resolved = fm
762 .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
763 .unwrap();
764 assert_eq!(
765 primary, resolved,
766 "with no covering font available the original must be kept"
767 );
768 }
769
770 #[test]
772 fn whitespace_does_not_trigger_a_fallback() {
773 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
774 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
775 let idx = fm.index_of(by_name).unwrap();
776 assert!(
778 fm.uncovered(idx, "a\u{00a0}b\tc")
779 .iter()
780 .all(|c| *c != '\t'),
781 "control and whitespace characters must be ignored"
782 );
783 }
784
785 #[test]
791 fn cjk_text_moves_off_a_latin_font_when_possible() {
792 let mut fm = FontManager::new();
793 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
794 return;
795 };
796 let Some(idx) = fm.index_of(latin) else {
797 return;
798 };
799 if fm.uncovered(idx, "这是中文").is_empty() {
800 return; }
802 let resolved = fm
803 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
804 .unwrap();
805 if resolved == latin {
806 return; }
808 let new_idx = fm.index_of(resolved).unwrap();
809 assert!(
810 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
811 "the replacement must cover more of the text than the original"
812 );
813 }
814}