1use super::{
4 DeterministicTextMeasurer, FLOWCHART_DEFAULT_FONT_KEY, TextMeasurer, TextMetrics, TextStyle,
5 WrapMode, flowchart_default_bold_delta_em, flowchart_default_bold_kern_delta_em,
6 font_key_uses_courier_metrics, is_flowchart_default_font, overrides, round_to_1_64_px,
7 style_requests_bold_font_weight, svg_wrapped_first_line_bbox_height_px,
8};
9
10#[derive(Debug, Clone, Default)]
11pub struct VendoredFontMetricsTextMeasurer {
12 fallback: DeterministicTextMeasurer,
13}
14
15#[derive(Clone, Copy)]
16struct FontMetricProfile<'a> {
17 entries: &'a [(char, f64)],
18 default_em: f64,
19 kern_pairs: &'a [(u32, u32, f64)],
20 space_trigrams: &'a [(u32, u32, f64)],
21 trigrams: &'a [(u32, u32, u32, f64)],
22 missing_v_comma_kern_em: f64,
23 missing_t_o_kern_em: f64,
24 missing_t_r_kern_em: f64,
25 missing_space_before_capital_a_em: f64,
26 missing_space_after_capital_a_before_open_paren_em: f64,
27}
28
29impl VendoredFontMetricsTextMeasurer {
30 fn metric_profile(
31 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
32 ) -> FontMetricProfile<'_> {
33 FontMetricProfile {
34 entries: table.entries,
35 default_em: table.default_em.max(0.1),
36 kern_pairs: table.kern_pairs,
37 space_trigrams: table.space_trigrams,
38 trigrams: table.trigrams,
39 missing_v_comma_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
40 -140.0 / 1024.0
41 } else {
42 0.0
43 },
44 missing_t_o_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
45 -128.0 / 1024.0
46 } else {
47 0.0
48 },
49 missing_t_r_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
50 -113.0 / 1024.0
51 } else {
52 0.0
53 },
54 missing_space_before_capital_a_em: if table.font_key
55 == "trebuchetms,verdana,arial,sans-serif"
56 {
57 -57.0 / 1024.0
58 } else {
59 0.0
60 },
61 missing_space_after_capital_a_before_open_paren_em: if table.font_key
62 == "trebuchetms,verdana,arial,sans-serif"
63 {
64 -57.0 / 1024.0
65 } else {
66 0.0
67 },
68 }
69 }
70
71 pub(super) fn quantize_svg_bbox_px_nearest(v: f64) -> f64 {
72 if !(v.is_finite() && v >= 0.0) {
73 return 0.0;
74 }
75 let x = v * 1024.0;
79 let f = x.floor();
80 let frac = x - f;
81 let i = if frac < 0.5 {
82 f
83 } else if frac > 0.5 {
84 f + 1.0
85 } else {
86 let fi = f as i64;
87 if fi % 2 == 0 { f } else { f + 1.0 }
88 };
89 i / 1024.0
90 }
91
92 fn quantize_svg_half_px_nearest(half_px: f64) -> f64 {
93 if !(half_px.is_finite() && half_px >= 0.0) {
94 return 0.0;
95 }
96 (half_px * 256.0).floor() / 256.0
100 }
101
102 fn normalize_font_key(s: &str) -> String {
103 s.chars()
104 .filter_map(|ch| {
105 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
109 None
110 } else {
111 Some(ch.to_ascii_lowercase())
112 }
113 })
114 .collect()
115 }
116
117 fn lookup_table(
118 &self,
119 style: &TextStyle,
120 ) -> Option<&'static crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables> {
121 let key = style
122 .font_family
123 .as_deref()
124 .map(Self::normalize_font_key)
125 .unwrap_or_default();
126 let key = if key.is_empty() {
127 FLOWCHART_DEFAULT_FONT_KEY
130 } else {
131 key.as_str()
132 };
133 if let Some(t) = crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(key)
134 {
135 return Some(t);
136 }
137
138 let key_lower = key;
141 if font_key_uses_courier_metrics(key_lower) {
142 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
143 "courier",
144 );
145 }
146 if key_lower.contains("sans-serif") {
150 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
151 "sans-serif",
152 );
153 }
154 None
155 }
156
157 fn lookup_char_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
158 fn find_entry_em(entries: &[(char, f64)], ch: char) -> Option<f64> {
159 let mut lo = 0usize;
160 let mut hi = entries.len();
161 while lo < hi {
162 let mid = (lo + hi) / 2;
163 match entries[mid].0.cmp(&ch) {
164 std::cmp::Ordering::Equal => return Some(entries[mid].1),
165 std::cmp::Ordering::Less => lo = mid + 1,
166 std::cmp::Ordering::Greater => hi = mid,
167 }
168 }
169 None
170 }
171
172 if let Some(em) = find_entry_em(entries, ch) {
173 return em;
174 }
175
176 let paired = match ch {
181 '(' => Some(')'),
182 ')' => Some('('),
183 '[' => Some(']'),
184 ']' => Some('['),
185 '{' => Some('}'),
186 '}' => Some('{'),
187 _ => None,
188 };
189 if let Some(other) = paired {
190 if let Some(other_em) = find_entry_em(entries, other) {
191 return other_em;
192 }
193 }
194 if ch.is_ascii() {
195 return default_em;
196 }
197
198 if ('\u{80}'..='\u{9f}').contains(&ch) {
199 return 0.997_8;
204 }
205
206 Self::lookup_non_ascii_fallback_em(default_em, ch)
207 }
208
209 fn lookup_non_ascii_fallback_em(default_em: f64, ch: char) -> f64 {
210 let code = ch as u32;
211
212 if unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) == 0
218 || (0x1f3fb..=0x1f3ff).contains(&code)
219 {
220 return 0.0;
221 }
222 if (0x0590..=0x05ff).contains(&code) {
223 return 0.479_980_468_75;
224 }
225 if (0x1f300..=0x1faff).contains(&code) || (0x2600..=0x27bf).contains(&code) {
226 return 1.249_67;
227 }
228 if (0xac00..=0xd7af).contains(&code) {
229 return 0.864_257_812_5;
230 }
231
232 match unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) {
233 2.. => 1.0,
234 _ => default_em,
235 }
236 }
237
238 fn lookup_kern_em(kern_pairs: &[(u32, u32, f64)], a: char, b: char) -> f64 {
239 let key_a = a as u32;
240 let key_b = b as u32;
241 let mut lo = 0usize;
242 let mut hi = kern_pairs.len();
243 while lo < hi {
244 let mid = (lo + hi) / 2;
245 let (ma, mb, v) = kern_pairs[mid];
246 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
247 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
248 (std::cmp::Ordering::Less, _) => lo = mid + 1,
249 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
250 _ => hi = mid,
251 }
252 }
253 0.0
254 }
255
256 fn lookup_profile_kern_em(profile: FontMetricProfile<'_>, a: char, b: char) -> f64 {
257 let explicit = Self::lookup_kern_em(profile.kern_pairs, a, b);
258 if explicit != 0.0 {
259 return explicit;
260 }
261
262 if a == 'v' && b == ',' {
263 return profile.missing_v_comma_kern_em;
268 }
269 if a == 'T' && b == 'o' {
270 return profile.missing_t_o_kern_em;
271 }
272 if a == 'T' && b == 'r' {
273 return profile.missing_t_r_kern_em;
274 }
275
276 0.0
277 }
278
279 fn lookup_space_trigram_em(space_trigrams: &[(u32, u32, f64)], a: char, b: char) -> f64 {
280 let key_a = a as u32;
281 let key_b = b as u32;
282 let mut lo = 0usize;
283 let mut hi = space_trigrams.len();
284 while lo < hi {
285 let mid = (lo + hi) / 2;
286 let (ma, mb, v) = space_trigrams[mid];
287 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
288 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
289 (std::cmp::Ordering::Less, _) => lo = mid + 1,
290 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
291 _ => hi = mid,
292 }
293 }
294 0.0
295 }
296
297 fn lookup_trigram_em(trigrams: &[(u32, u32, u32, f64)], a: char, b: char, c: char) -> f64 {
298 let key_a = a as u32;
299 let key_b = b as u32;
300 let key_c = c as u32;
301 let mut lo = 0usize;
302 let mut hi = trigrams.len();
303 while lo < hi {
304 let mid = (lo + hi) / 2;
305 let (ma, mb, mc, v) = trigrams[mid];
306 match (ma.cmp(&key_a), mb.cmp(&key_b), mc.cmp(&key_c)) {
307 (
308 std::cmp::Ordering::Equal,
309 std::cmp::Ordering::Equal,
310 std::cmp::Ordering::Equal,
311 ) => return v,
312 (std::cmp::Ordering::Less, _, _) => lo = mid + 1,
313 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less, _) => lo = mid + 1,
314 (
315 std::cmp::Ordering::Equal,
316 std::cmp::Ordering::Equal,
317 std::cmp::Ordering::Less,
318 ) => lo = mid + 1,
319 _ => hi = mid,
320 }
321 }
322 0.0
323 }
324
325 fn is_tiny_lattice_residual_em(v: f64) -> bool {
326 v.abs() <= (1.0 / 1024.0) + 1e-12
331 }
332
333 fn same_glyph_pair_kern_em(
334 profile: FontMetricProfile<'_>,
335 a: char,
336 b: char,
337 same_run_len_after: usize,
338 ) -> f64 {
339 let kern = Self::lookup_profile_kern_em(profile, a, b);
340 if a == b && Self::is_tiny_lattice_residual_em(kern) && same_run_len_after % 2 == 1 {
341 0.0
342 } else {
343 kern
344 }
345 }
346
347 fn same_glyph_trigram_em(profile: FontMetricProfile<'_>, a: char, b: char, c: char) -> f64 {
348 let delta = Self::lookup_trigram_em(profile.trigrams, a, b, c);
349 if a == b && b == c && Self::is_tiny_lattice_residual_em(delta) {
350 0.0
351 } else {
352 delta
353 }
354 }
355
356 fn lookup_html_override_em(overrides: &[(&'static str, f64)], text: &str) -> Option<f64> {
357 let mut lo = 0usize;
358 let mut hi = overrides.len();
359 while lo < hi {
360 let mid = (lo + hi) / 2;
361 let (k, v) = overrides[mid];
362 match k.cmp(text) {
363 std::cmp::Ordering::Equal => return Some(v),
364 std::cmp::Ordering::Less => lo = mid + 1,
365 std::cmp::Ordering::Greater => hi = mid,
366 }
367 }
368 None
369 }
370
371 fn lookup_svg_override_em(
372 overrides: &[(&'static str, f64, f64)],
373 text: &str,
374 ) -> Option<(f64, f64)> {
375 let mut lo = 0usize;
376 let mut hi = overrides.len();
377 while lo < hi {
378 let mid = (lo + hi) / 2;
379 let (k, l, r) = overrides[mid];
380 match k.cmp(text) {
381 std::cmp::Ordering::Equal => return Some((l, r)),
382 std::cmp::Ordering::Less => lo = mid + 1,
383 std::cmp::Ordering::Greater => hi = mid,
384 }
385 }
386 None
387 }
388
389 fn lookup_overhang_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
390 let mut lo = 0usize;
391 let mut hi = entries.len();
392 while lo < hi {
393 let mid = (lo + hi) / 2;
394 match entries[mid].0.cmp(&ch) {
395 std::cmp::Ordering::Equal => return entries[mid].1,
396 std::cmp::Ordering::Less => lo = mid + 1,
397 std::cmp::Ordering::Greater => hi = mid,
398 }
399 }
400 default_em
401 }
402
403 fn line_svg_bbox_extents_px(
404 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
405 text: &str,
406 font_size: f64,
407 ) -> (f64, f64) {
408 let profile = Self::metric_profile(table);
409 let t = text.trim_end();
410 if t.is_empty() {
411 return (0.0, 0.0);
412 }
413
414 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
415 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
416 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
417 return (left, right);
418 }
419
420 if let Some((left, right)) =
421 overrides::lookup_flowchart_svg_bbox_x_px(table.font_key, font_size, t)
422 {
423 return (left, right);
424 }
425
426 let first = t.chars().next().unwrap_or(' ');
427 let last = t.chars().last().unwrap_or(' ');
428
429 let advance_px_unscaled = {
436 let words: Vec<&str> = t.split_whitespace().filter(|s| !s.is_empty()).collect();
437 if words.len() >= 2 {
438 let mut sum_px = 0.0f64;
439 for (idx, w) in words.iter().enumerate() {
440 if idx == 0 {
441 sum_px += Self::line_width_px(profile, w, false, font_size);
442 } else {
443 let seg = format!(" {w}");
444 sum_px += Self::line_width_px(profile, &seg, false, font_size);
445 }
446 }
447 sum_px
448 } else {
449 Self::line_width_px(profile, t, false, font_size)
450 }
451 };
452
453 let advance_px = advance_px_unscaled * table.svg_scale;
454 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
455 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
463 0.0
464 } else {
465 Self::lookup_overhang_em(
466 table.svg_bbox_overhang_left,
467 table.svg_bbox_overhang_left_default_em,
468 first,
469 )
470 };
471 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
472 0.0
473 } else {
474 Self::lookup_overhang_em(
475 table.svg_bbox_overhang_right,
476 table.svg_bbox_overhang_right_default_em,
477 last,
478 )
479 };
480
481 let left = (half + left_oh_em * font_size).max(0.0);
482 let right = (half + right_oh_em * font_size).max(0.0);
483 (left, right)
484 }
485
486 fn line_svg_bbox_extents_px_single_run(
487 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
488 text: &str,
489 font_size: f64,
490 ) -> (f64, f64) {
491 let profile = Self::metric_profile(table);
492 let t = text.trim_end();
493 if t.is_empty() {
494 return (0.0, 0.0);
495 }
496
497 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
498 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
499 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
500 return (left, right);
501 }
502
503 let first = t.chars().next().unwrap_or(' ');
504 let last = t.chars().last().unwrap_or(' ');
505
506 let advance_px_unscaled = Self::line_width_px(profile, t, false, font_size);
509
510 let advance_px = advance_px_unscaled * table.svg_scale;
511 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
512
513 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
514 0.0
515 } else {
516 Self::lookup_overhang_em(
517 table.svg_bbox_overhang_left,
518 table.svg_bbox_overhang_left_default_em,
519 first,
520 )
521 };
522 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
523 0.0
524 } else {
525 Self::lookup_overhang_em(
526 table.svg_bbox_overhang_right,
527 table.svg_bbox_overhang_right_default_em,
528 last,
529 )
530 };
531
532 let left = (half + left_oh_em * font_size).max(0.0);
533 let right = (half + right_oh_em * font_size).max(0.0);
534 (left, right)
535 }
536
537 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang(
538 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
539 text: &str,
540 font_size: f64,
541 ) -> (f64, f64) {
542 let profile = Self::metric_profile(table);
543 let t = text.trim_end();
544 if t.is_empty() {
545 return (0.0, 0.0);
546 }
547
548 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
549 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
550 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
551 return (left, right);
552 }
553
554 let first = t.chars().next().unwrap_or(' ');
555 let last = t.chars().last().unwrap_or(' ');
556
557 let advance_px_unscaled = Self::line_width_px(profile, t, false, font_size);
558
559 let advance_px = advance_px_unscaled * table.svg_scale;
560 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
561
562 let left_oh_em = Self::lookup_overhang_em(
563 table.svg_bbox_overhang_left,
564 table.svg_bbox_overhang_left_default_em,
565 first,
566 );
567 let right_oh_em = Self::lookup_overhang_em(
568 table.svg_bbox_overhang_right,
569 table.svg_bbox_overhang_right_default_em,
570 last,
571 );
572
573 let left = (half + left_oh_em * font_size).max(0.0);
574 let right = (half + right_oh_em * font_size).max(0.0);
575 (left, right)
576 }
577
578 fn line_svg_bbox_width_px(
579 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
580 text: &str,
581 font_size: f64,
582 ) -> f64 {
583 let (l, r) = Self::line_svg_bbox_extents_px(table, text, font_size);
584 (l + r).max(0.0)
585 }
586
587 fn line_svg_bbox_width_single_run_px(
588 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
589 text: &str,
590 font_size: f64,
591 ) -> f64 {
592 let t = text.trim_end();
593 if !t.is_empty() {
594 if let Some((left_em, right_em)) =
595 overrides::lookup_sequence_svg_override_em(table.font_key, t)
596 {
597 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
598 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
599 return (left + right).max(0.0);
600 }
601 }
602
603 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, text, font_size);
604 (l + r).max(0.0)
605 }
606
607 fn line_svg_title_bbox_extents_px(
608 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
609 text: &str,
610 font_size: f64,
611 ) -> (f64, f64) {
612 let profile = Self::metric_profile(table);
613 let t = text.trim_end();
614 if t.is_empty() {
615 return (0.0, 0.0);
616 }
617
618 let advance_px = if let Some(em) = Self::lookup_html_override_em(table.html_overrides, t) {
623 em * font_size
624 } else {
625 Self::line_width_px(profile, t, false, font_size) * table.svg_scale
626 };
627 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
628 (half, half)
629 }
630
631 fn split_token_to_svg_bbox_width_px(
632 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
633 tok: &str,
634 max_width_px: f64,
635 font_size: f64,
636 ) -> (String, String) {
637 if max_width_px <= 0.0 {
638 return (tok.to_string(), String::new());
639 }
640 let chars = tok.chars().collect::<Vec<_>>();
641 if chars.is_empty() {
642 return (String::new(), String::new());
643 }
644
645 let first = chars[0];
646 let left_oh_em = if first.is_ascii() {
647 0.0
648 } else {
649 Self::lookup_overhang_em(
650 table.svg_bbox_overhang_left,
651 table.svg_bbox_overhang_left_default_em,
652 first,
653 )
654 };
655
656 let mut em = 0.0;
657 let mut prev: Option<char> = None;
658 let mut split_at = 1usize;
659 for (idx, ch) in chars.iter().enumerate() {
660 em += Self::lookup_char_em(table.entries, table.default_em.max(0.1), *ch);
661 if let Some(p) = prev {
662 em += Self::lookup_kern_em(table.kern_pairs, p, *ch);
663 }
664 prev = Some(*ch);
665
666 let right_oh_em = if ch.is_ascii() {
667 0.0
668 } else {
669 Self::lookup_overhang_em(
670 table.svg_bbox_overhang_right,
671 table.svg_bbox_overhang_right_default_em,
672 *ch,
673 )
674 };
675 let half_px = Self::quantize_svg_half_px_nearest(
676 (em * font_size * table.svg_scale / 2.0).max(0.0),
677 );
678 let w_px = 2.0 * half_px + (left_oh_em + right_oh_em) * font_size;
679 if w_px.is_finite() && w_px <= max_width_px {
680 split_at = idx + 1;
681 } else if idx > 0 {
682 break;
683 }
684 }
685 let head = chars[..split_at].iter().collect::<String>();
686 let tail = chars[split_at..].iter().collect::<String>();
687 (head, tail)
688 }
689
690 fn wrap_text_lines_svg_bbox_px(
691 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
692 text: &str,
693 max_width_px: Option<f64>,
694 font_size: f64,
695 tokenize_whitespace: bool,
696 ) -> Vec<String> {
697 const EPS_PX: f64 = 0.125;
698 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
699 let width_fn = if tokenize_whitespace {
700 Self::line_svg_bbox_width_px
701 } else {
702 Self::line_svg_bbox_width_single_run_px
703 };
704
705 let mut lines = Vec::new();
706 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
707 let Some(w) = max_width_px else {
708 lines.push(line);
709 continue;
710 };
711
712 let mut tokens = std::collections::VecDeque::from(
713 DeterministicTextMeasurer::split_line_to_words(&line),
714 );
715 let mut out: Vec<String> = Vec::new();
716 let mut cur = String::new();
717
718 while let Some(tok) = tokens.pop_front() {
719 if cur.is_empty() && tok == " " {
720 continue;
721 }
722
723 let candidate = format!("{cur}{tok}");
724 let candidate_trimmed = candidate.trim_end();
725 if width_fn(table, candidate_trimmed, font_size) <= w + EPS_PX {
726 cur = candidate;
727 continue;
728 }
729
730 if !cur.trim().is_empty() {
731 out.push(cur.trim_end().to_string());
732 cur.clear();
733 tokens.push_front(tok);
734 continue;
735 }
736
737 if tok == " " {
738 continue;
739 }
740
741 if width_fn(table, tok.as_str(), font_size) <= w + EPS_PX {
742 cur = tok;
743 continue;
744 }
745
746 let (head, tail) =
748 Self::split_token_to_svg_bbox_width_px(table, &tok, w + EPS_PX, font_size);
749 out.push(head);
750 if !tail.is_empty() {
751 tokens.push_front(tail);
752 }
753 }
754
755 if !cur.trim().is_empty() {
756 out.push(cur.trim_end().to_string());
757 }
758
759 if out.is_empty() {
760 lines.push("".to_string());
761 } else {
762 lines.extend(out);
763 }
764 }
765
766 if lines.is_empty() {
767 vec!["".to_string()]
768 } else {
769 lines
770 }
771 }
772
773 fn line_width_px(
774 profile: FontMetricProfile<'_>,
775 text: &str,
776 bold: bool,
777 font_size: f64,
778 ) -> f64 {
779 fn normalize_whitespace_like(ch: char) -> (char, f64) {
780 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
788 if ch == '\u{00A0}' {
789 (' ', NBSP_DELTA_EM)
790 } else {
791 (ch, 0.0)
792 }
793 }
794
795 let mut em = 0.0;
796 let mut prevprev: Option<char> = None;
797 let mut prev: Option<char> = None;
798 let mut same_run_len = 0usize;
799 for ch in text.chars() {
800 let (ch, delta_em) = normalize_whitespace_like(ch);
801 let next_same_run_len = if prev == Some(ch) {
802 same_run_len + 1
803 } else {
804 1
805 };
806 em += Self::lookup_char_em(profile.entries, profile.default_em, ch) + delta_em;
807 if let Some(p) = prev {
808 em += Self::same_glyph_pair_kern_em(profile, p, ch, next_same_run_len);
809 }
810 if bold {
811 if let Some(p) = prev {
812 em += flowchart_default_bold_kern_delta_em(p, ch);
813 }
814 em += flowchart_default_bold_delta_em(ch);
815 }
816 if let (Some(a), Some(b)) = (prevprev, prev) {
817 if b == ' ' {
818 if !(a.is_whitespace() || ch.is_whitespace()) {
819 let space_delta =
820 Self::lookup_space_trigram_em(profile.space_trigrams, a, ch);
821 if space_delta != 0.0 {
822 em += space_delta;
823 } else if a == 'A' && ch == '(' {
824 em += profile.missing_space_after_capital_a_before_open_paren_em;
825 } else if ch == 'A' && a.is_ascii_alphanumeric() {
826 em += profile.missing_space_before_capital_a_em;
831 }
832 }
833 } else if !(a.is_whitespace() || b.is_whitespace() || ch.is_whitespace()) {
834 em += Self::same_glyph_trigram_em(profile, a, b, ch);
835 }
836 }
837 prevprev = prev;
838 prev = Some(ch);
839 same_run_len = next_same_run_len;
840 }
841 em * font_size
842 }
843
844 fn split_token_to_width_px(
845 profile: FontMetricProfile<'_>,
846 tok: &str,
847 max_width_px: f64,
848 bold: bool,
849 font_size: f64,
850 ) -> (String, String) {
851 fn normalize_whitespace_like(ch: char) -> (char, f64) {
852 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
853 if ch == '\u{00A0}' {
854 (' ', NBSP_DELTA_EM)
855 } else {
856 (ch, 0.0)
857 }
858 }
859
860 if max_width_px <= 0.0 {
861 return (tok.to_string(), String::new());
862 }
863 let max_em = max_width_px / font_size.max(1.0);
864 let mut em = 0.0;
865 let mut prevprev: Option<char> = None;
866 let mut prev: Option<char> = None;
867 let mut same_run_len = 0usize;
868 let chars = tok.chars().collect::<Vec<_>>();
869 let mut split_at = 0usize;
870 for (idx, ch) in chars.iter().enumerate() {
871 let (ch_norm, delta_em) = normalize_whitespace_like(*ch);
872 let next_same_run_len = if prev == Some(ch_norm) {
873 same_run_len + 1
874 } else {
875 1
876 };
877 em += Self::lookup_char_em(profile.entries, profile.default_em, ch_norm) + delta_em;
878 if let Some(p) = prev {
879 em += Self::same_glyph_pair_kern_em(profile, p, ch_norm, next_same_run_len);
880 }
881 if bold {
882 if let Some(p) = prev {
883 em += flowchart_default_bold_kern_delta_em(p, ch_norm);
884 }
885 em += flowchart_default_bold_delta_em(ch_norm);
886 }
887 if let (Some(a), Some(b)) = (prevprev, prev) {
888 if !(a.is_whitespace() || b.is_whitespace() || ch_norm.is_whitespace()) {
889 em += Self::same_glyph_trigram_em(profile, a, b, ch_norm);
890 }
891 }
892 prevprev = prev;
893 prev = Some(ch_norm);
894 same_run_len = next_same_run_len;
895 if em > max_em && idx > 0 {
896 break;
897 }
898 split_at = idx + 1;
899 if em >= max_em {
900 break;
901 }
902 }
903 if split_at == 0 {
904 split_at = 1.min(chars.len());
905 }
906 let head = chars.iter().take(split_at).collect::<String>();
907 let tail = chars.iter().skip(split_at).collect::<String>();
908 (head, tail)
909 }
910
911 fn wrap_line_to_width_px(
912 profile: FontMetricProfile<'_>,
913 line: &str,
914 max_width_px: f64,
915 font_size: f64,
916 break_long_words: bool,
917 bold: bool,
918 ) -> Vec<String> {
919 fn split_html_breakable_segments(tok: &str) -> Vec<String> {
920 let hyphen_count = tok.chars().filter(|ch| *ch == '-').count();
928 let char_count = tok.chars().count();
929 let is_hyphenated_compound = hyphen_count >= 2 && char_count >= 16;
930 let is_url_like = tok.starts_with("http://") || tok.starts_with("https://");
931 let is_path_like = is_hyphenated_compound
932 || is_url_like
933 || tok.len() >= 24
934 && tok
935 .chars()
936 .filter(|ch| {
937 matches!(ch, '/' | '\\' | '-' | ':' | '?' | '&' | '#' | '[' | ']')
938 })
939 .count()
940 >= 2;
941 if !is_path_like {
942 return vec![tok.to_string()];
943 }
944
945 fn is_break_after(ch: char, is_url_like: bool) -> bool {
946 matches!(ch, '/' | '-' | ':' | '?' | '&' | '#' | ')' | ']' | '}')
947 || (is_url_like && ch == '.')
948 }
949
950 let mut out: Vec<String> = Vec::new();
951 let mut cur = String::new();
952 for ch in tok.chars() {
953 cur.push(ch);
954 if is_break_after(ch, is_url_like) && !cur.is_empty() {
955 out.push(std::mem::take(&mut cur));
956 }
957 }
958 if !cur.is_empty() {
959 out.push(cur);
960 }
961 if out.len() <= 1 {
962 vec![tok.to_string()]
963 } else {
964 out
965 }
966 }
967
968 let max_width_px = if break_long_words {
973 max_width_px
974 } else {
975 max_width_px + (1.0 / 64.0)
976 };
977
978 let mut tokens =
979 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
980 let mut out: Vec<String> = Vec::new();
981 let mut cur = String::new();
982
983 while let Some(tok) = tokens.pop_front() {
984 if cur.is_empty() && tok == " " {
985 continue;
986 }
987
988 let candidate = format!("{cur}{tok}");
989 let candidate_trimmed = candidate.trim_end();
990 if Self::line_width_px(profile, candidate_trimmed, bold, font_size) <= max_width_px {
991 cur = candidate;
992 continue;
993 }
994
995 if !break_long_words && tok != " " && !cur.trim().is_empty() {
996 let segments = split_html_breakable_segments(&tok);
1000 if segments.len() > 1 {
1001 let mut cur_candidate = cur.clone();
1002 let mut consumed = 0usize;
1003 for seg in &segments {
1004 let candidate = format!("{cur_candidate}{seg}");
1005 let candidate_trimmed = candidate.trim_end();
1006 if Self::line_width_px(profile, candidate_trimmed, bold, font_size)
1007 <= max_width_px
1008 {
1009 cur_candidate = candidate;
1010 consumed += 1;
1011 } else {
1012 break;
1013 }
1014 }
1015 if consumed > 0 {
1016 cur = cur_candidate;
1017 for seg in segments.into_iter().skip(consumed).rev() {
1018 tokens.push_front(seg);
1019 }
1020 continue;
1021 }
1022 }
1023 }
1024
1025 if !cur.trim().is_empty() {
1026 out.push(cur.trim_end().to_string());
1027 cur.clear();
1028 }
1029
1030 if tok == " " {
1031 continue;
1032 }
1033
1034 if Self::line_width_px(profile, tok.as_str(), bold, font_size) <= max_width_px {
1035 cur = tok;
1036 continue;
1037 }
1038
1039 if !break_long_words {
1040 let segments = split_html_breakable_segments(&tok);
1041 if segments.len() > 1 {
1042 for seg in segments.into_iter().rev() {
1043 tokens.push_front(seg);
1044 }
1045 continue;
1046 }
1047 out.push(tok);
1048 continue;
1049 }
1050
1051 let (head, tail) =
1052 Self::split_token_to_width_px(profile, &tok, max_width_px, bold, font_size);
1053 out.push(head);
1054 if !tail.is_empty() {
1055 tokens.push_front(tail);
1056 }
1057 }
1058
1059 if !cur.trim().is_empty() {
1060 out.push(cur.trim_end().to_string());
1061 }
1062
1063 if out.is_empty() {
1064 vec!["".to_string()]
1065 } else {
1066 out
1067 }
1068 }
1069
1070 fn wrap_text_lines_px(
1071 profile: FontMetricProfile<'_>,
1072 text: &str,
1073 style: &TextStyle,
1074 bold: bool,
1075 max_width_px: Option<f64>,
1076 wrap_mode: WrapMode,
1077 ) -> Vec<String> {
1078 let font_size = style.font_size.max(1.0);
1079 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
1080 let break_long_words = wrap_mode == WrapMode::SvgLike;
1081
1082 let mut lines = Vec::new();
1083 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1084 if let Some(w) = max_width_px {
1085 lines.extend(Self::wrap_line_to_width_px(
1086 profile,
1087 &line,
1088 w,
1089 font_size,
1090 break_long_words,
1091 bold,
1092 ));
1093 } else {
1094 lines.push(line);
1095 }
1096 }
1097
1098 if lines.is_empty() {
1099 vec!["".to_string()]
1100 } else {
1101 lines
1102 }
1103 }
1104}
1105
1106fn vendored_measure_wrapped_impl(
1107 measurer: &VendoredFontMetricsTextMeasurer,
1108 text: &str,
1109 style: &TextStyle,
1110 max_width: Option<f64>,
1111 wrap_mode: WrapMode,
1112 use_html_overrides: bool,
1113) -> (TextMetrics, Option<f64>) {
1114 let Some(table) = measurer.lookup_table(style) else {
1115 return measurer
1116 .fallback
1117 .measure_wrapped_with_raw_width(text, style, max_width, wrap_mode);
1118 };
1119
1120 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
1121 let font_size = style.font_size.max(1.0);
1122 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
1123 let line_height_factor = match wrap_mode {
1124 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
1125 WrapMode::HtmlLike => 1.5,
1126 };
1127
1128 let html_overrides: &[(&'static str, f64)] = if use_html_overrides && !bold {
1129 table.html_overrides
1130 } else {
1131 &[]
1132 };
1133 let profile = VendoredFontMetricsTextMeasurer::metric_profile(table);
1134
1135 let html_override_px = |em: f64| -> f64 {
1136 if (font_size - table.base_font_size_px).abs() < 0.01 {
1144 em * font_size
1145 } else {
1146 em * table.base_font_size_px
1147 }
1148 };
1149
1150 let html_width_override_px = |line: &str| -> Option<f64> {
1151 overrides::lookup_flowchart_html_width_px(table.font_key, font_size, line)
1154 };
1155
1156 let raw_width_unscaled = if wrap_mode == WrapMode::HtmlLike {
1165 let mut raw_w: f64 = 0.0;
1166 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1167 if let Some(w) = html_width_override_px(&line) {
1168 raw_w = raw_w.max(w);
1169 continue;
1170 }
1171 if let Some(em) =
1172 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, &line)
1173 {
1174 raw_w = raw_w.max(html_override_px(em));
1175 } else {
1176 raw_w = raw_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
1177 profile, &line, bold, font_size,
1178 ));
1179 }
1180 }
1181 Some(raw_w)
1182 } else {
1183 None
1184 };
1185
1186 fn split_html_min_content_segments(tok: &str) -> Vec<String> {
1199 fn is_break_after(ch: char) -> bool {
1205 matches!(ch, '-' | '?' | '&' | '#')
1206 }
1207
1208 let mut out: Vec<String> = Vec::new();
1209 let mut cur = String::new();
1210 for ch in tok.chars() {
1211 cur.push(ch);
1212 if is_break_after(ch) && !cur.is_empty() {
1213 out.push(std::mem::take(&mut cur));
1214 }
1215 }
1216 if !cur.is_empty() {
1217 out.push(cur);
1218 }
1219 if out.len() <= 1 {
1220 vec![tok.to_string()]
1221 } else {
1222 out
1223 }
1224 }
1225
1226 let html_min_content_width = if wrap_mode == WrapMode::HtmlLike && max_width.is_some() {
1227 let mut max_word_w: f64 = 0.0;
1228 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1229 for part in line.split(' ') {
1230 let part = part.trim();
1231 if part.is_empty() {
1232 continue;
1233 }
1234 for seg in split_html_min_content_segments(part) {
1235 max_word_w = max_word_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
1236 profile,
1237 seg.as_str(),
1238 bold,
1239 font_size,
1240 ));
1241 }
1242 }
1243 }
1244 if max_word_w.is_finite() && max_word_w > 0.0 {
1245 Some(max_word_w)
1246 } else {
1247 None
1248 }
1249 } else {
1250 None
1251 };
1252
1253 let lines = match wrap_mode {
1254 WrapMode::HtmlLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_px(
1255 profile, text, style, bold, max_width, wrap_mode,
1256 ),
1257 WrapMode::SvgLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
1258 table, text, max_width, font_size, true,
1259 ),
1260 WrapMode::SvgLikeSingleRun => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
1261 table, text, max_width, font_size, false,
1262 ),
1263 };
1264
1265 let mut width: f64 = 0.0;
1266 match wrap_mode {
1267 WrapMode::HtmlLike => {
1268 for line in &lines {
1269 if let Some(w) = html_width_override_px(line) {
1270 width = width.max(w);
1271 continue;
1272 }
1273 if let Some(em) =
1274 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, line)
1275 {
1276 width = width.max(html_override_px(em));
1277 } else {
1278 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
1279 profile, line, bold, font_size,
1280 ));
1281 }
1282 }
1283 }
1284 WrapMode::SvgLike => {
1285 for line in &lines {
1286 width = width.max(VendoredFontMetricsTextMeasurer::line_svg_bbox_width_px(
1287 table, line, font_size,
1288 ));
1289 }
1290 }
1291 WrapMode::SvgLikeSingleRun => {
1292 for line in &lines {
1293 width = width.max(
1294 VendoredFontMetricsTextMeasurer::line_svg_bbox_width_single_run_px(
1295 table, line, font_size,
1296 ),
1297 );
1298 }
1299 }
1300 }
1301
1302 if wrap_mode == WrapMode::HtmlLike {
1306 let needs_wrap = max_width.is_some_and(|w| raw_width_unscaled.is_some_and(|rw| rw > w));
1307 if let Some(w) = max_width {
1308 if needs_wrap {
1309 width = width.max(w);
1310 } else {
1311 width = width.min(w);
1312 }
1313 }
1314 if needs_wrap {
1315 if let Some(w) = html_min_content_width {
1316 width = width.max(w);
1317 }
1318 }
1319 width = round_to_1_64_px(width);
1322 if let Some(w) = max_width {
1323 width = if needs_wrap {
1324 width.max(w)
1325 } else {
1326 width.min(w)
1327 };
1328 }
1329 }
1330
1331 let height = match wrap_mode {
1332 WrapMode::HtmlLike => lines.len() as f64 * font_size * line_height_factor,
1333 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
1334 if lines.is_empty() {
1335 0.0
1336 } else {
1337 let first_line_h = svg_wrapped_first_line_bbox_height_px(style);
1343 let additional = (lines.len().saturating_sub(1)) as f64 * font_size * 1.1;
1344 first_line_h + additional
1345 }
1346 }
1347 };
1348
1349 let metrics = TextMetrics {
1350 width,
1351 height,
1352 line_count: lines.len(),
1353 };
1354 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
1355 raw_width_unscaled
1356 } else {
1357 None
1358 };
1359 (metrics, raw_width_px)
1360}
1361
1362impl TextMeasurer for VendoredFontMetricsTextMeasurer {
1363 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
1364 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
1365 }
1366
1367 fn measure_svg_text_computed_length_px(&self, text: &str, style: &TextStyle) -> f64 {
1368 let Some(table) = self.lookup_table(style) else {
1369 return self
1370 .fallback
1371 .measure_svg_text_computed_length_px(text, style);
1372 };
1373
1374 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
1375 let font_size = style.font_size.max(1.0);
1376 let profile = VendoredFontMetricsTextMeasurer::metric_profile(table);
1377 let mut width: f64 = 0.0;
1378 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1379 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
1380 profile, &line, bold, font_size,
1381 ));
1382 }
1383 if width.is_finite() && width >= 0.0 {
1384 width
1385 } else {
1386 0.0
1387 }
1388 }
1389
1390 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1391 let Some(table) = self.lookup_table(style) else {
1392 return self.fallback.measure_svg_text_bbox_x(text, style);
1393 };
1394
1395 let font_size = style.font_size.max(1.0);
1396 let mut left: f64 = 0.0;
1397 let mut right: f64 = 0.0;
1398 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1399 let (l, r) = Self::line_svg_bbox_extents_px(table, &line, font_size);
1400 left = left.max(l);
1401 right = right.max(r);
1402 }
1403 (left, right)
1404 }
1405
1406 fn measure_svg_text_bbox_x_with_ascii_overhang(
1407 &self,
1408 text: &str,
1409 style: &TextStyle,
1410 ) -> (f64, f64) {
1411 let Some(table) = self.lookup_table(style) else {
1412 return self
1413 .fallback
1414 .measure_svg_text_bbox_x_with_ascii_overhang(text, style);
1415 };
1416
1417 let font_size = style.font_size.max(1.0);
1418 let mut left: f64 = 0.0;
1419 let mut right: f64 = 0.0;
1420 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1421 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1422 table, &line, font_size,
1423 );
1424 left = left.max(l);
1425 right = right.max(r);
1426 }
1427 (left, right)
1428 }
1429
1430 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1431 let Some(table) = self.lookup_table(style) else {
1432 return self.fallback.measure_svg_title_bbox_x(text, style);
1433 };
1434
1435 let font_size = style.font_size.max(1.0);
1436 let mut left: f64 = 0.0;
1437 let mut right: f64 = 0.0;
1438 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1439 let (l, r) = Self::line_svg_title_bbox_extents_px(table, &line, font_size);
1440 left = left.max(l);
1441 right = right.max(r);
1442 }
1443 (left, right)
1444 }
1445
1446 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
1447 let Some(table) = self.lookup_table(style) else {
1448 return self
1449 .fallback
1450 .measure_svg_simple_text_bbox_width_px(text, style);
1451 };
1452
1453 let font_size = style.font_size.max(1.0);
1454 let t = text.trim_end();
1455 if !t.is_empty() {
1456 if let Some((left_em, right_em)) =
1457 overrides::lookup_sequence_svg_override_em(table.font_key, t)
1458 {
1459 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1460 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1461 return (left + right).max(0.0);
1462 }
1463 }
1464
1465 let mut width: f64 = 0.0;
1466 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1467 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1468 table, &line, font_size,
1469 );
1470 width = width.max((l + r).max(0.0));
1471 }
1472 width
1473 }
1474
1475 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
1476 let t = text.trim_end();
1477 if t.is_empty() {
1478 return 0.0;
1479 }
1480 let font_size = style.font_size.max(1.0);
1483 (font_size * 1.1).max(0.0)
1484 }
1485
1486 fn measure_wrapped(
1487 &self,
1488 text: &str,
1489 style: &TextStyle,
1490 max_width: Option<f64>,
1491 wrap_mode: WrapMode,
1492 ) -> TextMetrics {
1493 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true).0
1494 }
1495
1496 fn measure_wrapped_with_raw_width(
1497 &self,
1498 text: &str,
1499 style: &TextStyle,
1500 max_width: Option<f64>,
1501 wrap_mode: WrapMode,
1502 ) -> (TextMetrics, Option<f64>) {
1503 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true)
1504 }
1505
1506 fn measure_wrapped_raw(
1507 &self,
1508 text: &str,
1509 style: &TextStyle,
1510 max_width: Option<f64>,
1511 wrap_mode: WrapMode,
1512 ) -> TextMetrics {
1513 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, false).0
1514 }
1515}