1pub mod builtin;
9pub mod fallback;
10pub mod metrics;
11pub mod subset;
12
13pub use metrics::{unicode_to_winansi, winansi_to_char, StandardFontMetrics};
14use std::collections::HashMap;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17pub struct FontRegistry {
19 fonts: HashMap<FontKey, FontData>,
20}
21
22#[derive(Debug, Clone, Hash, PartialEq, Eq)]
23pub struct FontKey {
24 pub family: String,
25 pub weight: u32,
26 pub italic: bool,
27}
28
29#[derive(Debug, Clone)]
30pub enum FontData {
31 Standard(StandardFont),
33 Custom {
35 data: Vec<u8>,
36 used_glyphs: Vec<u16>,
38 metrics: Option<CustomFontMetrics>,
40 },
41}
42
43#[derive(Debug, Clone)]
45pub struct CustomFontMetrics {
46 pub units_per_em: u16,
47 pub advance_widths: HashMap<char, u16>,
48 pub default_advance: u16,
49 pub ascender: i16,
50 pub descender: i16,
51 pub glyph_ids: HashMap<char, u16>,
53}
54
55impl CustomFontMetrics {
56 pub fn char_width(&self, ch: char, font_size: f64) -> f64 {
58 let w = self
59 .advance_widths
60 .get(&ch)
61 .copied()
62 .unwrap_or(self.default_advance);
63 (w as f64 / self.units_per_em as f64) * font_size
64 }
65
66 pub fn from_font_data(data: &[u8]) -> Option<Self> {
68 let face = ttf_parser::Face::parse(data, 0).ok()?;
69 let units_per_em = face.units_per_em();
70 let ascender = face.ascender();
71 let descender = face.descender();
72
73 let mut advance_widths = HashMap::new();
74 let mut glyph_ids = HashMap::new();
75 let mut default_advance = 0u16;
76
77 if let Some(cmap) = face.tables().cmap {
79 for subtable in cmap.subtables {
80 if !subtable.is_unicode() {
81 continue;
82 }
83 subtable.codepoints(|code| {
84 if let Some(ch) = char::from_u32(code) {
85 if let Some(glyph_id) = face.glyph_index(ch) {
86 let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0);
87 advance_widths.insert(ch, advance);
88 glyph_ids.insert(ch, glyph_id.0);
89 if ch == ' ' {
90 default_advance = advance;
91 }
92 }
93 }
94 });
95 }
96 }
97
98 if default_advance == 0 {
99 default_advance = units_per_em / 2;
100 }
101
102 Some(CustomFontMetrics {
103 units_per_em,
104 advance_widths,
105 default_advance,
106 ascender,
107 descender,
108 glyph_ids,
109 })
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
115pub enum StandardFont {
116 Helvetica,
117 HelveticaBold,
118 HelveticaOblique,
119 HelveticaBoldOblique,
120 TimesRoman,
121 TimesBold,
122 TimesItalic,
123 TimesBoldItalic,
124 Courier,
125 CourierBold,
126 CourierOblique,
127 CourierBoldOblique,
128 Symbol,
129 ZapfDingbats,
130}
131
132impl FontData {
133 pub fn has_char(&self, ch: char) -> bool {
135 match self {
136 FontData::Custom {
137 metrics: Some(m), ..
138 } => m.glyph_ids.contains_key(&ch),
139 FontData::Custom { metrics: None, .. } => false,
140 FontData::Standard(_) => {
141 unicode_to_winansi(ch).is_some() || (ch as u32) >= 32 && (ch as u32) <= 255
142 }
143 }
144 }
145}
146
147impl StandardFont {
148 pub fn liberation_family(&self) -> Option<&'static str> {
153 match self {
154 Self::Helvetica
155 | Self::HelveticaBold
156 | Self::HelveticaOblique
157 | Self::HelveticaBoldOblique => Some("Liberation Sans"),
158 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
159 Some("Liberation Serif")
160 }
161 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
162 Some("Liberation Mono")
163 }
164 Self::Symbol | Self::ZapfDingbats => None,
165 }
166 }
167
168 pub fn descriptor_flags(&self) -> u32 {
171 match self {
172 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
173 32 | 2
174 }
175 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
176 32 | 1
177 }
178 _ => 32,
179 }
180 }
181
182 pub fn pdf_name(&self) -> &'static str {
184 match self {
185 Self::Helvetica => "Helvetica",
186 Self::HelveticaBold => "Helvetica-Bold",
187 Self::HelveticaOblique => "Helvetica-Oblique",
188 Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
189 Self::TimesRoman => "Times-Roman",
190 Self::TimesBold => "Times-Bold",
191 Self::TimesItalic => "Times-Italic",
192 Self::TimesBoldItalic => "Times-BoldItalic",
193 Self::Courier => "Courier",
194 Self::CourierBold => "Courier-Bold",
195 Self::CourierOblique => "Courier-Oblique",
196 Self::CourierBoldOblique => "Courier-BoldOblique",
197 Self::Symbol => "Symbol",
198 Self::ZapfDingbats => "ZapfDingbats",
199 }
200 }
201}
202
203impl Default for FontRegistry {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl FontRegistry {
210 pub fn new() -> Self {
211 let mut fonts = HashMap::new();
212
213 let standard_mappings = vec![
214 (("Helvetica", 400, false), StandardFont::Helvetica),
215 (("Helvetica", 700, false), StandardFont::HelveticaBold),
216 (("Helvetica", 400, true), StandardFont::HelveticaOblique),
217 (("Helvetica", 700, true), StandardFont::HelveticaBoldOblique),
218 (("Times", 400, false), StandardFont::TimesRoman),
219 (("Times", 700, false), StandardFont::TimesBold),
220 (("Times", 400, true), StandardFont::TimesItalic),
221 (("Times", 700, true), StandardFont::TimesBoldItalic),
222 (("Courier", 400, false), StandardFont::Courier),
223 (("Courier", 700, false), StandardFont::CourierBold),
224 (("Courier", 400, true), StandardFont::CourierOblique),
225 (("Courier", 700, true), StandardFont::CourierBoldOblique),
226 ];
227
228 for ((family, weight, italic), font) in standard_mappings {
229 fonts.insert(
230 FontKey {
231 family: family.to_string(),
232 weight,
233 italic,
234 },
235 FontData::Standard(font),
236 );
237 }
238
239 let mut registry = Self { fonts };
240 builtin::register_builtin_fonts(&mut registry);
241 registry
242 }
243
244 pub fn resolve(&self, families: &str, weight: u32, italic: bool) -> &FontData {
250 let snapped_weight = if weight >= 600 { 700 } else { 400 };
251
252 for family in families.split(',') {
253 let family = family.trim().trim_matches('"').trim_matches('\'');
254 if family.is_empty() {
255 continue;
256 }
257
258 let key = FontKey {
260 family: family.to_string(),
261 weight,
262 italic,
263 };
264 if let Some(font) = self.fonts.get(&key) {
265 return font;
266 }
267
268 let key = FontKey {
270 family: family.to_string(),
271 weight: snapped_weight,
272 italic,
273 };
274 if let Some(font) = self.fonts.get(&key) {
275 return font;
276 }
277
278 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
280 let key = FontKey {
281 family: family.to_string(),
282 weight: opposite_weight,
283 italic,
284 };
285 if let Some(font) = self.fonts.get(&key) {
286 return font;
287 }
288 }
289
290 let key = FontKey {
292 family: "Helvetica".to_string(),
293 weight: snapped_weight,
294 italic,
295 };
296 self.fonts.get(&key).unwrap_or_else(|| {
297 self.fonts
298 .get(&FontKey {
299 family: "Helvetica".to_string(),
300 weight: 400,
301 italic: false,
302 })
303 .expect("Helvetica must be registered")
304 })
305 }
306
307 pub fn resolve_for_char(
313 &self,
314 families: &str,
315 ch: char,
316 weight: u32,
317 italic: bool,
318 ) -> (&FontData, String) {
319 let snapped_weight = if weight >= 600 { 700 } else { 400 };
320
321 if ch.is_control() {
331 for family in families.split(',') {
332 let family = family.trim().trim_matches('"').trim_matches('\'');
333 if family.is_empty() {
334 continue;
335 }
336 for w in [
337 weight,
338 snapped_weight,
339 if snapped_weight == 700 { 400 } else { 700 },
340 ] {
341 let key = FontKey {
342 family: family.to_string(),
343 weight: w,
344 italic,
345 };
346 if let Some(font) = self.fonts.get(&key) {
347 return (font, family.to_string());
348 }
349 }
350 }
351 }
354
355 for family in families.split(',') {
356 let family = family.trim().trim_matches('"').trim_matches('\'');
357 if family.is_empty() {
358 continue;
359 }
360
361 let key = FontKey {
363 family: family.to_string(),
364 weight,
365 italic,
366 };
367 if let Some(font) = self.fonts.get(&key) {
368 if font.has_char(ch) {
369 return (font, family.to_string());
370 }
371 }
372
373 let key = FontKey {
375 family: family.to_string(),
376 weight: snapped_weight,
377 italic,
378 };
379 if let Some(font) = self.fonts.get(&key) {
380 if font.has_char(ch) {
381 return (font, family.to_string());
382 }
383 }
384
385 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
387 let key = FontKey {
388 family: family.to_string(),
389 weight: opposite_weight,
390 italic,
391 };
392 if let Some(font) = self.fonts.get(&key) {
393 if font.has_char(ch) {
394 return (font, family.to_string());
395 }
396 }
397 }
398
399 let builtin_key = FontKey {
401 family: "Noto Sans".to_string(),
402 weight: snapped_weight,
403 italic: false,
404 };
405 if let Some(font) = self.fonts.get(&builtin_key) {
406 if font.has_char(ch) {
407 return (font, "Noto Sans".to_string());
408 }
409 }
410
411 let key = FontKey {
413 family: "Helvetica".to_string(),
414 weight: snapped_weight,
415 italic,
416 };
417 let font = self.fonts.get(&key).unwrap_or_else(|| {
418 self.fonts
419 .get(&FontKey {
420 family: "Helvetica".to_string(),
421 weight: 400,
422 italic: false,
423 })
424 .expect("Helvetica must be registered")
425 });
426 (font, "Helvetica".to_string())
427 }
428
429 pub fn register(&mut self, family: &str, weight: u32, italic: bool, data: Vec<u8>) {
431 let metrics = CustomFontMetrics::from_font_data(&data);
432 self.fonts.insert(
433 FontKey {
434 family: family.to_string(),
435 weight,
436 italic,
437 },
438 FontData::Custom {
439 data,
440 used_glyphs: Vec::new(),
441 metrics,
442 },
443 );
444 }
445
446 pub fn iter(&self) -> impl Iterator<Item = (&FontKey, &FontData)> {
448 self.fonts.iter()
449 }
450}
451
452pub struct FontContext {
455 registry: FontRegistry,
456 sentinel_digit_count: u32,
460 saw_page_sentinel: AtomicBool,
466}
467
468impl Default for FontContext {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl FontContext {
475 pub fn new() -> Self {
476 Self {
477 registry: FontRegistry::new(),
478 sentinel_digit_count: 2,
479 saw_page_sentinel: AtomicBool::new(false),
480 }
481 }
482
483 pub fn sentinel_digit_count(&self) -> u32 {
485 self.sentinel_digit_count
486 }
487
488 pub fn set_sentinel_digit_count(&mut self, count: u32) {
490 self.sentinel_digit_count = count;
491 }
492
493 pub fn saw_page_sentinel(&self) -> bool {
496 self.saw_page_sentinel.load(Ordering::Relaxed)
497 }
498
499 pub fn reset_page_sentinel(&self) {
501 self.saw_page_sentinel.store(false, Ordering::Relaxed);
502 }
503
504 pub fn char_width(
509 &self,
510 ch: char,
511 family: &str,
512 weight: u32,
513 italic: bool,
514 font_size: f64,
515 ) -> f64 {
516 if ch == crate::layout::PAGE_NUMBER_SENTINEL || ch == crate::layout::TOTAL_PAGES_SENTINEL {
519 self.saw_page_sentinel.store(true, Ordering::Relaxed);
520 return self.char_width('0', family, weight, italic, font_size)
521 * self.sentinel_digit_count as f64;
522 }
523
524 let font_data = if !family.contains(',') {
527 let primary = self.registry.resolve(family, weight, italic);
528 if ch.is_control() || primary.has_char(ch) {
529 primary
530 } else {
531 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
532 data
533 }
534 } else {
535 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
536 data
537 };
538 match font_data {
539 FontData::Standard(std_font) => std_font.metrics().char_width(ch, font_size),
540 FontData::Custom {
541 metrics: Some(m), ..
542 } => m.char_width(ch, font_size),
543 FontData::Custom { metrics: None, .. } => {
544 StandardFont::Helvetica.metrics().char_width(ch, font_size)
545 }
546 }
547 }
548
549 pub fn measure_string(
551 &self,
552 text: &str,
553 family: &str,
554 weight: u32,
555 italic: bool,
556 font_size: f64,
557 letter_spacing: f64,
558 ) -> f64 {
559 let mut width = 0.0;
560 for ch in text.chars() {
561 width += self.char_width(ch, family, weight, italic, font_size) + letter_spacing;
562 }
563 width
564 }
565
566 pub fn resolve(&self, family: &str, weight: u32, italic: bool) -> &FontData {
568 self.registry.resolve(family, weight, italic)
569 }
570
571 pub fn registry(&self) -> &FontRegistry {
573 &self.registry
574 }
575
576 pub fn registry_mut(&mut self) -> &mut FontRegistry {
578 &mut self.registry
579 }
580
581 pub fn font_data(&self, family: &str, weight: u32, italic: bool) -> Option<&[u8]> {
584 let font_data = self.registry.resolve(family, weight, italic);
585 match font_data {
586 FontData::Custom { data, .. } => Some(data),
587 FontData::Standard(_) => None,
588 }
589 }
590
591 pub fn baseline_metrics(&self, family: &str, weight: u32, italic: bool) -> (f64, f64) {
602 let font_data = self.registry.resolve(family, weight, italic);
603 match font_data {
604 FontData::Custom {
605 metrics: Some(m), ..
606 } if m.units_per_em > 0 && m.ascender > 0 => (
607 m.ascender as f64 / m.units_per_em as f64,
608 (-(m.descender as f64)).max(0.0) / m.units_per_em as f64,
609 ),
610 FontData::Standard(sf) => standard_baseline_metrics(sf),
611 _ => (0.9053, 0.2119), }
613 }
614
615 pub fn units_per_em(&self, family: &str, weight: u32, italic: bool) -> u16 {
617 let font_data = self.registry.resolve(family, weight, italic);
618 match font_data {
619 FontData::Custom {
620 metrics: Some(m), ..
621 } => m.units_per_em,
622 FontData::Custom { metrics: None, .. } => 1000,
623 FontData::Standard(_) => 1000,
624 }
625 }
626}
627
628fn standard_baseline_metrics(sf: &StandardFont) -> (f64, f64) {
634 match sf {
635 StandardFont::Helvetica
636 | StandardFont::HelveticaBold
637 | StandardFont::HelveticaOblique
638 | StandardFont::HelveticaBoldOblique => (1854.0 / 2048.0, 434.0 / 2048.0),
639 StandardFont::TimesRoman
640 | StandardFont::TimesBold
641 | StandardFont::TimesItalic
642 | StandardFont::TimesBoldItalic => (1825.0 / 2048.0, 443.0 / 2048.0),
643 StandardFont::Courier
644 | StandardFont::CourierBold
645 | StandardFont::CourierOblique
646 | StandardFont::CourierBoldOblique => (1705.0 / 2048.0, 615.0 / 2048.0),
647 _ => (1854.0 / 2048.0, 434.0 / 2048.0),
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn test_font_context_helvetica() {
657 let ctx = FontContext::new();
658 let w = ctx.char_width(' ', "Helvetica", 400, false, 12.0);
659 assert!((w - 3.336).abs() < 0.001);
660 }
661
662 #[test]
663 fn test_font_context_bold_wider() {
664 let ctx = FontContext::new();
665 let regular = ctx.char_width('A', "Helvetica", 400, false, 12.0);
666 let bold = ctx.char_width('A', "Helvetica", 700, false, 12.0);
667 assert!(bold > regular, "Bold A should be wider than regular A");
668 }
669
670 #[test]
671 fn test_font_context_measure_string() {
672 let ctx = FontContext::new();
673 let w = ctx.measure_string("Hello", "Helvetica", 400, false, 12.0, 0.0);
674 assert!(w > 0.0);
675 }
676
677 #[test]
678 fn test_font_context_fallback() {
679 let ctx = FontContext::new();
680 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
681 let w2 = ctx.char_width('A', "UnknownFont", 400, false, 12.0);
682 assert!((w1 - w2).abs() < 0.001);
683 }
684
685 #[test]
686 fn test_font_context_weight_resolution() {
687 let ctx = FontContext::new();
688 let w700 = ctx.char_width('A', "Helvetica", 700, false, 12.0);
689 let w800 = ctx.char_width('A', "Helvetica", 800, false, 12.0);
690 assert!((w700 - w800).abs() < 0.001);
691 }
692
693 #[test]
694 fn test_font_fallback_chain_first_match() {
695 let ctx = FontContext::new();
696 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
697 let w2 = ctx.char_width('A', "Times, Helvetica", 400, false, 12.0);
698 assert!((w1 - w2).abs() < 0.001, "Should use Times (first in chain)");
699 }
700
701 #[test]
702 fn test_font_fallback_chain_second_match() {
703 let ctx = FontContext::new();
704 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
705 let w2 = ctx.char_width('A', "Missing, Helvetica", 400, false, 12.0);
706 assert!((w1 - w2).abs() < 0.001, "Should fall back to Helvetica");
707 }
708
709 #[test]
710 fn test_font_fallback_chain_all_missing() {
711 let ctx = FontContext::new();
712 let w = ctx.char_width('A', "Missing, AlsoMissing", 400, false, 12.0);
716 assert!(w > 0.0, "Should still produce a valid width from fallback");
717 }
718
719 #[test]
720 fn test_font_fallback_chain_quoted_families() {
721 let ctx = FontContext::new();
722 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
723 let w2 = ctx.char_width('A', "'Times', \"Helvetica\"", 400, false, 12.0);
724 assert!((w1 - w2).abs() < 0.001, "Should strip quotes and use Times");
725 }
726
727 #[test]
728 fn test_builtin_noto_sans_registered() {
729 let registry = FontRegistry::new();
730 let font = registry.resolve("Noto Sans", 400, false);
731 assert!(
732 matches!(font, FontData::Custom { .. }),
733 "Noto Sans should be registered as a custom font"
734 );
735 assert!(
736 font.has_char('\u{041F}'),
737 "Noto Sans should have Cyrillic П"
738 );
739 assert!(font.has_char('\u{03B1}'), "Noto Sans should have Greek α");
740 }
741
742 #[test]
743 fn test_builtin_noto_sans_fallback_for_cyrillic() {
744 let registry = FontRegistry::new();
745 let (font, family) = registry.resolve_for_char("Helvetica", '\u{041F}', 400, false);
746 assert_eq!(
747 family, "Noto Sans",
748 "Cyrillic should fall back to Noto Sans"
749 );
750 assert!(matches!(font, FontData::Custom { .. }));
751 }
752
753 #[test]
754 fn test_font_fallback_single_family_unchanged() {
755 let ctx = FontContext::new();
756 let w1 = ctx.char_width('A', "Courier", 400, false, 12.0);
757 let w2 = ctx.char_width('A', "Courier", 400, false, 12.0);
758 assert!(
759 (w1 - w2).abs() < 0.001,
760 "Single family should work as before"
761 );
762 }
763}