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 {
120 Self {
121 size,
122 ..Default::default()
123 }
124 }
125
126 pub fn line_height_pt(&self) -> f64 {
133 if let (Some(asc), Some(desc), Some(upem)) = (
134 self.resolved_ascender,
135 self.resolved_descender,
136 self.resolved_upem,
137 ) {
138 if upem > 0 {
139 return ((asc as f64) - (desc as f64)) / (upem as f64) * self.size;
140 }
141 }
142 self.size * self.line_height
143 }
144
145 pub fn measure_width(&self, text: &str) -> f64 {
164 if let (Some(ref widths), Some(_upem)) = (&self.resolved_widths, self.resolved_upem) {
165 let space_w = widths.get(b' ' as usize).copied().unwrap_or(0) as f64;
168 let mut w = 0.0;
169 for ch in text.chars() {
170 let code = ch as u32;
171 let cw = if (code as usize) < widths.len() {
172 widths[code as usize] as f64
173 } else {
174 space_w
175 };
176 w += cw / 1000.0 * self.size;
177 }
178 return w;
179 }
180 let table = match self.typeface {
181 FontFamily::Serif => &TIMES_WIDTHS,
182 FontFamily::SansSerif => &HELVETICA_WIDTHS,
183 FontFamily::Monospace => &COURIER_WIDTHS,
184 };
185 let default_w = table[b'n' as usize] as f64;
188 let mut width = 0.0;
189 for ch in text.chars() {
190 let code = ch as u32;
191 let char_width = if code < 128 {
192 table[code as usize] as f64
193 } else {
194 default_w
195 };
196 width += char_width / 1000.0 * self.size;
197 }
198 width
199 }
200}
201
202#[rustfmt::skip]
209static TIMES_WIDTHS: [u16; 128] = [
210 250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,
212 250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,
214 250,333,408,500,500,833,778,180,333,333,500,564,250,333,250,278,
216 500,500,500,500,500,500,500,500,500,500,278,278,564,564,564,444,
218 921,722,667,667,722,611,556,722,722,333,389,722,611,889,722,722,
220 556,722,667,556,611,722,722,944,722,722,611,333,278,333,469,500,
222 333,444,500,444,500,444,333,500,500,278,278,500,278,778,500,500,
224 500,500,333,389,278,500,500,722,500,500,444,480,200,480,541,250,
226];
227
228#[rustfmt::skip]
231static HELVETICA_WIDTHS: [u16; 128] = [
232 278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,
234 278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,
236 278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,
238 556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,
240 1015,667,667,722,722,611,556,778,722,278,500,667,556,833,722,778,
242 667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,
244 333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,
246 556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,278,
248];
249
250#[rustfmt::skip]
253static COURIER_WIDTHS: [u16; 128] = [
254 600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,
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];
263
264#[derive(Debug, Clone)]
266pub struct TextLayout {
267 pub lines: Vec<String>,
269 pub first_line_of_para: Vec<bool>,
271 pub size: Size,
273}
274
275pub fn wrap_text(
302 text: &str,
303 max_width: f64,
304 font: &FontMetrics,
305 text_indent: f64,
306 line_height_override: Option<f64>,
307) -> TextLayout {
308 if text.is_empty() {
309 return TextLayout {
310 lines: vec![],
311 first_line_of_para: vec![],
312 size: Size {
313 width: 0.0,
314 height: 0.0,
315 },
316 };
317 }
318
319 let mut lines = Vec::new();
320 let mut first_line_of_para = Vec::new();
321 let mut max_line_width = 0.0_f64;
322
323 for paragraph in text.split('\n') {
324 if paragraph.is_empty() {
325 lines.push(String::new());
326 first_line_of_para.push(true);
327 continue;
328 }
329
330 let words: Vec<&str> = paragraph.split_whitespace().collect();
331 if words.is_empty() {
332 lines.push(String::new());
333 first_line_of_para.push(true);
334 continue;
335 }
336
337 let mut is_first_line = true;
338 let mut current_line = String::new();
339 let mut current_width = 0.0;
340
341 for word in words {
342 let word_width = font.measure_width(word);
343 let space_width = if current_line.is_empty() {
344 0.0
345 } else {
346 font.measure_width(" ")
347 };
348
349 let effective_max = if is_first_line {
350 (max_width - text_indent).max(0.0)
351 } else {
352 max_width
353 };
354
355 if current_width + space_width + word_width > effective_max && !current_line.is_empty()
356 {
357 max_line_width = max_line_width.max(current_width);
361 lines.push(current_line);
362 first_line_of_para.push(is_first_line);
363 is_first_line = false;
364 current_line = word.to_string();
365 current_width = word_width;
366 } else {
367 if !current_line.is_empty() {
368 current_line.push(' ');
369 current_width += space_width;
370 }
371 current_line.push_str(word);
372 current_width += word_width;
373 }
374 }
375
376 if !current_line.is_empty() {
377 max_line_width = max_line_width.max(current_width);
378 lines.push(current_line);
379 first_line_of_para.push(is_first_line);
380 }
381 }
382
383 let lh = line_height_override.unwrap_or_else(|| font.line_height_pt());
384 let height = lines.len() as f64 * lh;
385
386 TextLayout {
387 lines,
388 first_line_of_para,
389 size: Size {
390 width: max_line_width,
391 height,
392 },
393 }
394}
395
396pub fn measure_text(text: &str, font: &FontMetrics) -> Size {
401 if text.is_empty() {
402 return Size {
403 width: 0.0,
404 height: 0.0,
405 };
406 }
407
408 let lines: Vec<&str> = text.split('\n').collect();
409 let max_width = lines
410 .iter()
411 .map(|l| font.measure_width(l))
412 .fold(0.0, f64::max);
413 let height = lines.len() as f64 * font.line_height_pt();
414
415 Size {
416 width: max_width,
417 height,
418 }
419}
420
421pub fn text_split_points(line_count: usize, line_height: f64) -> Vec<f64> {
427 if line_count <= 1 {
428 return Vec::new();
429 }
430 (1..line_count).map(|i| i as f64 * line_height).collect()
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn font_metrics_defaults() {
439 let f = FontMetrics::default();
440 assert_eq!(f.size, 10.0);
441 assert_eq!(f.line_height_pt(), 12.0); }
443
444 #[test]
445 fn measure_width_times() {
446 let f = FontMetrics {
447 size: 10.0,
448 typeface: FontFamily::Serif,
449 ..Default::default()
450 };
451 let w = f.measure_width("Hello");
453 assert!((w - 22.22).abs() < 0.01, "Times Hello={w}, expected ~22.22");
454 }
455
456 #[test]
457 fn measure_width_helvetica() {
458 let f = FontMetrics {
459 size: 10.0,
460 typeface: FontFamily::SansSerif,
461 ..Default::default()
462 };
463 let w = f.measure_width("Hello");
465 assert!((w - 22.78).abs() < 0.01, "Helv Hello={w}, expected ~22.78");
466 }
467
468 #[test]
469 fn measure_width_courier() {
470 let f = FontMetrics {
471 size: 10.0,
472 typeface: FontFamily::Monospace,
473 ..Default::default()
474 };
475 let w = f.measure_width("Hello");
477 assert!((w - 30.0).abs() < 0.01, "Courier Hello={w}, expected 30.0");
478 }
479
480 #[test]
481 fn times_narrower_than_old_avg() {
482 let f = FontMetrics {
485 size: 10.0,
486 typeface: FontFamily::Serif,
487 ..Default::default()
488 };
489 let w = f.measure_width("Hello");
490 assert!(w < 25.0, "Times should be narrower than old 0.5 avg");
491 }
492
493 #[test]
494 fn font_family_classification() {
495 assert_eq!(
496 FontFamily::from_typeface("Times New Roman"),
497 FontFamily::Serif
498 );
499 assert_eq!(FontFamily::from_typeface("Arial"), FontFamily::SansSerif);
500 assert_eq!(
501 FontFamily::from_typeface("Courier New"),
502 FontFamily::Monospace
503 );
504 assert_eq!(
505 FontFamily::from_typeface("Myriad Pro"),
506 FontFamily::SansSerif
507 );
508 assert_eq!(FontFamily::from_typeface("Verdana"), FontFamily::SansSerif);
509 assert_eq!(FontFamily::from_typeface("Georgia"), FontFamily::Serif);
510 }
511
512 #[test]
513 fn measure_text_single_line() {
514 let f = FontMetrics::default();
515 let s = measure_text("Hello", &f);
516 assert!(s.width > 0.0);
517 assert_eq!(s.height, 12.0);
518 }
519
520 #[test]
521 fn measure_text_multiline() {
522 let f = FontMetrics::default();
523 let s = measure_text("Line 1\nLine 2\nLine 3", &f);
524 assert_eq!(s.height, 36.0); }
526
527 #[test]
528 fn wrap_text_no_wrap_needed() {
529 let f = FontMetrics::default();
530 let result = wrap_text("Short", 200.0, &f, 0.0, None);
531 assert_eq!(result.lines.len(), 1);
532 assert_eq!(result.lines[0], "Short");
533 }
534
535 #[test]
536 fn wrap_text_preserves_newlines() {
537 let f = FontMetrics::default();
538 let result = wrap_text("Line 1\nLine 2", 200.0, &f, 0.0, None);
539 assert_eq!(result.lines.len(), 2);
540 assert_eq!(result.lines[0], "Line 1");
541 assert_eq!(result.lines[1], "Line 2");
542 }
543
544 #[test]
545 fn wrap_text_empty_string() {
546 let f = FontMetrics::default();
547 let result = wrap_text("", 100.0, &f, 0.0, None);
548 assert_eq!(result.lines.len(), 0);
549 assert_eq!(result.size.height, 0.0);
550 }
551
552 #[test]
553 fn resolved_widths_measure() {
554 let mut widths = vec![0u16; 256];
555 widths[b'H' as usize] = 700;
556 widths[b'i' as usize] = 300;
557 let f = FontMetrics {
558 size: 10.0,
559 resolved_widths: Some(widths),
560 resolved_upem: Some(1000),
561 ..Default::default()
562 };
563 let w = f.measure_width("Hi");
564 assert!((w - 10.0).abs() < 0.01, "resolved Hi={w}, expected 10.0");
565 }
566
567 #[test]
568 fn resolved_line_height() {
569 let f = FontMetrics {
570 size: 12.0,
571 resolved_ascender: Some(800),
572 resolved_descender: Some(-200),
573 resolved_upem: Some(1000),
574 ..Default::default()
575 };
576 let lh = f.line_height_pt();
577 assert!((lh - 12.0).abs() < 0.01, "resolved lh={lh}, expected 12.0");
578 }
579
580 #[test]
581 fn wrap_text_times_given_name() {
582 let f = FontMetrics {
584 size: 9.0,
585 typeface: FontFamily::Serif,
586 ..Default::default()
587 };
588 let result = wrap_text("Given Name (First Name)", 140.0, &f, 0.0, None);
589 assert_eq!(
590 result.lines.len(),
591 1,
592 "Should fit on 1 line but got: {:?}",
593 result.lines
594 );
595 }
596
597 #[test]
598 fn resolved_widths_upem_2048() {
599 let mut widths = vec![0u16; 256];
602 widths[b'A' as usize] = 600; let f = FontMetrics {
604 size: 10.0,
605 resolved_widths: Some(widths),
606 resolved_upem: Some(2048),
607 ..Default::default()
608 };
609 let w = f.measure_width("A");
610 assert!(
613 (w - 6.0).abs() < 0.01,
614 "upem=2048: A width={w}, expected 6.0"
615 );
616 }
617
618 #[test]
619 fn multibyte_utf8_single_char_width() {
620 let mut widths = vec![0u16; 256];
622 widths[0xE9] = 500; let f = FontMetrics {
624 size: 10.0,
625 resolved_widths: Some(widths),
626 resolved_upem: Some(1000),
627 ..Default::default()
628 };
629 let w = f.measure_width("\u{00E9}");
630 assert!(
633 (w - 5.0).abs() < 0.01,
634 "é width={w}, expected 5.0 (one glyph, not two bytes)"
635 );
636 }
637
638 #[test]
639 fn afm_fallback_multibyte_char() {
640 let f = FontMetrics {
642 size: 10.0,
643 typeface: FontFamily::SansSerif,
644 ..Default::default()
645 };
646 let w_n = f.measure_width("n");
647 let w_e_accent = f.measure_width("\u{00E9}");
648 assert!(
650 (w_e_accent - w_n).abs() < 0.01,
651 "AFM fallback: é={w_e_accent} should equal n={w_n}"
652 );
653 }
654
655 #[test]
656 fn text_split_points_multi() {
657 let pts = text_split_points(4, 12.0);
658 assert_eq!(pts.len(), 3);
659 assert!((pts[0] - 12.0).abs() < 0.01);
660 assert!((pts[1] - 24.0).abs() < 0.01);
661 assert!((pts[2] - 36.0).abs() < 0.01);
662 }
663
664 #[test]
665 fn text_split_points_single_line() {
666 let pts = text_split_points(1, 12.0);
667 assert!(pts.is_empty());
668 }
669}