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