1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::error::{LayoutError, Result};
10use crate::output::FontId;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14struct FontKey {
15 family: String,
16 bold: bool,
17 italic: bool,
18}
19
20#[derive(Debug, Clone, Copy)]
22pub struct FontMetrics {
23 pub ascent: f64,
25 pub descent: f64,
27 pub line_gap: f64,
29 pub units_per_em: u16,
31}
32
33#[derive(Debug, Clone)]
35pub struct ShapedText {
36 pub glyph_ids: Vec<u16>,
38 pub advances: Vec<f64>,
40 pub width: f64,
42}
43
44struct LoadedFont {
46 id: FontId,
47 family: String,
48 bold: bool,
49 italic: bool,
50 data: Arc<Vec<u8>>,
51 face_index: u32,
52 units_per_em: u16,
53 ascender: i16,
55 descender: i16,
56 line_gap: i16,
57 shaper_data: harfrust::ShaperData,
60}
61
62pub struct FontManager {
64 db: fontdb::Database,
65 cache: HashMap<FontKey, usize>,
67 fonts: Vec<LoadedFont>,
69 next_id: u32,
71 coverage_fallbacks: HashMap<(bool, bool), Vec<usize>>,
79 coverage_misses: HashSet<char>,
82}
83
84const BROAD_COVERAGE_FAMILIES: &[&str] = &[
89 "Noto Sans CJK SC",
91 "Noto Sans CJK JP",
92 "Noto Sans CJK KR",
93 "Noto Sans CJK TC",
94 "Noto Serif CJK SC",
95 "Source Han Sans SC",
96 "WenQuanYi Zen Hei",
97 "WenQuanYi Micro Hei",
98 "PingFang SC",
100 "PingFang TC",
101 "Hiragino Sans",
102 "Hiragino Kaku Gothic ProN",
103 "Apple SD Gothic Neo",
104 "Songti SC",
105 "STHeiti",
106 "Microsoft YaHei",
108 "Microsoft JhengHei",
109 "SimSun",
110 "SimHei",
111 "NSimSun",
112 "Yu Gothic",
113 "MS Gothic",
114 "Meiryo",
115 "Malgun Gothic",
116 "Arial Unicode MS",
118 "DejaVu Sans",
119];
120
121impl Default for FontManager {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl FontManager {
128 pub fn new() -> Self {
133 let mut db = fontdb::Database::new();
134
135 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
137 db.load_font_data(data.to_vec());
138 }
139
140 db.load_system_fonts();
142
143 FontManager {
144 db,
145 cache: HashMap::new(),
146 fonts: Vec::new(),
147 next_id: 0,
148 coverage_fallbacks: HashMap::new(),
149 coverage_misses: HashSet::new(),
150 }
151 }
152
153 pub fn new_deterministic() -> Result<Self> {
160 #[cfg(feature = "bundled-fonts")]
161 {
162 let mut db = fontdb::Database::new();
163 for (_family, data) in crate::bundled_fonts::bundled_font_data() {
164 db.load_font_data(data.to_vec());
165 }
166
167 Ok(FontManager {
168 db,
169 cache: HashMap::new(),
170 fonts: Vec::new(),
171 next_id: 0,
172 coverage_fallbacks: HashMap::new(),
173 coverage_misses: HashSet::new(),
174 })
175 }
176
177 #[cfg(not(feature = "bundled-fonts"))]
178 {
179 Err(LayoutError::Layout(
180 "deterministic font mode requires the 'bundled-fonts' feature".to_string(),
181 ))
182 }
183 }
184
185 pub fn load_additional_fonts(&mut self, font_files: &[crate::input::FontFile]) {
190 for font_file in font_files {
191 self.db.load_font_data(font_file.data.clone());
192 }
193 self.cache.clear();
195 }
196
197 pub fn new_with_fonts(fonts: Vec<(String, Vec<u8>)>) -> Self {
202 let mut db = fontdb::Database::new();
203 for (_name, data) in &fonts {
204 db.load_font_data(data.clone());
205 }
206 FontManager {
207 db,
208 cache: HashMap::new(),
209 fonts: Vec::new(),
210 next_id: 0,
211 coverage_fallbacks: HashMap::new(),
212 coverage_misses: HashSet::new(),
213 }
214 }
215
216 pub fn resolve_font_for_text(
233 &mut self,
234 family: Option<&str>,
235 bold: bool,
236 italic: bool,
237 text: &str,
238 ) -> Result<FontId> {
239 let primary = self.resolve_font(family, bold, italic)?;
240
241 let Some(idx) = self.index_of(primary) else {
242 return Ok(primary);
243 };
244 let missing = self.uncovered(idx, text);
245 if missing.is_empty() {
246 return Ok(primary);
247 }
248
249 match self.font_covering(&missing, bold, italic) {
250 None => Ok(primary),
253 Some(id) => Ok(id),
254 }
255 }
256
257 fn uncovered(&self, idx: usize, text: &str) -> Vec<char> {
262 let font = &self.fonts[idx];
263 let Ok(face) = ttf_parser::Face::parse(&font.data, font.face_index) else {
264 return Vec::new();
265 };
266 let mut seen = HashSet::new();
267 text.chars()
268 .filter(|&ch| !ch.is_whitespace() && !ch.is_control())
269 .filter(|&ch| face.glyph_index(ch).is_none())
270 .filter(|&ch| seen.insert(ch))
271 .collect()
272 }
273
274 fn covers(&self, idx: usize, ch: char) -> bool {
276 let font = &self.fonts[idx];
277 ttf_parser::Face::parse(&font.data, font.face_index)
278 .map(|face| face.glyph_index(ch).is_some())
279 .unwrap_or(false)
280 }
281
282 fn font_covering(&mut self, missing: &[char], bold: bool, italic: bool) -> Option<FontId> {
291 if missing.iter().all(|ch| self.coverage_misses.contains(ch)) {
292 return None;
293 }
294
295 let mut best: Option<(usize, usize)> = None; let consider = |this: &Self, idx: usize, best: &mut Option<(usize, usize)>| -> bool {
297 let covered = missing.iter().filter(|&&ch| this.covers(idx, ch)).count();
298 if covered == 0 {
299 return false;
300 }
301 if best.map(|(n, _)| covered > n).unwrap_or(true) {
302 *best = Some((covered, idx));
303 }
304 covered == missing.len()
305 };
306
307 if let Some(known) = self.coverage_fallbacks.get(&(bold, italic)).cloned() {
310 for idx in known {
311 if consider(self, idx, &mut best) {
312 return Some(self.fonts[idx].id);
313 }
314 }
315 }
316
317 let candidates: Vec<String> = BROAD_COVERAGE_FAMILIES
321 .iter()
322 .map(|s| s.to_string())
323 .chain(
324 self.db
325 .faces()
326 .filter_map(|f| f.families.first().map(|(name, _)| name.clone())),
327 )
328 .collect();
329
330 for name in candidates {
331 let Ok(id) = self.resolve_font(Some(&name), bold, italic) else {
332 continue;
333 };
334 let Some(idx) = self.index_of(id) else {
335 continue;
336 };
337 let complete = consider(self, idx, &mut best);
338 if complete {
339 self.coverage_fallbacks
340 .entry((bold, italic))
341 .or_default()
342 .push(idx);
343 return Some(id);
344 }
345 }
346
347 match best {
348 Some((_, idx)) => {
349 self.coverage_fallbacks
350 .entry((bold, italic))
351 .or_default()
352 .push(idx);
353 Some(self.fonts[idx].id)
354 }
355 None => {
356 for &ch in missing {
357 self.coverage_misses.insert(ch);
358 }
359 None
360 }
361 }
362 }
363
364 fn index_of(&self, id: FontId) -> Option<usize> {
366 self.fonts.iter().position(|f| f.id == id)
367 }
368
369 pub fn resolve_font(
372 &mut self,
373 family: Option<&str>,
374 bold: bool,
375 italic: bool,
376 ) -> Result<FontId> {
377 let family_name = family.unwrap_or("Arial");
378
379 let key = FontKey {
380 family: family_name.to_string(),
381 bold,
382 italic,
383 };
384
385 if let Some(&idx) = self.cache.get(&key) {
386 return Ok(self.fonts[idx].id);
387 }
388
389 let mapped = map_font_name(family_name);
391
392 let mut fallbacks: Vec<&str> = Vec::with_capacity(10);
394 fallbacks.push(family_name);
395 for alt in mapped {
396 if *alt != family_name {
397 fallbacks.push(alt);
398 }
399 }
400 for generic in &[
401 "Carlito",
402 "Arial",
403 "Liberation Sans",
404 "Helvetica",
405 "DejaVu Sans",
406 "Noto Sans",
407 ] {
408 if !fallbacks.contains(generic) {
409 fallbacks.push(generic);
410 }
411 }
412
413 let style = if italic {
414 fontdb::Style::Italic
415 } else {
416 fontdb::Style::Normal
417 };
418 let weight = if bold {
419 fontdb::Weight::BOLD
420 } else {
421 fontdb::Weight::NORMAL
422 };
423
424 let mut found_id = None;
425 for fallback in &fallbacks {
426 let query = fontdb::Query {
427 families: &[fontdb::Family::Name(fallback)],
428 weight,
429 style,
430 stretch: fontdb::Stretch::Normal,
431 };
432
433 if let Some(id) = self.db.query(&query) {
434 found_id = Some(id);
435 break;
436 }
437 }
438
439 if found_id.is_none() {
441 for generic_family in &[
442 fontdb::Family::SansSerif,
443 fontdb::Family::Serif,
444 fontdb::Family::Monospace,
445 ] {
446 let query = fontdb::Query {
447 families: &[*generic_family],
448 weight,
449 style,
450 stretch: fontdb::Stretch::Normal,
451 };
452 if let Some(id) = self.db.query(&query) {
453 found_id = Some(id);
454 break;
455 }
456 }
457 }
458
459 let db_id = found_id.ok_or_else(|| {
460 LayoutError::FontNotFound(format!("No font found for family '{family_name}'"))
461 })?;
462
463 let font_id = FontId(self.next_id);
464 self.next_id += 1;
465
466 let (data, face_index) = self
468 .db
469 .with_face_data(db_id, |data, idx| (Arc::new(data.to_vec()), idx))
470 .ok_or_else(|| LayoutError::FontParse("Failed to load font data".into()))?;
471
472 let (units_per_em, ascender, descender, line_gap) = {
473 let face = ttf_parser::Face::parse(&data, face_index)
474 .map_err(|e| LayoutError::FontParse(format!("ttf-parser error: {e}")))?;
475 (
476 face.units_per_em(),
477 face.ascender(),
478 face.descender(),
479 face.line_gap(),
480 )
481 };
482
483 if units_per_em == 0 {
486 return Err(LayoutError::FontParse(format!(
487 "font '{family_name}' declares zero units per em"
488 )));
489 }
490
491 let shaper_data = {
492 let face = harfrust::FontRef::from_index(&data, face_index)
493 .map_err(|e| LayoutError::FontParse(format!("failed to read font face: {e}")))?;
494 harfrust::ShaperData::new(&face)
495 };
496
497 let actual_family = self
498 .db
499 .face(db_id)
500 .map(|f| {
501 f.families
502 .first()
503 .map(|(name, _)| name.clone())
504 .unwrap_or_else(|| family_name.to_string())
505 })
506 .unwrap_or_else(|| family_name.to_string());
507
508 let idx = self.fonts.len();
509 self.fonts.push(LoadedFont {
510 id: font_id,
511 family: actual_family,
512 bold,
513 italic,
514 data,
515 face_index,
516 units_per_em,
517 ascender,
518 descender,
519 line_gap,
520 shaper_data,
521 });
522 self.cache.insert(key, idx);
523
524 Ok(font_id)
525 }
526
527 pub fn metrics(&self, font_id: FontId, size_pt: f64) -> Result<FontMetrics> {
529 let font = self.get_font(font_id)?;
530 let scale = size_pt / font.units_per_em as f64;
531
532 Ok(FontMetrics {
533 ascent: font.ascender as f64 * scale,
534 descent: -(font.descender as f64) * scale, line_gap: font.line_gap as f64 * scale,
536 units_per_em: font.units_per_em,
537 })
538 }
539
540 pub fn shape_text(&self, font_id: FontId, text: &str, size_pt: f64) -> Result<ShapedText> {
542 if text.is_empty() {
545 return Ok(ShapedText {
546 glyph_ids: Vec::new(),
547 advances: Vec::new(),
548 width: 0.0,
549 });
550 }
551
552 let font = self.get_font(font_id)?;
553
554 let face = harfrust::FontRef::from_index(&font.data, font.face_index)
555 .map_err(|e| LayoutError::Shaping(format!("failed to read font face: {e}")))?;
556
557 let shaper = font.shaper_data.shaper(&face).build();
558
559 let mut buffer = harfrust::UnicodeBuffer::new();
560 buffer.push_str(text);
561 buffer.guess_segment_properties();
564
565 let output = shaper.shape(buffer, harfrust::ShapeOptions::default());
566 let infos = output.glyph_infos();
567 let positions = output.glyph_positions();
568
569 let upem = font.units_per_em as f64;
570 let scale = size_pt / upem;
571
572 let mut glyph_ids = Vec::with_capacity(infos.len());
573 let mut advances = Vec::with_capacity(positions.len());
574 let mut total_width = 0.0;
575
576 for (info, pos) in infos.iter().zip(positions.iter()) {
577 glyph_ids.push(info.glyph_id as u16);
578 let advance = pos.x_advance as f64 * scale;
579 advances.push(advance);
580 total_width += advance;
581 }
582
583 Ok(ShapedText {
584 glyph_ids,
585 advances,
586 width: total_width,
587 })
588 }
589
590 pub fn font_data(&self, font_id: FontId) -> Result<crate::output::FontData> {
592 let font = self.get_font(font_id)?;
593 Ok(crate::output::FontData {
594 id: font.id,
595 family: font.family.clone(),
596 data: (*font.data).clone(),
597 face_index: font.face_index,
598 bold: font.bold,
599 italic: font.italic,
600 })
601 }
602
603 pub fn all_font_data(&self) -> Vec<crate::output::FontData> {
605 self.fonts
606 .iter()
607 .map(|f| crate::output::FontData {
608 id: f.id,
609 family: f.family.clone(),
610 data: (*f.data).clone(),
611 face_index: f.face_index,
612 bold: f.bold,
613 italic: f.italic,
614 })
615 .collect()
616 }
617
618 fn get_font(&self, font_id: FontId) -> Result<&LoadedFont> {
619 self.fonts
620 .iter()
621 .find(|f| f.id == font_id)
622 .ok_or_else(|| LayoutError::FontNotFound(format!("FontId({}) not loaded", font_id.0)))
623 }
624}
625
626fn map_font_name(name: &str) -> &[&str] {
633 match name {
634 "Calibri" => &["Calibri", "Carlito"],
635 "Calibri Light" => &["Calibri Light", "Carlito"],
636 "Cambria" => &["Cambria", "Caladea"],
637 "Cambria Math" => &["Cambria Math", "Cambria", "Caladea"],
638 "Arial" => &["Arial", "Liberation Sans", "Helvetica"],
639 "Times New Roman" => &["Times New Roman", "Liberation Serif", "Times"],
640 "Courier New" => &["Courier New", "Liberation Mono", "Courier"],
641 "Consolas" => &["Consolas", "Liberation Mono", "DejaVu Sans Mono"],
642 "Segoe UI" => &["Segoe UI", "Carlito", "Liberation Sans"],
643 "Tahoma" => &["Tahoma", "Liberation Sans", "Helvetica"],
644 "Verdana" => &["Verdana", "Liberation Sans", "DejaVu Sans"],
645 "Georgia" => &["Georgia", "Caladea", "Liberation Serif"],
646 "Palatino Linotype" => &["Palatino Linotype", "Palatino", "Liberation Serif"],
647 "Book Antiqua" => &["Book Antiqua", "Palatino", "Liberation Serif"],
648 "Garamond" => &["Garamond", "Caladea", "Liberation Serif"],
649 "Trebuchet MS" => &["Trebuchet MS", "Liberation Sans", "DejaVu Sans"],
650 "Impact" => &["Impact", "Liberation Sans", "Arial"],
651 "Comic Sans MS" => &["Comic Sans MS", "Liberation Sans", "DejaVu Sans"],
652 "Symbol" => &["Symbol", "DejaVu Sans"],
653 "Wingdings" => &["Wingdings", "Symbol"],
654 _ => &[],
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 #[cfg(feature = "bundled-fonts")]
662 use crate::bundled_fonts::bundled_font_data;
663
664 #[cfg(feature = "bundled-fonts")]
665 #[test]
666 fn deterministic_font_manager_uses_only_bundled_fonts() {
667 let mut fm = FontManager::new_deterministic().expect("bundled fonts should load");
668
669 assert_eq!(fm.db.faces().count(), bundled_font_data().len());
670 assert!(fm.resolve_font(Some("Arial"), false, false).is_ok());
671 }
672
673 #[cfg(not(feature = "bundled-fonts"))]
674 #[test]
675 fn deterministic_font_manager_requires_bundled_fonts() {
676 match FontManager::new_deterministic() {
677 Err(LayoutError::Layout(message)) => assert_eq!(
678 message,
679 "deterministic font mode requires the 'bundled-fonts' feature"
680 ),
681 _ => panic!("deterministic mode must reject a build without bundled fonts"),
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 Ok(mut fm) = FontManager::new_deterministic() else {
760 return; };
762 let primary = fm.resolve_font(Some("Carlito"), false, false).unwrap();
763 let resolved = fm
764 .resolve_font_for_text(Some("Carlito"), false, false, "这是中文")
765 .unwrap();
766 assert_eq!(
767 primary, resolved,
768 "with no covering font available the original must be kept"
769 );
770 }
771
772 #[test]
774 fn whitespace_does_not_trigger_a_fallback() {
775 let Ok(mut fm) = FontManager::new_deterministic() else {
776 return;
777 };
778 let by_name = fm.resolve_font(Some("Carlito"), false, false).unwrap();
779 let idx = fm.index_of(by_name).unwrap();
780 assert!(
782 fm.uncovered(idx, "a\u{00a0}b\tc")
783 .iter()
784 .all(|c| *c != '\t'),
785 "control and whitespace characters must be ignored"
786 );
787 }
788
789 #[test]
795 fn cjk_text_moves_off_a_latin_font_when_possible() {
796 let mut fm = FontManager::new();
797 let Ok(latin) = fm.resolve_font(Some("Liberation Serif"), false, false) else {
798 return;
799 };
800 let Some(idx) = fm.index_of(latin) else {
801 return;
802 };
803 if fm.uncovered(idx, "这是中文").is_empty() {
804 return; }
806 let resolved = fm
807 .resolve_font_for_text(Some("Liberation Serif"), false, false, "这是中文")
808 .unwrap();
809 if resolved == latin {
810 return; }
812 let new_idx = fm.index_of(resolved).unwrap();
813 assert!(
814 fm.uncovered(new_idx, "这是中文").len() < fm.uncovered(idx, "这是中文").len(),
815 "the replacement must cover more of the text than the original"
816 );
817 }
818}