1use crate::types::{Size, TextAlign};
36
37#[derive(Debug, Clone)]
39pub struct FontMetrics {
40 pub size: f64,
42 pub line_height: f64,
44 pub avg_char_width: f64,
46 pub text_align: TextAlign,
48 pub typeface: FontFamily,
50 pub resolved_widths: Option<Vec<u16>>,
52 pub resolved_upem: Option<u16>,
54 pub resolved_ascender: Option<i16>,
56 pub resolved_descender: Option<i16>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum FontFamily {
63 Serif,
65 #[default]
67 SansSerif,
68 Monospace,
70}
71
72impl FontFamily {
73 pub fn from_generic_family(gf: &str) -> Self {
75 match gf {
76 "serif" | "decorative" | "cursive" => FontFamily::Serif,
77 "monospaced" => FontFamily::Monospace,
78 _ => FontFamily::SansSerif,
80 }
81 }
82
83 pub fn from_typeface(name: &str) -> Self {
85 let lower = name.to_ascii_lowercase();
86 if lower.contains("courier") || lower.contains("consolas") || lower.contains("mono") {
87 FontFamily::Monospace
88 } else if lower.contains("times")
89 || lower.contains("georgia")
90 || lower.contains("garamond")
91 || lower.contains("palatino")
92 || lower.contains("cambria")
93 || lower.contains("book antiqua")
94 || lower.contains("century")
95 || lower.contains("serif")
96 {
97 FontFamily::Serif
98 } else {
99 FontFamily::SansSerif
101 }
102 }
103}
104
105impl Default for FontMetrics {
106 fn default() -> Self {
107 Self {
108 size: 10.0,
110 line_height: 1.2,
112 avg_char_width: 0.50,
114 text_align: TextAlign::Left,
115 typeface: FontFamily::SansSerif,
116 resolved_widths: None,
117 resolved_upem: None,
118 resolved_ascender: None,
119 resolved_descender: None,
120 }
121 }
122}
123
124impl FontMetrics {
125 pub fn new(size: f64) -> Self {
127 Self {
128 size,
129 ..Default::default()
130 }
131 }
132
133 pub fn line_height_pt(&self) -> f64 {
140 if let (Some(asc), Some(desc), Some(upem)) = (
141 self.resolved_ascender,
142 self.resolved_descender,
143 self.resolved_upem,
144 ) {
145 if upem > 0 {
146 return ((asc as f64) - (desc as f64)) / (upem as f64) * self.size;
147 }
148 }
149 self.size * self.line_height
150 }
151
152 pub fn measure_width(&self, text: &str) -> f64 {
171 if let (Some(ref widths), Some(_upem)) = (&self.resolved_widths, self.resolved_upem) {
172 let space_w = widths.get(b' ' as usize).copied().unwrap_or(0) as f64;
175 let mut w = 0.0;
176 for ch in text.chars() {
177 let code = ch as u32;
178 let cw = if (code as usize) < widths.len() {
179 widths[code as usize] as f64
180 } else {
181 space_w
182 };
183 w += cw / 1000.0 * self.size;
184 }
185 return w;
186 }
187 let table = match self.typeface {
188 FontFamily::Serif => &TIMES_WIDTHS,
189 FontFamily::SansSerif => &HELVETICA_WIDTHS,
190 FontFamily::Monospace => &COURIER_WIDTHS,
191 };
192 let default_w = table[b'n' as usize] as f64;
195 let mut width = 0.0;
196 for ch in text.chars() {
197 let code = ch as u32;
198 let char_width = if code < 128 {
199 table[code as usize] as f64
200 } else {
201 default_w
202 };
203 width += char_width / 1000.0 * self.size;
204 }
205 width
206 }
207}
208
209#[rustfmt::skip]
216static TIMES_WIDTHS: [u16; 128] = [
217 250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,
219 250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,
221 250,333,408,500,500,833,778,180,333,333,500,564,250,333,250,278,
223 500,500,500,500,500,500,500,500,500,500,278,278,564,564,564,444,
225 921,722,667,667,722,611,556,722,722,333,389,722,611,889,722,722,
227 556,722,667,556,611,722,722,944,722,722,611,333,278,333,469,500,
229 333,444,500,444,500,444,333,500,500,278,278,500,278,778,500,500,
231 500,500,333,389,278,500,500,722,500,500,444,480,200,480,541,250,
233];
234
235#[rustfmt::skip]
238static HELVETICA_WIDTHS: [u16; 128] = [
239 278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,
241 278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,
243 278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,
245 556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,
247 1015,667,667,722,722,611,556,778,722,278,500,667,556,833,722,778,
249 667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,
251 333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,
253 556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,278,
255];
256
257#[rustfmt::skip]
260static COURIER_WIDTHS: [u16; 128] = [
261 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
262 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
263 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
264 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
265 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
266 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
267 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
268 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
269];
270
271#[derive(Debug, Clone)]
273pub struct TextLayout {
274 pub lines: Vec<String>,
276 pub first_line_of_para: Vec<bool>,
278 pub size: Size,
280}
281
282pub fn wrap_text(
309 text: &str,
310 max_width: f64,
311 font: &FontMetrics,
312 text_indent: f64,
313 line_height_override: Option<f64>,
314) -> TextLayout {
315 if text.is_empty() {
316 return TextLayout {
317 lines: vec![],
318 first_line_of_para: vec![],
319 size: Size {
320 width: 0.0,
321 height: 0.0,
322 },
323 };
324 }
325
326 let mut lines = Vec::new();
327 let mut first_line_of_para = Vec::new();
328 let mut max_line_width = 0.0_f64;
329
330 for paragraph in text.split('\n') {
331 if paragraph.is_empty() {
332 lines.push(String::new());
333 first_line_of_para.push(true);
334 continue;
335 }
336
337 let words: Vec<&str> = paragraph.split_whitespace().collect();
338 if words.is_empty() {
339 lines.push(String::new());
340 first_line_of_para.push(true);
341 continue;
342 }
343
344 let mut is_first_line = true;
345 let mut current_line = String::new();
346 let mut current_width = 0.0;
347
348 for word in words {
349 let word_width = font.measure_width(word);
350 let space_width = if current_line.is_empty() {
351 0.0
352 } else {
353 font.measure_width(" ")
354 };
355
356 let effective_max = if is_first_line {
357 (max_width - text_indent).max(0.0)
358 } else {
359 max_width
360 };
361
362 if current_width + space_width + word_width > effective_max && !current_line.is_empty()
363 {
364 max_line_width = max_line_width.max(current_width);
368 lines.push(current_line);
369 first_line_of_para.push(is_first_line);
370 is_first_line = false;
371 current_line = word.to_string();
372 current_width = word_width;
373 } else {
374 if !current_line.is_empty() {
375 current_line.push(' ');
376 current_width += space_width;
377 }
378 current_line.push_str(word);
379 current_width += word_width;
380 }
381 }
382
383 if !current_line.is_empty() {
384 max_line_width = max_line_width.max(current_width);
385 lines.push(current_line);
386 first_line_of_para.push(is_first_line);
387 }
388 }
389
390 let lh = line_height_override.unwrap_or_else(|| font.line_height_pt());
391 let height = lines.len() as f64 * lh;
392
393 TextLayout {
394 lines,
395 first_line_of_para,
396 size: Size {
397 width: max_line_width,
398 height,
399 },
400 }
401}
402
403pub fn measure_text(text: &str, font: &FontMetrics) -> Size {
408 if text.is_empty() {
409 return Size {
410 width: 0.0,
411 height: 0.0,
412 };
413 }
414
415 let lines: Vec<&str> = text.split('\n').collect();
416 let max_width = lines
417 .iter()
418 .map(|l| font.measure_width(l))
419 .fold(0.0, f64::max);
420 let height = lines.len() as f64 * font.line_height_pt();
421
422 Size {
423 width: max_width,
424 height,
425 }
426}
427
428pub fn text_split_points(line_count: usize, line_height: f64) -> Vec<f64> {
434 if line_count <= 1 {
435 return Vec::new();
436 }
437 (1..line_count).map(|i| i as f64 * line_height).collect()
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn font_metrics_defaults() {
446 let f = FontMetrics::default();
447 assert_eq!(f.size, 10.0);
448 assert_eq!(f.line_height_pt(), 12.0); }
450
451 #[test]
452 fn measure_width_times() {
453 let f = FontMetrics {
454 size: 10.0,
455 typeface: FontFamily::Serif,
456 ..Default::default()
457 };
458 let w = f.measure_width("Hello");
460 assert!((w - 22.22).abs() < 0.01, "Times Hello={w}, expected ~22.22");
461 }
462
463 #[test]
464 fn measure_width_helvetica() {
465 let f = FontMetrics {
466 size: 10.0,
467 typeface: FontFamily::SansSerif,
468 ..Default::default()
469 };
470 let w = f.measure_width("Hello");
472 assert!((w - 22.78).abs() < 0.01, "Helv Hello={w}, expected ~22.78");
473 }
474
475 #[test]
476 fn measure_width_courier() {
477 let f = FontMetrics {
478 size: 10.0,
479 typeface: FontFamily::Monospace,
480 ..Default::default()
481 };
482 let w = f.measure_width("Hello");
484 assert!((w - 30.0).abs() < 0.01, "Courier Hello={w}, expected 30.0");
485 }
486
487 #[test]
488 fn times_narrower_than_old_avg() {
489 let f = FontMetrics {
492 size: 10.0,
493 typeface: FontFamily::Serif,
494 ..Default::default()
495 };
496 let w = f.measure_width("Hello");
497 assert!(w < 25.0, "Times should be narrower than old 0.5 avg");
498 }
499
500 #[test]
501 fn font_family_classification() {
502 assert_eq!(
503 FontFamily::from_typeface("Times New Roman"),
504 FontFamily::Serif
505 );
506 assert_eq!(FontFamily::from_typeface("Arial"), FontFamily::SansSerif);
507 assert_eq!(
508 FontFamily::from_typeface("Courier New"),
509 FontFamily::Monospace
510 );
511 assert_eq!(
512 FontFamily::from_typeface("Myriad Pro"),
513 FontFamily::SansSerif
514 );
515 assert_eq!(FontFamily::from_typeface("Verdana"), FontFamily::SansSerif);
516 assert_eq!(FontFamily::from_typeface("Georgia"), FontFamily::Serif);
517 }
518
519 #[test]
520 fn measure_text_single_line() {
521 let f = FontMetrics::default();
522 let s = measure_text("Hello", &f);
523 assert!(s.width > 0.0);
524 assert_eq!(s.height, 12.0);
525 }
526
527 #[test]
528 fn measure_text_multiline() {
529 let f = FontMetrics::default();
530 let s = measure_text("Line 1\nLine 2\nLine 3", &f);
531 assert_eq!(s.height, 36.0); }
533
534 #[test]
535 fn wrap_text_no_wrap_needed() {
536 let f = FontMetrics::default();
537 let result = wrap_text("Short", 200.0, &f, 0.0, None);
538 assert_eq!(result.lines.len(), 1);
539 assert_eq!(result.lines[0], "Short");
540 }
541
542 #[test]
543 fn wrap_text_preserves_newlines() {
544 let f = FontMetrics::default();
545 let result = wrap_text("Line 1\nLine 2", 200.0, &f, 0.0, None);
546 assert_eq!(result.lines.len(), 2);
547 assert_eq!(result.lines[0], "Line 1");
548 assert_eq!(result.lines[1], "Line 2");
549 }
550
551 #[test]
552 fn wrap_text_empty_string() {
553 let f = FontMetrics::default();
554 let result = wrap_text("", 100.0, &f, 0.0, None);
555 assert_eq!(result.lines.len(), 0);
556 assert_eq!(result.size.height, 0.0);
557 }
558
559 #[test]
560 fn resolved_widths_measure() {
561 let mut widths = vec![0u16; 256];
562 widths[b'H' as usize] = 700;
563 widths[b'i' as usize] = 300;
564 let f = FontMetrics {
565 size: 10.0,
566 resolved_widths: Some(widths),
567 resolved_upem: Some(1000),
568 ..Default::default()
569 };
570 let w = f.measure_width("Hi");
571 assert!((w - 10.0).abs() < 0.01, "resolved Hi={w}, expected 10.0");
572 }
573
574 #[test]
575 fn resolved_line_height() {
576 let f = FontMetrics {
577 size: 12.0,
578 resolved_ascender: Some(800),
579 resolved_descender: Some(-200),
580 resolved_upem: Some(1000),
581 ..Default::default()
582 };
583 let lh = f.line_height_pt();
584 assert!((lh - 12.0).abs() < 0.01, "resolved lh={lh}, expected 12.0");
585 }
586
587 #[test]
588 fn wrap_text_times_given_name() {
589 let f = FontMetrics {
591 size: 9.0,
592 typeface: FontFamily::Serif,
593 ..Default::default()
594 };
595 let result = wrap_text("Given Name (First Name)", 140.0, &f, 0.0, None);
596 assert_eq!(
597 result.lines.len(),
598 1,
599 "Should fit on 1 line but got: {:?}",
600 result.lines
601 );
602 }
603
604 #[test]
605 fn resolved_widths_upem_2048() {
606 let mut widths = vec![0u16; 256];
609 widths[b'A' as usize] = 600; let f = FontMetrics {
611 size: 10.0,
612 resolved_widths: Some(widths),
613 resolved_upem: Some(2048),
614 ..Default::default()
615 };
616 let w = f.measure_width("A");
617 assert!(
620 (w - 6.0).abs() < 0.01,
621 "upem=2048: A width={w}, expected 6.0"
622 );
623 }
624
625 #[test]
626 fn multibyte_utf8_single_char_width() {
627 let mut widths = vec![0u16; 256];
629 widths[0xE9] = 500; let f = FontMetrics {
631 size: 10.0,
632 resolved_widths: Some(widths),
633 resolved_upem: Some(1000),
634 ..Default::default()
635 };
636 let w = f.measure_width("\u{00E9}");
637 assert!(
640 (w - 5.0).abs() < 0.01,
641 "é width={w}, expected 5.0 (one glyph, not two bytes)"
642 );
643 }
644
645 #[test]
646 fn afm_fallback_multibyte_char() {
647 let f = FontMetrics {
649 size: 10.0,
650 typeface: FontFamily::SansSerif,
651 ..Default::default()
652 };
653 let w_n = f.measure_width("n");
654 let w_e_accent = f.measure_width("\u{00E9}");
655 assert!(
657 (w_e_accent - w_n).abs() < 0.01,
658 "AFM fallback: é={w_e_accent} should equal n={w_n}"
659 );
660 }
661
662 #[test]
663 fn text_split_points_multi() {
664 let pts = text_split_points(4, 12.0);
665 assert_eq!(pts.len(), 3);
666 assert!((pts[0] - 12.0).abs() < 0.01);
667 assert!((pts[1] - 24.0).abs() < 0.01);
668 assert!((pts[2] - 36.0).abs() < 0.01);
669 }
670
671 #[test]
672 fn text_split_points_single_line() {
673 let pts = text_split_points(1, 12.0);
674 assert!(pts.is_empty());
675 }
676}