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;
15
16pub struct FontRegistry {
18 fonts: HashMap<FontKey, FontData>,
19}
20
21#[derive(Debug, Clone, Hash, PartialEq, Eq)]
22pub struct FontKey {
23 pub family: String,
24 pub weight: u32,
25 pub italic: bool,
26}
27
28#[derive(Debug, Clone)]
29pub enum FontData {
30 Standard(StandardFont),
32 Custom {
34 data: Vec<u8>,
35 used_glyphs: Vec<u16>,
37 metrics: Option<CustomFontMetrics>,
39 },
40}
41
42#[derive(Debug, Clone)]
44pub struct CustomFontMetrics {
45 pub units_per_em: u16,
46 pub advance_widths: HashMap<char, u16>,
47 pub default_advance: u16,
48 pub ascender: i16,
49 pub descender: i16,
50 pub glyph_ids: HashMap<char, u16>,
52}
53
54impl CustomFontMetrics {
55 pub fn char_width(&self, ch: char, font_size: f64) -> f64 {
57 let w = self
58 .advance_widths
59 .get(&ch)
60 .copied()
61 .unwrap_or(self.default_advance);
62 (w as f64 / self.units_per_em as f64) * font_size
63 }
64
65 pub fn from_font_data(data: &[u8]) -> Option<Self> {
67 let face = ttf_parser::Face::parse(data, 0).ok()?;
68 let units_per_em = face.units_per_em();
69 let ascender = face.ascender();
70 let descender = face.descender();
71
72 let mut advance_widths = HashMap::new();
73 let mut glyph_ids = HashMap::new();
74 let mut default_advance = 0u16;
75
76 for code in 32u32..=0xFFFF {
78 if let Some(ch) = char::from_u32(code) {
79 if let Some(glyph_id) = face.glyph_index(ch) {
80 let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0);
81 advance_widths.insert(ch, advance);
82 glyph_ids.insert(ch, glyph_id.0);
83 if ch == ' ' {
84 default_advance = advance;
85 }
86 }
87 }
88 }
89
90 if default_advance == 0 {
91 default_advance = units_per_em / 2;
92 }
93
94 Some(CustomFontMetrics {
95 units_per_em,
96 advance_widths,
97 default_advance,
98 ascender,
99 descender,
100 glyph_ids,
101 })
102 }
103}
104
105#[derive(Debug, Clone, Copy)]
107pub enum StandardFont {
108 Helvetica,
109 HelveticaBold,
110 HelveticaOblique,
111 HelveticaBoldOblique,
112 TimesRoman,
113 TimesBold,
114 TimesItalic,
115 TimesBoldItalic,
116 Courier,
117 CourierBold,
118 CourierOblique,
119 CourierBoldOblique,
120 Symbol,
121 ZapfDingbats,
122}
123
124impl FontData {
125 pub fn has_char(&self, ch: char) -> bool {
127 match self {
128 FontData::Custom {
129 metrics: Some(m), ..
130 } => m.glyph_ids.contains_key(&ch),
131 FontData::Custom { metrics: None, .. } => false,
132 FontData::Standard(_) => {
133 unicode_to_winansi(ch).is_some() || (ch as u32) >= 32 && (ch as u32) <= 255
134 }
135 }
136 }
137}
138
139impl StandardFont {
140 pub fn liberation_family(&self) -> Option<&'static str> {
145 match self {
146 Self::Helvetica
147 | Self::HelveticaBold
148 | Self::HelveticaOblique
149 | Self::HelveticaBoldOblique => Some("Liberation Sans"),
150 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
151 Some("Liberation Serif")
152 }
153 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
154 Some("Liberation Mono")
155 }
156 Self::Symbol | Self::ZapfDingbats => None,
157 }
158 }
159
160 pub fn descriptor_flags(&self) -> u32 {
163 match self {
164 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
165 32 | 2
166 }
167 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
168 32 | 1
169 }
170 _ => 32,
171 }
172 }
173
174 pub fn pdf_name(&self) -> &'static str {
176 match self {
177 Self::Helvetica => "Helvetica",
178 Self::HelveticaBold => "Helvetica-Bold",
179 Self::HelveticaOblique => "Helvetica-Oblique",
180 Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
181 Self::TimesRoman => "Times-Roman",
182 Self::TimesBold => "Times-Bold",
183 Self::TimesItalic => "Times-Italic",
184 Self::TimesBoldItalic => "Times-BoldItalic",
185 Self::Courier => "Courier",
186 Self::CourierBold => "Courier-Bold",
187 Self::CourierOblique => "Courier-Oblique",
188 Self::CourierBoldOblique => "Courier-BoldOblique",
189 Self::Symbol => "Symbol",
190 Self::ZapfDingbats => "ZapfDingbats",
191 }
192 }
193}
194
195impl Default for FontRegistry {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl FontRegistry {
202 pub fn new() -> Self {
203 let mut fonts = HashMap::new();
204
205 let standard_mappings = vec![
206 (("Helvetica", 400, false), StandardFont::Helvetica),
207 (("Helvetica", 700, false), StandardFont::HelveticaBold),
208 (("Helvetica", 400, true), StandardFont::HelveticaOblique),
209 (("Helvetica", 700, true), StandardFont::HelveticaBoldOblique),
210 (("Times", 400, false), StandardFont::TimesRoman),
211 (("Times", 700, false), StandardFont::TimesBold),
212 (("Times", 400, true), StandardFont::TimesItalic),
213 (("Times", 700, true), StandardFont::TimesBoldItalic),
214 (("Courier", 400, false), StandardFont::Courier),
215 (("Courier", 700, false), StandardFont::CourierBold),
216 (("Courier", 400, true), StandardFont::CourierOblique),
217 (("Courier", 700, true), StandardFont::CourierBoldOblique),
218 ];
219
220 for ((family, weight, italic), font) in standard_mappings {
221 fonts.insert(
222 FontKey {
223 family: family.to_string(),
224 weight,
225 italic,
226 },
227 FontData::Standard(font),
228 );
229 }
230
231 let mut registry = Self { fonts };
232 builtin::register_builtin_fonts(&mut registry);
233 registry
234 }
235
236 pub fn resolve(&self, families: &str, weight: u32, italic: bool) -> &FontData {
242 let snapped_weight = if weight >= 600 { 700 } else { 400 };
243
244 for family in families.split(',') {
245 let family = family.trim().trim_matches('"').trim_matches('\'');
246 if family.is_empty() {
247 continue;
248 }
249
250 let key = FontKey {
252 family: family.to_string(),
253 weight,
254 italic,
255 };
256 if let Some(font) = self.fonts.get(&key) {
257 return font;
258 }
259
260 let key = FontKey {
262 family: family.to_string(),
263 weight: snapped_weight,
264 italic,
265 };
266 if let Some(font) = self.fonts.get(&key) {
267 return font;
268 }
269
270 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
272 let key = FontKey {
273 family: family.to_string(),
274 weight: opposite_weight,
275 italic,
276 };
277 if let Some(font) = self.fonts.get(&key) {
278 return font;
279 }
280 }
281
282 let key = FontKey {
284 family: "Helvetica".to_string(),
285 weight: snapped_weight,
286 italic,
287 };
288 self.fonts.get(&key).unwrap_or_else(|| {
289 self.fonts
290 .get(&FontKey {
291 family: "Helvetica".to_string(),
292 weight: 400,
293 italic: false,
294 })
295 .expect("Helvetica must be registered")
296 })
297 }
298
299 pub fn resolve_for_char(
305 &self,
306 families: &str,
307 ch: char,
308 weight: u32,
309 italic: bool,
310 ) -> (&FontData, String) {
311 let snapped_weight = if weight >= 600 { 700 } else { 400 };
312
313 for family in families.split(',') {
314 let family = family.trim().trim_matches('"').trim_matches('\'');
315 if family.is_empty() {
316 continue;
317 }
318
319 let key = FontKey {
321 family: family.to_string(),
322 weight,
323 italic,
324 };
325 if let Some(font) = self.fonts.get(&key) {
326 if font.has_char(ch) {
327 return (font, family.to_string());
328 }
329 }
330
331 let key = FontKey {
333 family: family.to_string(),
334 weight: snapped_weight,
335 italic,
336 };
337 if let Some(font) = self.fonts.get(&key) {
338 if font.has_char(ch) {
339 return (font, family.to_string());
340 }
341 }
342
343 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
345 let key = FontKey {
346 family: family.to_string(),
347 weight: opposite_weight,
348 italic,
349 };
350 if let Some(font) = self.fonts.get(&key) {
351 if font.has_char(ch) {
352 return (font, family.to_string());
353 }
354 }
355 }
356
357 let builtin_key = FontKey {
359 family: "Noto Sans".to_string(),
360 weight: snapped_weight,
361 italic: false,
362 };
363 if let Some(font) = self.fonts.get(&builtin_key) {
364 if font.has_char(ch) {
365 return (font, "Noto Sans".to_string());
366 }
367 }
368
369 let key = FontKey {
371 family: "Helvetica".to_string(),
372 weight: snapped_weight,
373 italic,
374 };
375 let font = self.fonts.get(&key).unwrap_or_else(|| {
376 self.fonts
377 .get(&FontKey {
378 family: "Helvetica".to_string(),
379 weight: 400,
380 italic: false,
381 })
382 .expect("Helvetica must be registered")
383 });
384 (font, "Helvetica".to_string())
385 }
386
387 pub fn register(&mut self, family: &str, weight: u32, italic: bool, data: Vec<u8>) {
389 let metrics = CustomFontMetrics::from_font_data(&data);
390 self.fonts.insert(
391 FontKey {
392 family: family.to_string(),
393 weight,
394 italic,
395 },
396 FontData::Custom {
397 data,
398 used_glyphs: Vec::new(),
399 metrics,
400 },
401 );
402 }
403
404 pub fn iter(&self) -> impl Iterator<Item = (&FontKey, &FontData)> {
406 self.fonts.iter()
407 }
408}
409
410pub struct FontContext {
413 registry: FontRegistry,
414 sentinel_digit_count: u32,
418}
419
420impl Default for FontContext {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426impl FontContext {
427 pub fn new() -> Self {
428 Self {
429 registry: FontRegistry::new(),
430 sentinel_digit_count: 2,
431 }
432 }
433
434 pub fn sentinel_digit_count(&self) -> u32 {
436 self.sentinel_digit_count
437 }
438
439 pub fn set_sentinel_digit_count(&mut self, count: u32) {
441 self.sentinel_digit_count = count;
442 }
443
444 pub fn char_width(
449 &self,
450 ch: char,
451 family: &str,
452 weight: u32,
453 italic: bool,
454 font_size: f64,
455 ) -> f64 {
456 if ch == crate::layout::PAGE_NUMBER_SENTINEL || ch == crate::layout::TOTAL_PAGES_SENTINEL {
459 return self.char_width('0', family, weight, italic, font_size)
460 * self.sentinel_digit_count as f64;
461 }
462
463 let font_data = if !family.contains(',') {
466 let primary = self.registry.resolve(family, weight, italic);
467 if ch.is_whitespace() || primary.has_char(ch) {
468 primary
469 } else {
470 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
471 data
472 }
473 } else {
474 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
475 data
476 };
477 match font_data {
478 FontData::Standard(std_font) => std_font.metrics().char_width(ch, font_size),
479 FontData::Custom {
480 metrics: Some(m), ..
481 } => m.char_width(ch, font_size),
482 FontData::Custom { metrics: None, .. } => {
483 StandardFont::Helvetica.metrics().char_width(ch, font_size)
484 }
485 }
486 }
487
488 pub fn measure_string(
490 &self,
491 text: &str,
492 family: &str,
493 weight: u32,
494 italic: bool,
495 font_size: f64,
496 letter_spacing: f64,
497 ) -> f64 {
498 let mut width = 0.0;
499 for ch in text.chars() {
500 width += self.char_width(ch, family, weight, italic, font_size) + letter_spacing;
501 }
502 width
503 }
504
505 pub fn resolve(&self, family: &str, weight: u32, italic: bool) -> &FontData {
507 self.registry.resolve(family, weight, italic)
508 }
509
510 pub fn registry(&self) -> &FontRegistry {
512 &self.registry
513 }
514
515 pub fn registry_mut(&mut self) -> &mut FontRegistry {
517 &mut self.registry
518 }
519
520 pub fn font_data(&self, family: &str, weight: u32, italic: bool) -> Option<&[u8]> {
523 let font_data = self.registry.resolve(family, weight, italic);
524 match font_data {
525 FontData::Custom { data, .. } => Some(data),
526 FontData::Standard(_) => None,
527 }
528 }
529
530 pub fn units_per_em(&self, family: &str, weight: u32, italic: bool) -> u16 {
532 let font_data = self.registry.resolve(family, weight, italic);
533 match font_data {
534 FontData::Custom {
535 metrics: Some(m), ..
536 } => m.units_per_em,
537 FontData::Custom { metrics: None, .. } => 1000,
538 FontData::Standard(_) => 1000,
539 }
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_font_context_helvetica() {
549 let ctx = FontContext::new();
550 let w = ctx.char_width(' ', "Helvetica", 400, false, 12.0);
551 assert!((w - 3.336).abs() < 0.001);
552 }
553
554 #[test]
555 fn test_font_context_bold_wider() {
556 let ctx = FontContext::new();
557 let regular = ctx.char_width('A', "Helvetica", 400, false, 12.0);
558 let bold = ctx.char_width('A', "Helvetica", 700, false, 12.0);
559 assert!(bold > regular, "Bold A should be wider than regular A");
560 }
561
562 #[test]
563 fn test_font_context_measure_string() {
564 let ctx = FontContext::new();
565 let w = ctx.measure_string("Hello", "Helvetica", 400, false, 12.0, 0.0);
566 assert!(w > 0.0);
567 }
568
569 #[test]
570 fn test_font_context_fallback() {
571 let ctx = FontContext::new();
572 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
573 let w2 = ctx.char_width('A', "UnknownFont", 400, false, 12.0);
574 assert!((w1 - w2).abs() < 0.001);
575 }
576
577 #[test]
578 fn test_font_context_weight_resolution() {
579 let ctx = FontContext::new();
580 let w700 = ctx.char_width('A', "Helvetica", 700, false, 12.0);
581 let w800 = ctx.char_width('A', "Helvetica", 800, false, 12.0);
582 assert!((w700 - w800).abs() < 0.001);
583 }
584
585 #[test]
586 fn test_font_fallback_chain_first_match() {
587 let ctx = FontContext::new();
588 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
589 let w2 = ctx.char_width('A', "Times, Helvetica", 400, false, 12.0);
590 assert!((w1 - w2).abs() < 0.001, "Should use Times (first in chain)");
591 }
592
593 #[test]
594 fn test_font_fallback_chain_second_match() {
595 let ctx = FontContext::new();
596 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
597 let w2 = ctx.char_width('A', "Missing, Helvetica", 400, false, 12.0);
598 assert!((w1 - w2).abs() < 0.001, "Should fall back to Helvetica");
599 }
600
601 #[test]
602 fn test_font_fallback_chain_all_missing() {
603 let ctx = FontContext::new();
604 let w = ctx.char_width('A', "Missing, AlsoMissing", 400, false, 12.0);
608 assert!(w > 0.0, "Should still produce a valid width from fallback");
609 }
610
611 #[test]
612 fn test_font_fallback_chain_quoted_families() {
613 let ctx = FontContext::new();
614 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
615 let w2 = ctx.char_width('A', "'Times', \"Helvetica\"", 400, false, 12.0);
616 assert!((w1 - w2).abs() < 0.001, "Should strip quotes and use Times");
617 }
618
619 #[test]
620 fn test_builtin_noto_sans_registered() {
621 let registry = FontRegistry::new();
622 let font = registry.resolve("Noto Sans", 400, false);
623 assert!(
624 matches!(font, FontData::Custom { .. }),
625 "Noto Sans should be registered as a custom font"
626 );
627 assert!(
628 font.has_char('\u{041F}'),
629 "Noto Sans should have Cyrillic П"
630 );
631 assert!(font.has_char('\u{03B1}'), "Noto Sans should have Greek α");
632 }
633
634 #[test]
635 fn test_builtin_noto_sans_fallback_for_cyrillic() {
636 let registry = FontRegistry::new();
637 let (font, family) = registry.resolve_for_char("Helvetica", '\u{041F}', 400, false);
638 assert_eq!(
639 family, "Noto Sans",
640 "Cyrillic should fall back to Noto Sans"
641 );
642 assert!(matches!(font, FontData::Custom { .. }));
643 }
644
645 #[test]
646 fn test_font_fallback_single_family_unchanged() {
647 let ctx = FontContext::new();
648 let w1 = ctx.char_width('A', "Courier", 400, false, 12.0);
649 let w2 = ctx.char_width('A', "Courier", 400, false, 12.0);
650 assert!(
651 (w1 - w2).abs() < 0.001,
652 "Single family should work as before"
653 );
654 }
655}