1pub mod builtin;
9pub mod fallback;
10pub mod metrics;
11pub mod subset;
12
13pub use metrics::{unicode_to_winansi, 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 pdf_name(&self) -> &'static str {
142 match self {
143 Self::Helvetica => "Helvetica",
144 Self::HelveticaBold => "Helvetica-Bold",
145 Self::HelveticaOblique => "Helvetica-Oblique",
146 Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
147 Self::TimesRoman => "Times-Roman",
148 Self::TimesBold => "Times-Bold",
149 Self::TimesItalic => "Times-Italic",
150 Self::TimesBoldItalic => "Times-BoldItalic",
151 Self::Courier => "Courier",
152 Self::CourierBold => "Courier-Bold",
153 Self::CourierOblique => "Courier-Oblique",
154 Self::CourierBoldOblique => "Courier-BoldOblique",
155 Self::Symbol => "Symbol",
156 Self::ZapfDingbats => "ZapfDingbats",
157 }
158 }
159}
160
161impl Default for FontRegistry {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl FontRegistry {
168 pub fn new() -> Self {
169 let mut fonts = HashMap::new();
170
171 let standard_mappings = vec![
172 (("Helvetica", 400, false), StandardFont::Helvetica),
173 (("Helvetica", 700, false), StandardFont::HelveticaBold),
174 (("Helvetica", 400, true), StandardFont::HelveticaOblique),
175 (("Helvetica", 700, true), StandardFont::HelveticaBoldOblique),
176 (("Times", 400, false), StandardFont::TimesRoman),
177 (("Times", 700, false), StandardFont::TimesBold),
178 (("Times", 400, true), StandardFont::TimesItalic),
179 (("Times", 700, true), StandardFont::TimesBoldItalic),
180 (("Courier", 400, false), StandardFont::Courier),
181 (("Courier", 700, false), StandardFont::CourierBold),
182 (("Courier", 400, true), StandardFont::CourierOblique),
183 (("Courier", 700, true), StandardFont::CourierBoldOblique),
184 ];
185
186 for ((family, weight, italic), font) in standard_mappings {
187 fonts.insert(
188 FontKey {
189 family: family.to_string(),
190 weight,
191 italic,
192 },
193 FontData::Standard(font),
194 );
195 }
196
197 let mut registry = Self { fonts };
198 builtin::register_builtin_fonts(&mut registry);
199 registry
200 }
201
202 pub fn resolve(&self, families: &str, weight: u32, italic: bool) -> &FontData {
208 let snapped_weight = if weight >= 600 { 700 } else { 400 };
209
210 for family in families.split(',') {
211 let family = family.trim().trim_matches('"').trim_matches('\'');
212 if family.is_empty() {
213 continue;
214 }
215
216 let key = FontKey {
218 family: family.to_string(),
219 weight,
220 italic,
221 };
222 if let Some(font) = self.fonts.get(&key) {
223 return font;
224 }
225
226 let key = FontKey {
228 family: family.to_string(),
229 weight: snapped_weight,
230 italic,
231 };
232 if let Some(font) = self.fonts.get(&key) {
233 return font;
234 }
235
236 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
238 let key = FontKey {
239 family: family.to_string(),
240 weight: opposite_weight,
241 italic,
242 };
243 if let Some(font) = self.fonts.get(&key) {
244 return font;
245 }
246 }
247
248 let key = FontKey {
250 family: "Helvetica".to_string(),
251 weight: snapped_weight,
252 italic,
253 };
254 self.fonts.get(&key).unwrap_or_else(|| {
255 self.fonts
256 .get(&FontKey {
257 family: "Helvetica".to_string(),
258 weight: 400,
259 italic: false,
260 })
261 .expect("Helvetica must be registered")
262 })
263 }
264
265 pub fn resolve_for_char(
271 &self,
272 families: &str,
273 ch: char,
274 weight: u32,
275 italic: bool,
276 ) -> (&FontData, String) {
277 let snapped_weight = if weight >= 600 { 700 } else { 400 };
278
279 for family in families.split(',') {
280 let family = family.trim().trim_matches('"').trim_matches('\'');
281 if family.is_empty() {
282 continue;
283 }
284
285 let key = FontKey {
287 family: family.to_string(),
288 weight,
289 italic,
290 };
291 if let Some(font) = self.fonts.get(&key) {
292 if font.has_char(ch) {
293 return (font, family.to_string());
294 }
295 }
296
297 let key = FontKey {
299 family: family.to_string(),
300 weight: snapped_weight,
301 italic,
302 };
303 if let Some(font) = self.fonts.get(&key) {
304 if font.has_char(ch) {
305 return (font, family.to_string());
306 }
307 }
308
309 let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
311 let key = FontKey {
312 family: family.to_string(),
313 weight: opposite_weight,
314 italic,
315 };
316 if let Some(font) = self.fonts.get(&key) {
317 if font.has_char(ch) {
318 return (font, family.to_string());
319 }
320 }
321 }
322
323 let builtin_key = FontKey {
325 family: "Noto Sans".to_string(),
326 weight: snapped_weight,
327 italic: false,
328 };
329 if let Some(font) = self.fonts.get(&builtin_key) {
330 if font.has_char(ch) {
331 return (font, "Noto Sans".to_string());
332 }
333 }
334
335 let key = FontKey {
337 family: "Helvetica".to_string(),
338 weight: snapped_weight,
339 italic,
340 };
341 let font = self.fonts.get(&key).unwrap_or_else(|| {
342 self.fonts
343 .get(&FontKey {
344 family: "Helvetica".to_string(),
345 weight: 400,
346 italic: false,
347 })
348 .expect("Helvetica must be registered")
349 });
350 (font, "Helvetica".to_string())
351 }
352
353 pub fn register(&mut self, family: &str, weight: u32, italic: bool, data: Vec<u8>) {
355 let metrics = CustomFontMetrics::from_font_data(&data);
356 self.fonts.insert(
357 FontKey {
358 family: family.to_string(),
359 weight,
360 italic,
361 },
362 FontData::Custom {
363 data,
364 used_glyphs: Vec::new(),
365 metrics,
366 },
367 );
368 }
369
370 pub fn iter(&self) -> impl Iterator<Item = (&FontKey, &FontData)> {
372 self.fonts.iter()
373 }
374}
375
376pub struct FontContext {
379 registry: FontRegistry,
380}
381
382impl Default for FontContext {
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388impl FontContext {
389 pub fn new() -> Self {
390 Self {
391 registry: FontRegistry::new(),
392 }
393 }
394
395 pub fn char_width(
400 &self,
401 ch: char,
402 family: &str,
403 weight: u32,
404 italic: bool,
405 font_size: f64,
406 ) -> f64 {
407 let font_data = if !family.contains(',') {
410 let primary = self.registry.resolve(family, weight, italic);
411 if ch.is_whitespace() || primary.has_char(ch) {
412 primary
413 } else {
414 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
415 data
416 }
417 } else {
418 let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
419 data
420 };
421 match font_data {
422 FontData::Standard(std_font) => std_font.metrics().char_width(ch, font_size),
423 FontData::Custom {
424 metrics: Some(m), ..
425 } => m.char_width(ch, font_size),
426 FontData::Custom { metrics: None, .. } => {
427 StandardFont::Helvetica.metrics().char_width(ch, font_size)
428 }
429 }
430 }
431
432 pub fn measure_string(
434 &self,
435 text: &str,
436 family: &str,
437 weight: u32,
438 italic: bool,
439 font_size: f64,
440 letter_spacing: f64,
441 ) -> f64 {
442 let font_data = self.registry.resolve(family, weight, italic);
443 match font_data {
444 FontData::Standard(std_font) => {
445 std_font
446 .metrics()
447 .measure_string(text, font_size, letter_spacing)
448 }
449 FontData::Custom {
450 metrics: Some(m), ..
451 } => {
452 let mut width = 0.0;
453 for ch in text.chars() {
454 width += m.char_width(ch, font_size) + letter_spacing;
455 }
456 width
457 }
458 FontData::Custom { metrics: None, .. } => StandardFont::Helvetica
459 .metrics()
460 .measure_string(text, font_size, letter_spacing),
461 }
462 }
463
464 pub fn resolve(&self, family: &str, weight: u32, italic: bool) -> &FontData {
466 self.registry.resolve(family, weight, italic)
467 }
468
469 pub fn registry(&self) -> &FontRegistry {
471 &self.registry
472 }
473
474 pub fn registry_mut(&mut self) -> &mut FontRegistry {
476 &mut self.registry
477 }
478
479 pub fn font_data(&self, family: &str, weight: u32, italic: bool) -> Option<&[u8]> {
482 let font_data = self.registry.resolve(family, weight, italic);
483 match font_data {
484 FontData::Custom { data, .. } => Some(data),
485 FontData::Standard(_) => None,
486 }
487 }
488
489 pub fn units_per_em(&self, family: &str, weight: u32, italic: bool) -> u16 {
491 let font_data = self.registry.resolve(family, weight, italic);
492 match font_data {
493 FontData::Custom {
494 metrics: Some(m), ..
495 } => m.units_per_em,
496 FontData::Custom { metrics: None, .. } => 1000,
497 FontData::Standard(_) => 1000,
498 }
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_font_context_helvetica() {
508 let ctx = FontContext::new();
509 let w = ctx.char_width(' ', "Helvetica", 400, false, 12.0);
510 assert!((w - 3.336).abs() < 0.001);
511 }
512
513 #[test]
514 fn test_font_context_bold_wider() {
515 let ctx = FontContext::new();
516 let regular = ctx.char_width('A', "Helvetica", 400, false, 12.0);
517 let bold = ctx.char_width('A', "Helvetica", 700, false, 12.0);
518 assert!(bold > regular, "Bold A should be wider than regular A");
519 }
520
521 #[test]
522 fn test_font_context_measure_string() {
523 let ctx = FontContext::new();
524 let w = ctx.measure_string("Hello", "Helvetica", 400, false, 12.0, 0.0);
525 assert!(w > 0.0);
526 }
527
528 #[test]
529 fn test_font_context_fallback() {
530 let ctx = FontContext::new();
531 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
532 let w2 = ctx.char_width('A', "UnknownFont", 400, false, 12.0);
533 assert!((w1 - w2).abs() < 0.001);
534 }
535
536 #[test]
537 fn test_font_context_weight_resolution() {
538 let ctx = FontContext::new();
539 let w700 = ctx.char_width('A', "Helvetica", 700, false, 12.0);
540 let w800 = ctx.char_width('A', "Helvetica", 800, false, 12.0);
541 assert!((w700 - w800).abs() < 0.001);
542 }
543
544 #[test]
545 fn test_font_fallback_chain_first_match() {
546 let ctx = FontContext::new();
547 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
548 let w2 = ctx.char_width('A', "Times, Helvetica", 400, false, 12.0);
549 assert!((w1 - w2).abs() < 0.001, "Should use Times (first in chain)");
550 }
551
552 #[test]
553 fn test_font_fallback_chain_second_match() {
554 let ctx = FontContext::new();
555 let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
556 let w2 = ctx.char_width('A', "Missing, Helvetica", 400, false, 12.0);
557 assert!((w1 - w2).abs() < 0.001, "Should fall back to Helvetica");
558 }
559
560 #[test]
561 fn test_font_fallback_chain_all_missing() {
562 let ctx = FontContext::new();
563 let w = ctx.char_width('A', "Missing, AlsoMissing", 400, false, 12.0);
567 assert!(w > 0.0, "Should still produce a valid width from fallback");
568 }
569
570 #[test]
571 fn test_font_fallback_chain_quoted_families() {
572 let ctx = FontContext::new();
573 let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
574 let w2 = ctx.char_width('A', "'Times', \"Helvetica\"", 400, false, 12.0);
575 assert!((w1 - w2).abs() < 0.001, "Should strip quotes and use Times");
576 }
577
578 #[test]
579 fn test_builtin_noto_sans_registered() {
580 let registry = FontRegistry::new();
581 let font = registry.resolve("Noto Sans", 400, false);
582 assert!(
583 matches!(font, FontData::Custom { .. }),
584 "Noto Sans should be registered as a custom font"
585 );
586 assert!(
587 font.has_char('\u{041F}'),
588 "Noto Sans should have Cyrillic П"
589 );
590 assert!(font.has_char('\u{03B1}'), "Noto Sans should have Greek α");
591 }
592
593 #[test]
594 fn test_builtin_noto_sans_fallback_for_cyrillic() {
595 let registry = FontRegistry::new();
596 let (font, family) = registry.resolve_for_char("Helvetica", '\u{041F}', 400, false);
597 assert_eq!(
598 family, "Noto Sans",
599 "Cyrillic should fall back to Noto Sans"
600 );
601 assert!(matches!(font, FontData::Custom { .. }));
602 }
603
604 #[test]
605 fn test_font_fallback_single_family_unchanged() {
606 let ctx = FontContext::new();
607 let w1 = ctx.char_width('A', "Courier", 400, false, 12.0);
608 let w2 = ctx.char_width('A', "Courier", 400, false, 12.0);
609 assert!(
610 (w1 - w2).abs() < 0.001,
611 "Single family should work as before"
612 );
613 }
614}