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 for code in 32u32..=0xFFFF {
79 if let Some(ch) = char::from_u32(code) {
80 if let Some(glyph_id) = face.glyph_index(ch) {
81 let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0);
82 advance_widths.insert(ch, advance);
83 glyph_ids.insert(ch, glyph_id.0);
84 if ch == ' ' {
85 default_advance = advance;
86 }
87 }
88 }
89 }
90
91 if default_advance == 0 {
92 default_advance = units_per_em / 2;
93 }
94
95 Some(CustomFontMetrics {
96 units_per_em,
97 advance_widths,
98 default_advance,
99 ascender,
100 descender,
101 glyph_ids,
102 })
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
108pub enum StandardFont {
109 Helvetica,
110 HelveticaBold,
111 HelveticaOblique,
112 HelveticaBoldOblique,
113 TimesRoman,
114 TimesBold,
115 TimesItalic,
116 TimesBoldItalic,
117 Courier,
118 CourierBold,
119 CourierOblique,
120 CourierBoldOblique,
121 Symbol,
122 ZapfDingbats,
123}
124
125impl FontData {
126 pub fn has_char(&self, ch: char) -> bool {
128 match self {
129 FontData::Custom {
130 metrics: Some(m), ..
131 } => m.glyph_ids.contains_key(&ch),
132 FontData::Custom { metrics: None, .. } => false,
133 FontData::Standard(_) => {
134 unicode_to_winansi(ch).is_some() || (ch as u32) >= 32 && (ch as u32) <= 255
135 }
136 }
137 }
138}
139
140impl StandardFont {
141 pub fn liberation_family(&self) -> Option<&'static str> {
146 match self {
147 Self::Helvetica
148 | Self::HelveticaBold
149 | Self::HelveticaOblique
150 | Self::HelveticaBoldOblique => Some("Liberation Sans"),
151 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
152 Some("Liberation Serif")
153 }
154 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
155 Some("Liberation Mono")
156 }
157 Self::Symbol | Self::ZapfDingbats => None,
158 }
159 }
160
161 pub fn descriptor_flags(&self) -> u32 {
164 match self {
165 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
166 32 | 2
167 }
168 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
169 32 | 1
170 }
171 _ => 32,
172 }
173 }
174
175 pub fn pdf_name(&self) -> &'static str {
177 match self {
178 Self::Helvetica => "Helvetica",
179 Self::HelveticaBold => "Helvetica-Bold",
180 Self::HelveticaOblique => "Helvetica-Oblique",
181 Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
182 Self::TimesRoman => "Times-Roman",
183 Self::TimesBold => "Times-Bold",
184 Self::TimesItalic => "Times-Italic",
185 Self::TimesBoldItalic => "Times-BoldItalic",
186 Self::Courier => "Courier",
187 Self::CourierBold => "Courier-Bold",
188 Self::CourierOblique => "Courier-Oblique",
189 Self::CourierBoldOblique => "Courier-BoldOblique",
190 Self::Symbol => "Symbol",
191 Self::ZapfDingbats => "ZapfDingbats",
192 }
193 }
194}
195
196impl Default for FontRegistry {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl FontRegistry {
203 pub fn new() -> Self {
204 let mut fonts = HashMap::new();
205
206 let standard_mappings = vec![
207 (("Helvetica", 400, false), StandardFont::Helvetica),
208 (("Helvetica", 700, false), StandardFont::HelveticaBold),
209 (("Helvetica", 400, true), StandardFont::HelveticaOblique),
210 (("Helvetica", 700, true), StandardFont::HelveticaBoldOblique),
211 (("Times", 400, false), StandardFont::TimesRoman),
212 (("Times", 700, false), StandardFont::TimesBold),
213 (("Times", 400, true), StandardFont::TimesItalic),
214 (("Times", 700, true), StandardFont::TimesBoldItalic),
215 (("Courier", 400, false), StandardFont::Courier),
216 (("Courier", 700, false), StandardFont::CourierBold),
217 (("Courier", 400, true), StandardFont::CourierOblique),
218 (("Courier", 700, true), StandardFont::CourierBoldOblique),
219 ];
220
221 for ((family, weight, italic), font) in standard_mappings {
222 fonts.insert(
223 FontKey {
224 family: family.to_string(),
225 weight,
226 italic,
227 },
228 FontData::Standard(font),
229 );
230 }
231
232 let mut registry = Self { fonts };
233 builtin::register_builtin_fonts(&mut registry);
234 registry
235 }
236
237 pub fn resolve(&self, families: &str, weight: u32, italic: bool) -> &FontData {
243 let snapped_weight = if weight >= 600 { 700 } else { 400 };
244
245 for family in families.split(',') {
246 let family = family.trim().trim_matches('"').trim_matches('\'');
247 if family.is_empty() {
248 continue;
249 }
250
251 let key = FontKey {
253 family: family.to_string(),
254 weight,
255 italic,
256 };
257 if let Some(font) = self.fonts.get(&key) {
258 return font;
259 }
260
261 let key = FontKey {
263 family: family.to_string(),
264 weight: snapped_weight,
265 italic,
266 };
267 if let Some(font) = self.fonts.get(&key) {
268 return font;
269 }
270
271 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
273 let key = FontKey {
274 family: family.to_string(),
275 weight: opposite_weight,
276 italic,
277 };
278 if let Some(font) = self.fonts.get(&key) {
279 return font;
280 }
281 }
282
283 let key = FontKey {
285 family: "Helvetica".to_string(),
286 weight: snapped_weight,
287 italic,
288 };
289 self.fonts.get(&key).unwrap_or_else(|| {
290 self.fonts
291 .get(&FontKey {
292 family: "Helvetica".to_string(),
293 weight: 400,
294 italic: false,
295 })
296 .expect("Helvetica must be registered")
297 })
298 }
299
300 pub fn resolve_for_char(
306 &self,
307 families: &str,
308 ch: char,
309 weight: u32,
310 italic: bool,
311 ) -> (&FontData, String) {
312 let snapped_weight = if weight >= 600 { 700 } else { 400 };
313
314 if ch == crate::layout::PAGE_NUMBER_SENTINEL || ch == crate::layout::TOTAL_PAGES_SENTINEL {
324 for family in families.split(',') {
325 let family = family.trim().trim_matches('"').trim_matches('\'');
326 if family.is_empty() {
327 continue;
328 }
329 for w in [
330 weight,
331 snapped_weight,
332 if snapped_weight == 700 { 400 } else { 700 },
333 ] {
334 let key = FontKey {
335 family: family.to_string(),
336 weight: w,
337 italic,
338 };
339 if let Some(font) = self.fonts.get(&key) {
340 return (font, family.to_string());
341 }
342 }
343 }
344 }
347
348 for family in families.split(',') {
349 let family = family.trim().trim_matches('"').trim_matches('\'');
350 if family.is_empty() {
351 continue;
352 }
353
354 let key = FontKey {
356 family: family.to_string(),
357 weight,
358 italic,
359 };
360 if let Some(font) = self.fonts.get(&key) {
361 if font.has_char(ch) {
362 return (font, family.to_string());
363 }
364 }
365
366 let key = FontKey {
368 family: family.to_string(),
369 weight: snapped_weight,
370 italic,
371 };
372 if let Some(font) = self.fonts.get(&key) {
373 if font.has_char(ch) {
374 return (font, family.to_string());
375 }
376 }
377
378 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
380 let key = FontKey {
381 family: family.to_string(),
382 weight: opposite_weight,
383 italic,
384 };
385 if let Some(font) = self.fonts.get(&key) {
386 if font.has_char(ch) {
387 return (font, family.to_string());
388 }
389 }
390 }
391
392 let builtin_key = FontKey {
394 family: "Noto Sans".to_string(),
395 weight: snapped_weight,
396 italic: false,
397 };
398 if let Some(font) = self.fonts.get(&builtin_key) {
399 if font.has_char(ch) {
400 return (font, "Noto Sans".to_string());
401 }
402 }
403
404 let key = FontKey {
406 family: "Helvetica".to_string(),
407 weight: snapped_weight,
408 italic,
409 };
410 let font = self.fonts.get(&key).unwrap_or_else(|| {
411 self.fonts
412 .get(&FontKey {
413 family: "Helvetica".to_string(),
414 weight: 400,
415 italic: false,
416 })
417 .expect("Helvetica must be registered")
418 });
419 (font, "Helvetica".to_string())
420 }
421
422 pub fn register(&mut self, family: &str, weight: u32, italic: bool, data: Vec<u8>) {
424 let metrics = CustomFontMetrics::from_font_data(&data);
425 self.fonts.insert(
426 FontKey {
427 family: family.to_string(),
428 weight,
429 italic,
430 },
431 FontData::Custom {
432 data,
433 used_glyphs: Vec::new(),
434 metrics,
435 },
436 );
437 }
438
439 pub fn iter(&self) -> impl Iterator<Item = (&FontKey, &FontData)> {
441 self.fonts.iter()
442 }
443}
444
445pub struct FontContext {
448 registry: FontRegistry,
449 sentinel_digit_count: u32,
453 saw_page_sentinel: AtomicBool,
459}
460
461impl Default for FontContext {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl FontContext {
468 pub fn new() -> Self {
469 Self {
470 registry: FontRegistry::new(),
471 sentinel_digit_count: 2,
472 saw_page_sentinel: AtomicBool::new(false),
473 }
474 }
475
476 pub fn sentinel_digit_count(&self) -> u32 {
478 self.sentinel_digit_count
479 }
480
481 pub fn set_sentinel_digit_count(&mut self, count: u32) {
483 self.sentinel_digit_count = count;
484 }
485
486 pub fn saw_page_sentinel(&self) -> bool {
489 self.saw_page_sentinel.load(Ordering::Relaxed)
490 }
491
492 pub fn reset_page_sentinel(&self) {
494 self.saw_page_sentinel.store(false, Ordering::Relaxed);
495 }
496
497 pub fn char_width(
502 &self,
503 ch: char,
504 family: &str,
505 weight: u32,
506 italic: bool,
507 font_size: f64,
508 ) -> f64 {
509 if ch == crate::layout::PAGE_NUMBER_SENTINEL || ch == crate::layout::TOTAL_PAGES_SENTINEL {
512 self.saw_page_sentinel.store(true, Ordering::Relaxed);
513 return self.char_width('0', family, weight, italic, font_size)
514 * self.sentinel_digit_count as f64;
515 }
516
517 let font_data = if !family.contains(',') {
520 let primary = self.registry.resolve(family, weight, italic);
521 if ch.is_whitespace() || primary.has_char(ch) {
522 primary
523 } else {
524 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
525 data
526 }
527 } else {
528 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
529 data
530 };
531 match font_data {
532 FontData::Standard(std_font) => std_font.metrics().char_width(ch, font_size),
533 FontData::Custom {
534 metrics: Some(m), ..
535 } => m.char_width(ch, font_size),
536 FontData::Custom { metrics: None, .. } => {
537 StandardFont::Helvetica.metrics().char_width(ch, font_size)
538 }
539 }
540 }
541
542 pub fn measure_string(
544 &self,
545 text: &str,
546 family: &str,
547 weight: u32,
548 italic: bool,
549 font_size: f64,
550 letter_spacing: f64,
551 ) -> f64 {
552 let mut width = 0.0;
553 for ch in text.chars() {
554 width += self.char_width(ch, family, weight, italic, font_size) + letter_spacing;
555 }
556 width
557 }
558
559 pub fn resolve(&self, family: &str, weight: u32, italic: bool) -> &FontData {
561 self.registry.resolve(family, weight, italic)
562 }
563
564 pub fn registry(&self) -> &FontRegistry {
566 &self.registry
567 }
568
569 pub fn registry_mut(&mut self) -> &mut FontRegistry {
571 &mut self.registry
572 }
573
574 pub fn font_data(&self, family: &str, weight: u32, italic: bool) -> Option<&[u8]> {
577 let font_data = self.registry.resolve(family, weight, italic);
578 match font_data {
579 FontData::Custom { data, .. } => Some(data),
580 FontData::Standard(_) => None,
581 }
582 }
583
584 pub fn baseline_metrics(&self, family: &str, weight: u32, italic: bool) -> (f64, f64) {
595 let font_data = self.registry.resolve(family, weight, italic);
596 match font_data {
597 FontData::Custom {
598 metrics: Some(m), ..
599 } if m.units_per_em > 0 && m.ascender > 0 => (
600 m.ascender as f64 / m.units_per_em as f64,
601 (-(m.descender as f64)).max(0.0) / m.units_per_em as f64,
602 ),
603 FontData::Standard(sf) => standard_baseline_metrics(sf),
604 _ => (0.9053, 0.2119), }
606 }
607
608 pub fn units_per_em(&self, family: &str, weight: u32, italic: bool) -> u16 {
610 let font_data = self.registry.resolve(family, weight, italic);
611 match font_data {
612 FontData::Custom {
613 metrics: Some(m), ..
614 } => m.units_per_em,
615 FontData::Custom { metrics: None, .. } => 1000,
616 FontData::Standard(_) => 1000,
617 }
618 }
619}
620
621fn standard_baseline_metrics(sf: &StandardFont) -> (f64, f64) {
627 match sf {
628 StandardFont::Helvetica
629 | StandardFont::HelveticaBold
630 | StandardFont::HelveticaOblique
631 | StandardFont::HelveticaBoldOblique => (1854.0 / 2048.0, 434.0 / 2048.0),
632 StandardFont::TimesRoman
633 | StandardFont::TimesBold
634 | StandardFont::TimesItalic
635 | StandardFont::TimesBoldItalic => (1825.0 / 2048.0, 443.0 / 2048.0),
636 StandardFont::Courier
637 | StandardFont::CourierBold
638 | StandardFont::CourierOblique
639 | StandardFont::CourierBoldOblique => (1705.0 / 2048.0, 615.0 / 2048.0),
640 _ => (1854.0 / 2048.0, 434.0 / 2048.0),
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 #[test]
649 fn test_font_context_helvetica() {
650 let ctx = FontContext::new();
651 let w = ctx.char_width(' ', "Helvetica", 400, false, 12.0);
652 assert!((w - 3.336).abs() < 0.001);
653 }
654
655 #[test]
656 fn test_font_context_bold_wider() {
657 let ctx = FontContext::new();
658 let regular = ctx.char_width('A', "Helvetica", 400, false, 12.0);
659 let bold = ctx.char_width('A', "Helvetica", 700, false, 12.0);
660 assert!(bold > regular, "Bold A should be wider than regular A");
661 }
662
663 #[test]
664 fn test_font_context_measure_string() {
665 let ctx = FontContext::new();
666 let w = ctx.measure_string("Hello", "Helvetica", 400, false, 12.0, 0.0);
667 assert!(w > 0.0);
668 }
669
670 #[test]
671 fn test_font_context_fallback() {
672 let ctx = FontContext::new();
673 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
674 let w2 = ctx.char_width('A', "UnknownFont", 400, false, 12.0);
675 assert!((w1 - w2).abs() < 0.001);
676 }
677
678 #[test]
679 fn test_font_context_weight_resolution() {
680 let ctx = FontContext::new();
681 let w700 = ctx.char_width('A', "Helvetica", 700, false, 12.0);
682 let w800 = ctx.char_width('A', "Helvetica", 800, false, 12.0);
683 assert!((w700 - w800).abs() < 0.001);
684 }
685
686 #[test]
687 fn test_font_fallback_chain_first_match() {
688 let ctx = FontContext::new();
689 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
690 let w2 = ctx.char_width('A', "Times, Helvetica", 400, false, 12.0);
691 assert!((w1 - w2).abs() < 0.001, "Should use Times (first in chain)");
692 }
693
694 #[test]
695 fn test_font_fallback_chain_second_match() {
696 let ctx = FontContext::new();
697 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
698 let w2 = ctx.char_width('A', "Missing, Helvetica", 400, false, 12.0);
699 assert!((w1 - w2).abs() < 0.001, "Should fall back to Helvetica");
700 }
701
702 #[test]
703 fn test_font_fallback_chain_all_missing() {
704 let ctx = FontContext::new();
705 let w = ctx.char_width('A', "Missing, AlsoMissing", 400, false, 12.0);
709 assert!(w > 0.0, "Should still produce a valid width from fallback");
710 }
711
712 #[test]
713 fn test_font_fallback_chain_quoted_families() {
714 let ctx = FontContext::new();
715 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
716 let w2 = ctx.char_width('A', "'Times', \"Helvetica\"", 400, false, 12.0);
717 assert!((w1 - w2).abs() < 0.001, "Should strip quotes and use Times");
718 }
719
720 #[test]
721 fn test_builtin_noto_sans_registered() {
722 let registry = FontRegistry::new();
723 let font = registry.resolve("Noto Sans", 400, false);
724 assert!(
725 matches!(font, FontData::Custom { .. }),
726 "Noto Sans should be registered as a custom font"
727 );
728 assert!(
729 font.has_char('\u{041F}'),
730 "Noto Sans should have Cyrillic П"
731 );
732 assert!(font.has_char('\u{03B1}'), "Noto Sans should have Greek α");
733 }
734
735 #[test]
736 fn test_builtin_noto_sans_fallback_for_cyrillic() {
737 let registry = FontRegistry::new();
738 let (font, family) = registry.resolve_for_char("Helvetica", '\u{041F}', 400, false);
739 assert_eq!(
740 family, "Noto Sans",
741 "Cyrillic should fall back to Noto Sans"
742 );
743 assert!(matches!(font, FontData::Custom { .. }));
744 }
745
746 #[test]
747 fn test_font_fallback_single_family_unchanged() {
748 let ctx = FontContext::new();
749 let w1 = ctx.char_width('A', "Courier", 400, false, 12.0);
750 let w2 = ctx.char_width('A', "Courier", 400, false, 12.0);
751 assert!(
752 (w1 - w2).abs() < 0.001,
753 "Single family should work as before"
754 );
755 }
756}