1use std::borrow::Cow;
2
3use crate::{
4 EmbeddedFontId, Font, ShapedGlyph, advance_units_to_pt, normalize::nfc_text, shape, text_width,
5};
6
7#[must_use]
31pub fn shape_text(font: Font, size: f32, text: &str) -> ShapedRun {
32 let text = nfc_text(text);
33 let text = text.as_ref();
34 match font {
35 Font::Base14(_) => ShapedRun {
36 glyphs: Vec::new(),
37 advance_pt: text_width(font, size, text),
38 },
39 Font::Embedded(id) => {
40 let ef = id.data();
41 let glyphs = shape(ef, text);
42 let upem = f32::from(ef.units_per_em);
43 let advance_pt: f32 = glyphs
44 .iter()
45 .map(|g| advance_units_to_pt(g.advance_units, size, upem))
46 .sum();
47 ShapedRun { glyphs, advance_pt }
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
68pub struct ShapedRun {
69 pub glyphs: Vec<ShapedGlyph>,
71 pub advance_pt: f32,
73}
74
75#[derive(Debug, Clone)]
100pub struct WordSubRun {
101 pub font: Font,
105 pub text: String,
109 pub glyphs: Vec<ShapedGlyph>,
114 pub advance_pt: f32,
118}
119
120#[must_use]
146pub fn split_runs_at(
147 runs: &[WordSubRun],
148 offset: usize,
149 size_pt: f32,
150 fallbacks: &[EmbeddedFontId],
151) -> Option<(Vec<WordSubRun>, Vec<WordSubRun>)> {
152 let mut prefix = Vec::new();
153 let mut suffix = Vec::new();
154 let mut start = 0_usize;
155 for run in runs {
156 let end = start.checked_add(run.text.len())?;
157 if end <= offset {
158 prefix.push(run.clone());
159 } else if start >= offset {
160 suffix.push(run.clone());
161 } else {
162 let local_offset = offset - start;
163 let before = run.text.get(..local_offset)?;
164 let after = run.text.get(local_offset..)?;
165 prefix.extend(shape_with_fallback(run.font, fallbacks, size_pt, before));
166 suffix.extend(shape_with_fallback(run.font, fallbacks, size_pt, after));
167 }
168 start = end;
169 }
170 (offset <= start).then_some((prefix, suffix))
171}
172
173#[must_use]
211pub fn shape_with_fallback(
212 primary: Font,
213 fallbacks: &[EmbeddedFontId],
214 size_pt: f32,
215 text: &str,
216) -> Vec<WordSubRun> {
217 let text = nfc_text(text);
218 let text = text.as_ref();
219 if text.is_empty() {
220 return Vec::new();
221 }
222
223 let primary_id = match primary {
224 Font::Base14(_) => {
225 return vec![WordSubRun {
227 font: primary,
228 text: text.to_owned(),
229 glyphs: Vec::new(),
230 advance_pt: text_width(primary, size_pt, text),
231 }];
232 }
233 Font::Embedded(id) => id,
234 };
235
236 let primary_ef = primary_id.data();
237 let primary_glyphs = shape(primary_ef, text);
238
239 if primary_glyphs.iter().all(|g| g.gid != 0) || fallbacks.is_empty() {
240 return vec![into_subrun(
242 primary,
243 text.to_owned(),
244 primary_glyphs,
245 primary_ef.units_per_em,
246 size_pt,
247 )];
248 }
249
250 let clusters = glyph_clusters(&primary_glyphs, text.len());
253
254 let resolutions = clusters.map(|cluster| {
258 let has_notdef = cluster.glyphs.iter().any(|g| g.gid == 0);
259 if !has_notdef {
260 return ClusterResolution {
261 font: primary,
262 byte_range: cluster.byte_range,
263 glyphs: Cow::Borrowed(cluster.glyphs),
264 };
265 }
266 let cluster_text = &text[cluster.byte_range.clone()];
269 let mut accepted: Option<(Font, Vec<ShapedGlyph>)> = None;
270 for &fb_id in fallbacks {
271 let fb_font = Font::Embedded(fb_id);
272 let fb_ef = fb_id.data();
273 let fb_glyphs = shape(fb_ef, cluster_text);
274 if !fb_glyphs.is_empty() && fb_glyphs.iter().all(|g| g.gid != 0) {
275 let shift = u32::try_from(cluster.byte_range.start).unwrap_or(u32::MAX);
283 let shifted: Vec<_> = fb_glyphs
284 .into_iter()
285 .map(|g| ShapedGlyph {
286 cluster: g.cluster + shift,
287 ..g
288 })
289 .collect();
290 accepted = Some((fb_font, shifted));
291 break;
292 }
293 }
294 match accepted {
295 Some((fb_font, fb_glyphs)) => ClusterResolution {
296 font: fb_font,
297 byte_range: cluster.byte_range,
298 glyphs: Cow::Owned(fb_glyphs),
299 },
300 None => ClusterResolution {
301 font: primary,
302 byte_range: cluster.byte_range,
303 glyphs: Cow::Borrowed(cluster.glyphs),
304 },
305 }
306 });
307
308 let mut subruns: Vec<WordSubRun> = Vec::new();
310 let mut current: Option<(Font, std::ops::Range<usize>, Vec<ShapedGlyph>)> = None;
311 for res in resolutions {
312 match current.take() {
313 Some((font, range, mut glyphs)) if font == res.font => {
314 let new_range = range.start..res.byte_range.end;
315 glyphs.extend_from_slice(&res.glyphs);
316 current = Some((font, new_range, glyphs));
317 }
318 Some((font, range, glyphs)) => {
319 subruns.push(finalize_subrun(font, range, glyphs, text, size_pt));
320 current = Some((res.font, res.byte_range, res.glyphs.into_owned()));
321 }
322 None => current = Some((res.font, res.byte_range, res.glyphs.into_owned())),
323 }
324 }
325 if let Some((font, range, glyphs)) = current {
326 subruns.push(finalize_subrun(font, range, glyphs, text, size_pt));
327 }
328 subruns
329}
330
331#[derive(Clone, Debug)]
335pub struct GlyphCluster<'a> {
336 pub byte_range: std::ops::Range<usize>,
338 pub glyphs: &'a [ShapedGlyph],
340}
341
342struct ClusterResolution<'a> {
345 font: Font,
346 byte_range: std::ops::Range<usize>,
347 glyphs: Cow<'a, [ShapedGlyph]>,
348}
349
350pub fn glyph_clusters(
374 glyphs: &[ShapedGlyph],
375 text_len: usize,
376) -> impl std::iter::FusedIterator<Item = GlyphCluster<'_>> {
377 let mut start = 0;
378 std::iter::from_fn(move || {
379 let cluster = glyphs.get(start)?.cluster;
380 let mut end = start + 1;
381 while glyphs
382 .get(end)
383 .is_some_and(|glyph| glyph.cluster == cluster)
384 {
385 end += 1;
386 }
387 let end_byte = glyphs
388 .get(end)
389 .map_or(text_len, |glyph| glyph.cluster as usize);
390 let group = GlyphCluster {
391 byte_range: cluster as usize..end_byte,
392 glyphs: &glyphs[start..end],
393 };
394 start = end;
395 Some(group)
396 })
397 .fuse()
398}
399
400fn finalize_subrun(
405 font: Font,
406 byte_range: std::ops::Range<usize>,
407 glyphs: Vec<ShapedGlyph>,
408 parent_text: &str,
409 size_pt: f32,
410) -> WordSubRun {
411 let local_text = parent_text[byte_range.clone()].to_owned();
412 let shift = u32::try_from(byte_range.start).unwrap_or(u32::MAX);
417 let rebased: Vec<_> = glyphs
418 .into_iter()
419 .map(|g| ShapedGlyph {
420 cluster: g.cluster.saturating_sub(shift),
421 ..g
422 })
423 .collect();
424 let advance_pt: f32 = match font {
425 Font::Embedded(id) => {
426 let upem = embedded_upem(id);
427 rebased
428 .iter()
429 .map(|g| advance_units_to_pt(g.advance_units, size_pt, upem))
430 .sum()
431 }
432 Font::Base14(_) => text_width(font, size_pt, &local_text),
433 };
434 WordSubRun {
435 font,
436 text: local_text,
437 glyphs: rebased,
438 advance_pt,
439 }
440}
441
442fn into_subrun(
446 font: Font,
447 text: String,
448 glyphs: Vec<ShapedGlyph>,
449 upem: u16,
450 size_pt: f32,
451) -> WordSubRun {
452 let upem_f = f32::from(upem);
453 let advance_pt: f32 = glyphs
454 .iter()
455 .map(|g| advance_units_to_pt(g.advance_units, size_pt, upem_f))
456 .sum();
457 WordSubRun {
458 font,
459 text,
460 glyphs,
461 advance_pt,
462 }
463}
464
465fn embedded_upem(id: EmbeddedFontId) -> f32 {
467 f32::from(id.data().units_per_em)
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn adjacent_fallback_clusters_merge_and_rebase() {
476 let runs = shape_with_fallback(
477 Font::Embedded(EmbeddedFontId::Regular),
478 &[EmbeddedFontId::Math],
479 11.0,
480 "office⨌⨌office",
481 );
482 assert_eq!(runs.len(), 3);
483 assert_eq!(
484 runs.iter().map(|run| run.text.as_str()).collect::<Vec<_>>(),
485 ["office", "⨌⨌", "office"]
486 );
487 assert_eq!(runs[1].font, Font::Embedded(EmbeddedFontId::Math));
488 let expected = shape_with_fallback(Font::Embedded(EmbeddedFontId::Math), &[], 11.0, "⨌⨌");
489 assert_eq!(runs[1].glyphs, expected[0].glyphs);
490 assert_eq!(runs[1].advance_pt, expected[0].advance_pt);
491 assert_eq!(runs[0].glyphs, runs[2].glyphs);
492 }
493
494 #[test]
495 fn unresolved_fallback_clusters_merge_with_original_positioning() {
496 let primary = Font::Embedded(EmbeddedFontId::Regular);
497 let text = "officeé🎉office🎉".repeat(32);
498 let expected = shape_with_fallback(primary, &[], 11.0, &text);
499 let actual = shape_with_fallback(primary, &[EmbeddedFontId::Math], 11.0, &text);
500 assert_eq!(actual.len(), 1);
501 assert_eq!(actual[0].font, expected[0].font);
502 assert_eq!(actual[0].text, expected[0].text);
503 assert_eq!(actual[0].glyphs, expected[0].glyphs);
504 assert_eq!(actual[0].advance_pt, expected[0].advance_pt);
505 }
506
507 #[test]
508 #[allow(clippy::expect_used, reason = "test requires nonempty cluster groups")]
509 fn cluster_views_borrow_glyphs_and_preserve_positioning() {
510 let glyph = ShapedGlyph {
511 gid: 1,
512 cluster: 0,
513 advance_units: -17,
514 x_offset_units: 23,
515 y_offset_units: -9,
516 };
517 let glyphs = [
518 glyph,
519 ShapedGlyph { gid: 2, ..glyph },
520 ShapedGlyph {
521 gid: 3,
522 cluster: 3,
523 ..glyph
524 },
525 ];
526 let mut clusters = glyph_clusters(&glyphs, "ffié".len());
527 let first = clusters.next().expect("first cluster");
528 assert_eq!(first.byte_range, 0..3);
529 assert!(std::ptr::eq(first.glyphs.as_ptr(), glyphs.as_ptr()));
530 assert_eq!(first.glyphs, &glyphs[..2]);
531 let last = clusters.next().expect("last cluster");
532 assert_eq!(last.byte_range, 3..5);
533 assert_eq!(last.glyphs, &glyphs[2..]);
534 assert!(clusters.next().is_none());
535 assert!(clusters.next().is_none());
536 assert!(glyph_clusters(&[], 4).next().is_none());
537 }
538
539 #[test]
540 #[allow(
541 clippy::expect_used,
542 reason = "test requires valid shaped source ranges"
543 )]
544 fn cluster_views_use_normalized_subrun_text() {
545 let runs = shape_with_fallback(
546 Font::Embedded(EmbeddedFontId::Regular),
547 &[EmbeddedFontId::Math],
548 11.0,
549 "e\u{301}office⨌",
550 );
551 assert!(
552 runs.iter()
553 .any(|run| run.font == Font::Embedded(EmbeddedFontId::Math))
554 );
555 let mut text = String::new();
556 let mut saw_ligature = false;
557 for run in &runs {
558 for cluster in glyph_clusters(&run.glyphs, run.text.len()) {
559 let part = run
560 .text
561 .get(cluster.byte_range)
562 .expect("valid source range");
563 saw_ligature |= part == "ffi";
564 text.push_str(part);
565 }
566 }
567 assert_eq!(text, "éoffice⨌");
568 assert!(saw_ligature);
569 }
570
571 #[test]
572 fn cluster_views_leave_external_range_validation_to_caller() {
573 let glyph = ShapedGlyph {
574 gid: 1,
575 cluster: 1,
576 advance_units: 0,
577 x_offset_units: 0,
578 y_offset_units: 0,
579 };
580 for offsets in [[1, 3], [3, 0], [0, 9]] {
581 let glyphs = offsets.map(|cluster| ShapedGlyph { cluster, ..glyph });
582 let clusters: Vec<_> = glyph_clusters(&glyphs, "éx".len()).collect();
583 assert_eq!(clusters.len(), 2);
584 assert!(
585 clusters
586 .iter()
587 .any(|cluster| "éx".get(cluster.byte_range.clone()).is_none())
588 );
589 }
590 }
591
592 use crate::Base14Font;
593
594 #[test]
595 #[allow(clippy::expect_used, reason = "test setup and required split results")]
596 fn split_runs_preserves_untouched_geometry() {
597 let font = Font::Embedded(EmbeddedFontId::Regular);
598 let mut runs = shape_with_fallback(font, &[EmbeddedFontId::Math], 11.0, "a⨌b");
599 assert!(runs.len() >= 3);
600 runs[0].advance_pt += 0.25;
603 runs[0].glyphs[0].x_offset_units += 1;
604 let offset = runs[0].text.len();
605 let (left, right) =
606 split_runs_at(&runs, offset, 11.0, &[EmbeddedFontId::Math]).expect("run boundary");
607 let joined: Vec<_> = left.into_iter().chain(right).collect();
608 assert_eq!(joined.len(), runs.len());
609 for (actual, expected) in joined.iter().zip(&runs) {
610 assert_eq!(actual.font, expected.font);
611 assert_eq!(actual.text, expected.text);
612 assert_eq!(actual.glyphs, expected.glyphs);
613 assert_eq!(actual.advance_pt, expected.advance_pt);
614 }
615 for offset in [0, "a⨌b".len()] {
616 let (left, right) = split_runs_at(&runs, offset, 11.0, &[]).expect("end boundary");
617 assert_eq!(left.len() + right.len(), runs.len());
618 assert_eq!(left.is_empty(), offset == 0);
619 assert_eq!(right.is_empty(), offset != 0);
620 }
621 }
622
623 #[test]
624 #[allow(clippy::expect_used, reason = "test setup and required split results")]
625 fn split_runs_reshapes_a_cut_ligature() {
626 let font = Font::Embedded(EmbeddedFontId::Regular);
627 let runs = shape_with_fallback(font, &[], 12.0, "office");
628 assert!(runs.iter().map(|run| run.glyphs.len()).sum::<usize>() < 6);
629 let (left, right) = split_runs_at(&runs, 2, 12.0, &[]).expect("inside ffi ligature");
630 for (actual, text) in [(&left, "of"), (&right, "fice")] {
631 let expected = shape_with_fallback(font, &[], 12.0, text);
632 assert_eq!(actual.len(), expected.len());
633 for (actual, expected) in actual.iter().zip(&expected) {
634 assert_eq!(actual.text, expected.text);
635 assert_eq!(actual.glyphs, expected.glyphs);
636 assert_eq!(actual.advance_pt, expected.advance_pt);
637 }
638 }
639 }
640
641 #[test]
642 #[allow(clippy::expect_used, reason = "required split results")]
643 fn split_runs_checks_offsets_in_normalized_text() {
644 let runs = shape_with_fallback(
645 Font::Embedded(EmbeddedFontId::Regular),
646 &[],
647 12.0,
648 "e\u{301}x",
649 );
650 assert_eq!(runs[0].text, "éx");
651 assert!(split_runs_at(&runs, 1, 12.0, &[]).is_none());
652 assert!(split_runs_at(&runs, 4, 12.0, &[]).is_none());
653 assert!(split_runs_at(&runs, usize::MAX, 12.0, &[]).is_none());
654 let (left, right) = split_runs_at(&runs, 2, 12.0, &[]).expect("normalized boundary");
655 assert_eq!(left[0].text, "é");
656 assert_eq!(right[0].text, "x");
657 let (left, right) = split_runs_at(&[], 0, 12.0, &[]).expect("empty boundary");
658 assert!(left.is_empty() && right.is_empty());
659 assert!(split_runs_at(&[], 1, 12.0, &[]).is_none());
660 }
661
662 #[test]
663 fn embedded_shape_is_empty_for_empty_string() {
664 let ef = EmbeddedFontId::Regular.data();
665 let glyphs = shape(ef, "");
666 assert!(glyphs.is_empty());
667 }
668
669 #[test]
670 fn embedded_shape_returns_clusters_in_byte_order() {
671 let ef = EmbeddedFontId::Regular.data();
672 let glyphs = shape(ef, "Привет");
673 assert!(!glyphs.is_empty());
674 let mut prev: u32 = 0;
677 for g in &glyphs {
678 assert!(
679 g.cluster >= prev,
680 "cluster regression: {prev} -> {}",
681 g.cluster
682 );
683 prev = g.cluster;
684 }
685 }
686
687 #[test]
688 fn embedded_shape_preserves_gpos_kerning() {
689 let ef = EmbeddedFontId::Regular.data();
690 let glyphs = shape(ef, "AV");
691 assert!(!glyphs.is_empty());
692 let nominal: i32 = glyphs
693 .iter()
694 .map(|g| i32::from(ef.advance_units(g.gid)))
695 .sum();
696 let shaped: i32 = glyphs.iter().map(|g| g.advance_units).sum();
697
698 assert!(
699 shaped < nominal,
700 "expected AV kerning to tighten advance: shaped={shaped} nominal={nominal}"
701 );
702 }
703
704 #[test]
705 fn embedded_shape_preserves_combining_mark_offsets() {
706 let ef = EmbeddedFontId::Regular.data();
707 let glyphs = shape(ef, "q\u{0302}\u{0301}");
708 assert!(
709 glyphs.len() >= 3,
710 "expected base 'q' + 2 combining marks, got {glyphs:?}"
711 );
712 assert!(
713 glyphs[1..]
714 .iter()
715 .any(|g| g.x_offset_units != 0 || g.y_offset_units != 0),
716 "expected at least one combining mark offset, got {glyphs:?}"
717 );
718 }
719
720 #[test]
721 fn shape_text_normalizes_decomposed_romanian() {
722 let font = Font::Embedded(EmbeddedFontId::Regular);
723 let decomposed = shape_text(font, 12.0, "S\u{0326}");
724 let precomposed = shape_text(font, 12.0, "\u{0218}");
725
726 let decomposed_gids: Vec<u16> = decomposed.glyphs.iter().map(|g| g.gid).collect();
727 let precomposed_gids: Vec<u16> = precomposed.glyphs.iter().map(|g| g.gid).collect();
728 assert_eq!(decomposed_gids, precomposed_gids);
729 assert!((decomposed.advance_pt - precomposed.advance_pt).abs() < f32::EPSILON);
730 }
731
732 #[test]
733 fn embedded_fi_ligature_collapses_glyphs() {
734 let ef = EmbeddedFontId::Regular.data();
741 let fi = shape(ef, "fi");
742 let f = shape(ef, "f");
743 let i = shape(ef, "i");
744 assert_eq!(fi.len(), 1, "expected fi ligature, got glyphs {fi:?}");
745 assert_ne!(fi[0].gid, f[0].gid);
746 assert_ne!(fi[0].gid, i[0].gid);
747 }
748
749 #[test]
750 fn fallback_empty_text_returns_empty() {
751 let primary = Font::Embedded(EmbeddedFontId::Regular);
752 let fallbacks = &[EmbeddedFontId::Math];
753 assert!(shape_with_fallback(primary, fallbacks, 11.0, "").is_empty());
754 }
755
756 #[test]
757 fn fallback_pure_primary_returns_single_subrun() {
758 let primary = Font::Embedded(EmbeddedFontId::Regular);
761 let fallbacks = &[EmbeddedFontId::Math];
762 let subs = shape_with_fallback(primary, fallbacks, 11.0, "Hello");
763 assert_eq!(subs.len(), 1, "expected one sub-run, got {}", subs.len());
764 assert_eq!(subs[0].font, primary);
765 assert_eq!(subs[0].text, "Hello");
766 assert!(!subs[0].glyphs.is_empty());
767 assert!(subs[0].glyphs.iter().all(|g| g.gid != 0));
768 assert!(subs[0].advance_pt > 0.0);
769 }
770
771 #[test]
772 fn fallback_shape_normalizes_subrun_text() {
773 let primary = Font::Embedded(EmbeddedFontId::Regular);
774 let fallbacks = &[EmbeddedFontId::Math];
775 let subs = shape_with_fallback(primary, fallbacks, 11.0, "S\u{0326}");
776
777 assert_eq!(subs.len(), 1);
778 assert_eq!(subs[0].text, "\u{0218}");
779 }
780
781 #[test]
782 fn fallback_mixed_latin_and_math_produces_alternating_subruns() {
783 let primary = Font::Embedded(EmbeddedFontId::Regular);
788 let fallbacks = &[EmbeddedFontId::Math];
789 let subs = shape_with_fallback(primary, fallbacks, 11.0, "a\u{2264}b");
790 assert_eq!(subs.len(), 3, "expected 3 sub-runs, got {subs:?}");
791
792 assert_eq!(subs[0].font, primary);
793 assert_eq!(subs[0].text, "a");
794
795 assert_eq!(subs[1].font, Font::Embedded(EmbeddedFontId::Math));
796 assert_eq!(subs[1].text, "\u{2264}");
797 assert!(
800 subs[1]
801 .glyphs
802 .iter()
803 .all(|g| (g.cluster as usize) < subs[1].text.len()),
804 "math sub-run glyph clusters not rebased: {:?}",
805 subs[1].glyphs,
806 );
807
808 assert_eq!(subs[2].font, primary);
809 assert_eq!(subs[2].text, "b");
810 }
811
812 #[test]
813 fn fallback_all_fail_keeps_primary_notdef() {
814 let primary = Font::Embedded(EmbeddedFontId::Regular);
819 let fallbacks = &[EmbeddedFontId::Math];
820 let subs = shape_with_fallback(primary, fallbacks, 11.0, "\u{1F389}");
821 assert_eq!(subs.len(), 1);
822 assert_eq!(subs[0].font, primary);
823 assert!(
824 subs[0].glyphs.iter().any(|g| g.gid == 0),
825 "expected .notdef for unsupported emoji, got {:?}",
826 subs[0].glyphs,
827 );
828 }
829
830 #[test]
831 fn fallback_base14_primary_returns_empty_glyphs_subrun() {
832 let primary = Font::Base14(Base14Font::Helvetica);
836 let fallbacks = &[EmbeddedFontId::Math];
837 let subs = shape_with_fallback(primary, fallbacks, 11.0, "Hello");
838 assert_eq!(subs.len(), 1);
839 assert_eq!(subs[0].font, primary);
840 assert!(subs[0].glyphs.is_empty());
841 assert!(subs[0].advance_pt > 0.0);
842 }
843
844 #[test]
845 fn fallback_no_fallbacks_configured_returns_single_subrun_even_with_notdef() {
846 let primary = Font::Embedded(EmbeddedFontId::Regular);
850 let subs = shape_with_fallback(primary, &[], 11.0, "\u{2264}");
851 assert_eq!(subs.len(), 1);
852 assert_eq!(subs[0].font, primary);
853 }
854
855 #[test]
856 fn fallback_math_subrun_advance_uses_math_face_upem() {
857 let primary = Font::Embedded(EmbeddedFontId::Regular);
861 let fallbacks = &[EmbeddedFontId::Math];
862 let subs = shape_with_fallback(primary, fallbacks, 11.0, "\u{2264}");
863 assert_eq!(subs.len(), 1);
864 assert_eq!(subs[0].font, Font::Embedded(EmbeddedFontId::Math));
865 assert!(subs[0].advance_pt > 0.0);
866 }
867}