1use harfrust::{Direction, Feature, FontRef, ShapeOptions, Tag, UnicodeBuffer};
2
3use crate::font::registry::FontRegistry;
4use crate::font::resolve::ResolvedFont;
5use crate::shaping::run::{ShapedGlyph, ShapedRun};
6use crate::types::FontFeature;
7
8pub fn to_harfrust_features(features: &[FontFeature]) -> Vec<Feature> {
12 features
13 .iter()
14 .map(|f| Feature::new(Tag::new(&f.tag), f.value, ..))
15 .collect()
16}
17
18fn units_per_em(bytes: &[u8], face_index: u32) -> Option<u16> {
26 let font_ref = swash::FontRef::from_index(bytes, face_index as usize)?;
27 let upem = font_ref.metrics(&[]).units_per_em;
28 if upem == 0 { None } else { Some(upem) }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum TextDirection {
34 #[default]
36 Auto,
37 LeftToRight,
38 RightToLeft,
39}
40
41pub fn shape_text(
53 registry: &FontRegistry,
54 resolved: &ResolvedFont,
55 text: &str,
56 text_offset: usize,
57) -> Option<ShapedRun> {
58 shape_text_with_fallback(
59 registry,
60 resolved,
61 text,
62 text_offset,
63 TextDirection::Auto,
64 &[],
65 )
66}
67
68pub fn shape_text_with_fallback(
74 registry: &FontRegistry,
75 resolved: &ResolvedFont,
76 text: &str,
77 text_offset: usize,
78 direction: TextDirection,
79 features: &[Feature],
80) -> Option<ShapedRun> {
81 let mut run = shape_text_directed(registry, resolved, text, text_offset, direction, features)?;
82
83 if run.glyphs.iter().any(|g| g.glyph_id == 0) && !text.is_empty() {
85 apply_glyph_fallback(registry, resolved, text, text_offset, features, &mut run);
86 }
87
88 Some(run)
89}
90
91fn apply_glyph_fallback(
109 registry: &FontRegistry,
110 primary: &ResolvedFont,
111 text: &str,
112 text_offset: usize,
113 features: &[Feature],
114 run: &mut ShapedRun,
115) {
116 use crate::font::resolve::find_fallback_font;
117
118 let mut spans: Vec<std::ops::Range<usize>> = Vec::new();
120 let mut i = 0;
121 while i < run.glyphs.len() {
122 if run.glyphs[i].glyph_id == 0 {
123 let start = i;
124 while i < run.glyphs.len() && run.glyphs[i].glyph_id == 0 {
125 i += 1;
126 }
127 spans.push(start..i);
128 } else {
129 i += 1;
130 }
131 }
132 if spans.is_empty() {
133 return;
134 }
135
136 for span in spans.into_iter().rev() {
138 let Some((byte_start, byte_end)) = notdef_char_range(&run.glyphs, &span, text) else {
139 continue;
140 };
141 let Some(slice) = text.get(byte_start..byte_end) else {
142 continue;
143 };
144 let Some(first_char) = slice.chars().next() else {
145 continue;
146 };
147
148 let Some(fallback_id) = find_fallback_font(registry, first_char, primary.font_face_id)
149 else {
150 continue; };
152 let Some(fallback_entry) = registry.get(fallback_id) else {
153 continue;
154 };
155
156 let fallback_resolved = ResolvedFont {
157 font_face_id: fallback_id,
158 size_px: primary.size_px,
159 face_index: fallback_entry.face_index,
160 swash_cache_key: fallback_entry.swash_cache_key,
161 scale_factor: primary.scale_factor,
162 weight: primary.weight,
163 };
164
165 let Some(fallback_run) = shape_text_directed(
166 registry,
167 &fallback_resolved,
168 slice,
169 text_offset + byte_start,
170 run.direction,
171 features,
172 ) else {
173 continue;
174 };
175 if fallback_run.glyphs.is_empty() {
176 continue;
177 }
178
179 let replacement: Vec<ShapedGlyph> = fallback_run
182 .glyphs
183 .into_iter()
184 .map(|mut g| {
185 g.cluster += byte_start as u32;
186 g.font_face_id = fallback_id;
187 g
188 })
189 .collect();
190
191 run.glyphs.splice(span, replacement);
192 }
193
194 run.advance_width = run.glyphs.iter().map(|g| g.x_advance).sum();
195}
196
197fn notdef_char_range(
207 glyphs: &[ShapedGlyph],
208 span: &std::ops::Range<usize>,
209 text: &str,
210) -> Option<(usize, usize)> {
211 let inside = glyphs.get(span.clone())?;
212 let start = inside.iter().map(|g| g.cluster as usize).min()?;
213 let last = inside.iter().map(|g| g.cluster as usize).max()?;
214
215 let end = glyphs
216 .iter()
217 .enumerate()
218 .filter(|(i, _)| !span.contains(i))
219 .map(|(_, g)| g.cluster as usize)
220 .filter(|&c| c > last)
221 .min()
222 .unwrap_or(text.len());
223
224 if start >= end || !text.is_char_boundary(start) || !text.is_char_boundary(end) {
225 return None;
226 }
227 Some((start, end))
228}
229
230pub fn shape_text_directed(
232 registry: &FontRegistry,
233 resolved: &ResolvedFont,
234 text: &str,
235 text_offset: usize,
236 direction: TextDirection,
237 features: &[Feature],
238) -> Option<ShapedRun> {
239 let entry = registry.get(resolved.font_face_id)?;
240 let font = FontRef::from_index(entry.bytes(), entry.face_index).ok()?;
241
242 let upem = units_per_em(entry.bytes(), entry.face_index).unwrap_or(0) as f32;
243 if upem == 0.0 {
244 return None;
245 }
246 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
249 let physical_size = resolved.size_px * sf;
250 let physical_scale = physical_size / upem;
251 let inv_sf = 1.0 / sf;
252
253 let mut buffer = UnicodeBuffer::new();
254 buffer.push_str(text);
255 match direction {
256 TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight),
257 TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft),
258 TextDirection::Auto => {}
259 }
260 buffer.guess_segment_properties();
272
273 let resolved_direction = if buffer.direction() == Direction::RightToLeft {
276 TextDirection::RightToLeft
277 } else {
278 TextDirection::LeftToRight
279 };
280
281 let shaper_data = entry.shaper_data(&font);
285 let shaper = shaper_data.shaper(&font).build();
286 let glyph_buffer = shaper.shape(buffer, ShapeOptions::new().features(features));
287
288 let infos = glyph_buffer.glyph_infos();
289 let positions = glyph_buffer.glyph_positions();
290
291 let mut glyphs = Vec::with_capacity(infos.len());
292 let mut total_advance = 0.0f32;
293
294 for (info, pos) in infos.iter().zip(positions.iter()) {
295 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
296 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
297 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
298 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
299
300 glyphs.push(ShapedGlyph {
301 glyph_id: info.glyph_id as u16,
302 cluster: info.cluster,
303 x_advance,
304 y_advance,
305 x_offset,
306 y_offset,
307 font_face_id: resolved.font_face_id,
308 });
309
310 total_advance += x_advance;
311 }
312
313 Some(ShapedRun {
314 font_face_id: resolved.font_face_id,
315 size_px: resolved.size_px,
316 weight: resolved.weight,
317 glyphs,
318 advance_width: total_advance,
319 text_range: text_offset..text_offset + text.len(),
320 direction: resolved_direction,
321 bidi_level: if resolved_direction == TextDirection::RightToLeft {
322 1
323 } else {
324 0
325 },
326 underline_style: crate::types::UnderlineStyle::None,
327 overline: false,
328 strikeout: false,
329 is_link: false,
330 foreground_color: None,
331 underline_color: None,
332 background_color: None,
333 anchor_href: None,
334 tooltip: None,
335 vertical_alignment: crate::types::VerticalAlignment::Normal,
336 image_name: None,
337 image_height: 0.0,
338 })
339}
340
341pub fn shape_text_with_buffer(
343 registry: &FontRegistry,
344 resolved: &ResolvedFont,
345 text: &str,
346 text_offset: usize,
347 buffer: UnicodeBuffer,
348 features: &[Feature],
349) -> Option<(ShapedRun, UnicodeBuffer)> {
350 let entry = registry.get(resolved.font_face_id)?;
351 let font = FontRef::from_index(entry.bytes(), entry.face_index).ok()?;
352
353 let upem = units_per_em(entry.bytes(), entry.face_index).unwrap_or(0) as f32;
354 if upem == 0.0 {
355 return None;
356 }
357 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
358 let physical_size = resolved.size_px * sf;
359 let physical_scale = physical_size / upem;
360 let inv_sf = 1.0 / sf;
361
362 let mut buffer = buffer;
363 buffer.push_str(text);
364 buffer.guess_segment_properties();
367
368 let resolved_direction = if buffer.direction() == Direction::RightToLeft {
369 TextDirection::RightToLeft
370 } else {
371 TextDirection::LeftToRight
372 };
373
374 let shaper_data = entry.shaper_data(&font);
375 let shaper = shaper_data.shaper(&font).build();
376 let glyph_buffer = shaper.shape(buffer, ShapeOptions::new().features(features));
377
378 let infos = glyph_buffer.glyph_infos();
379 let positions = glyph_buffer.glyph_positions();
380
381 let mut glyphs = Vec::with_capacity(infos.len());
382 let mut total_advance = 0.0f32;
383
384 for (info, pos) in infos.iter().zip(positions.iter()) {
385 let x_advance = pos.x_advance as f32 * physical_scale * inv_sf;
386 let y_advance = pos.y_advance as f32 * physical_scale * inv_sf;
387 let x_offset = pos.x_offset as f32 * physical_scale * inv_sf;
388 let y_offset = pos.y_offset as f32 * physical_scale * inv_sf;
389
390 glyphs.push(ShapedGlyph {
391 glyph_id: info.glyph_id as u16,
392 cluster: info.cluster,
393 x_advance,
394 y_advance,
395 x_offset,
396 y_offset,
397 font_face_id: resolved.font_face_id,
398 });
399
400 total_advance += x_advance;
401 }
402
403 let run = ShapedRun {
404 font_face_id: resolved.font_face_id,
405 size_px: resolved.size_px,
406 weight: resolved.weight,
407 glyphs,
408 advance_width: total_advance,
409 text_range: text_offset..text_offset + text.len(),
410 direction: resolved_direction,
411 bidi_level: if resolved_direction == TextDirection::RightToLeft {
412 1
413 } else {
414 0
415 },
416 underline_style: crate::types::UnderlineStyle::None,
417 overline: false,
418 strikeout: false,
419 is_link: false,
420 foreground_color: None,
421 underline_color: None,
422 background_color: None,
423 anchor_href: None,
424 tooltip: None,
425 vertical_alignment: crate::types::VerticalAlignment::Normal,
426 image_name: None,
427 image_height: 0.0,
428 };
429
430 let recycled = glyph_buffer.clear();
432 Some((run, recycled))
433}
434
435pub struct FontMetricsPx {
436 pub ascent: f32,
437 pub descent: f32,
438 pub leading: f32,
439 pub underline_offset: f32,
440 pub strikeout_offset: f32,
441 pub stroke_size: f32,
442}
443
444pub fn font_metrics_px(registry: &FontRegistry, resolved: &ResolvedFont) -> Option<FontMetricsPx> {
449 let entry = registry.get(resolved.font_face_id)?;
450 let font_ref = swash::FontRef::from_index(entry.bytes(), entry.face_index as usize)?;
451 let sf = resolved.scale_factor.max(f32::MIN_POSITIVE);
452 let physical_size = resolved.size_px * sf;
453 let metrics = font_ref.metrics(&[]).scale(physical_size);
454 let inv_sf = 1.0 / sf;
455
456 Some(FontMetricsPx {
457 ascent: metrics.ascent * inv_sf,
458 descent: metrics.descent * inv_sf,
459 leading: metrics.leading * inv_sf,
460 underline_offset: metrics.underline_offset * inv_sf,
461 strikeout_offset: metrics.strikeout_offset * inv_sf,
462 stroke_size: metrics.stroke_size * inv_sf,
463 })
464}
465
466pub struct BidiRun {
468 pub byte_range: std::ops::Range<usize>,
469 pub direction: TextDirection,
470 pub visual_order: usize,
472 pub level: u8,
480}
481
482pub struct BidiParagraph {
492 pub runs: Vec<BidiRun>,
494 pub para_level: u8,
497}
498
499impl BidiParagraph {
500 pub fn base_direction(&self) -> TextDirection {
502 if self.para_level % 2 == 1 {
503 TextDirection::RightToLeft
504 } else {
505 TextDirection::LeftToRight
506 }
507 }
508}
509
510fn base_para_level(base: TextDirection) -> Option<unicode_bidi::Level> {
519 match base {
520 TextDirection::Auto => None,
521 TextDirection::LeftToRight => Some(unicode_bidi::Level::ltr()),
522 TextDirection::RightToLeft => Some(unicode_bidi::Level::rtl()),
523 }
524}
525
526pub fn analyze_paragraph(text: &str, base: TextDirection) -> BidiParagraph {
532 use unicode_bidi::BidiInfo;
533
534 if text.is_empty() {
535 return BidiParagraph {
536 runs: Vec::new(),
537 para_level: base_para_level(base).map_or(0, |l| l.number()),
538 };
539 }
540
541 let bidi_info = BidiInfo::new(text, base_para_level(base));
542
543 let para_level = bidi_info
547 .paragraphs
548 .first()
549 .map(|p| p.level.number())
550 .or_else(|| base_para_level(base).map(|l| l.number()))
551 .unwrap_or(0);
552
553 let mut starts: Vec<(usize, u8)> = Vec::new();
557 for (idx, _) in text.char_indices() {
558 let level = bidi_info.levels[idx].number();
559 if starts.last().map(|&(_, l)| l) != Some(level) {
560 starts.push((idx, level));
561 }
562 }
563
564 let ends = starts
566 .iter()
567 .skip(1)
568 .map(|&(start, _)| start)
569 .chain(std::iter::once(text.len()));
570
571 let runs: Vec<BidiRun> = starts
572 .iter()
573 .zip(ends)
574 .map(|(&(start, level), end)| BidiRun {
575 byte_range: start..end,
576 direction: if level % 2 == 1 {
577 TextDirection::RightToLeft
578 } else {
579 TextDirection::LeftToRight
580 },
581 visual_order: 0,
587 level,
588 })
589 .collect();
590
591 BidiParagraph { runs, para_level }
592}
593
594pub fn visual_order(levels: &[u8]) -> Vec<usize> {
601 let mut order: Vec<usize> = (0..levels.len()).collect();
602 let Some(&max) = levels.iter().max() else {
603 return order;
604 };
605 let Some(min_odd) = levels.iter().copied().filter(|l| l % 2 == 1).min() else {
607 return order;
608 };
609
610 let mut level = max;
611 while level >= min_odd {
612 let mut i = 0;
613 while i < order.len() {
614 if levels[order[i]] >= level {
615 let start = i;
616 while i < order.len() && levels[order[i]] >= level {
617 i += 1;
618 }
619 order[start..i].reverse();
620 } else {
621 i += 1;
622 }
623 }
624 level -= 1;
626 }
627 order
628}
629
630pub fn bidi_runs(text: &str) -> Vec<BidiRun> {
638 use unicode_bidi::BidiInfo;
639
640 if text.is_empty() {
641 return Vec::new();
642 }
643
644 let bidi_info = BidiInfo::new(text, None);
645 let mut runs = Vec::new();
646
647 for para in &bidi_info.paragraphs {
648 let (levels, level_runs) = bidi_info.visual_runs(para, para.range.clone());
649 for level_run in level_runs {
650 if level_run.is_empty() {
651 continue;
652 }
653 let level = levels[level_run.start];
654 let direction = if level.is_rtl() {
655 TextDirection::RightToLeft
656 } else {
657 TextDirection::LeftToRight
658 };
659 let visual_order = runs.len();
660 runs.push(BidiRun {
661 byte_range: level_run,
662 direction,
663 visual_order,
664 level: level.number(),
665 });
666 }
667 }
668
669 if runs.is_empty() {
670 runs.push(BidiRun {
671 byte_range: 0..text.len(),
672 direction: TextDirection::LeftToRight,
673 visual_order: 0,
674 level: 0,
675 });
676 }
677
678 runs
679}
680
681#[cfg(test)]
682mod bidi_tests {
683 use super::*;
684
685 const ARABIC: &str = "\u{0643}\u{062A}\u{0628}"; const HEBREW: &str = "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"; #[test]
689 fn rule_l2_leaves_all_ltr_text_alone() {
690 assert_eq!(visual_order(&[0, 0, 0]), vec![0, 1, 2]);
691 assert_eq!(visual_order(&[]), Vec::<usize>::new());
692 }
693
694 #[test]
695 fn rule_l2_reverses_a_run_of_rtl() {
696 assert_eq!(visual_order(&[0, 1, 1, 0]), vec![0, 2, 1, 3]);
699 }
700
701 #[test]
702 fn rule_l2_nests_an_ltr_island_inside_rtl() {
703 let levels = [0, 1, 2, 2, 1, 0];
708 assert_eq!(visual_order(&levels), vec![0, 4, 2, 3, 1, 5]);
709 }
710
711 #[test]
712 fn rule_l2_reverses_the_whole_line_in_an_rtl_paragraph() {
713 assert_eq!(visual_order(&[1, 2, 1]), vec![2, 1, 0]);
716 }
717
718 #[test]
719 fn a_leading_digit_does_not_fool_auto_detection() {
720 let text = "123 \u{0643}\u{062A}\u{0628}";
725 assert_eq!(analyze_paragraph(text, TextDirection::Auto).para_level, 1);
726 }
727
728 #[test]
729 fn an_explicit_base_direction_overrides_first_strong_detection() {
730 let text = "NASA \u{0623}\u{0639}\u{0644}\u{0646}\u{062A}";
735 assert_eq!(
736 analyze_paragraph(text, TextDirection::Auto).para_level,
737 0,
738 "auto-detection is expected to get this one wrong"
739 );
740
741 let forced = analyze_paragraph(text, TextDirection::RightToLeft);
742 assert_eq!(forced.para_level, 1);
743 assert_eq!(forced.base_direction(), TextDirection::RightToLeft);
744
745 let auto = analyze_paragraph(text, TextDirection::Auto);
750 let first_visual = |p: &BidiParagraph| {
751 let levels: Vec<u8> = p.runs.iter().map(|r| r.level).collect();
752 visual_order(&levels).first().map(|&i| p.runs[i].direction)
753 };
754 assert_eq!(first_visual(&auto), Some(TextDirection::LeftToRight));
755 assert_eq!(first_visual(&forced), Some(TextDirection::RightToLeft));
756 }
757
758 #[test]
759 fn pure_arabic_auto_detects_as_rtl() {
760 let para = analyze_paragraph(ARABIC, TextDirection::Auto);
761 assert_eq!(para.base_direction(), TextDirection::RightToLeft);
762 assert_eq!(para.runs.len(), 1);
763 assert_eq!(para.runs[0].direction, TextDirection::RightToLeft);
764 assert_eq!(para.runs[0].byte_range, 0..ARABIC.len());
765 }
766
767 #[test]
768 fn runs_come_back_in_logical_order_and_cover_the_text() {
769 let text = format!("hello {HEBREW} world");
771 let para = analyze_paragraph(&text, TextDirection::Auto);
772
773 assert!(para.runs.len() >= 2, "expected a directional split");
774 assert_eq!(para.runs[0].byte_range.start, 0);
775 assert_eq!(para.runs.last().unwrap().byte_range.end, text.len());
776 for pair in para.runs.windows(2) {
777 assert_eq!(
778 pair[0].byte_range.end, pair[1].byte_range.start,
779 "runs must tile the text with no gap or overlap"
780 );
781 assert!(
782 pair[0].byte_range.start < pair[1].byte_range.start,
783 "runs must be in logical order"
784 );
785 }
786 assert!(
787 para.runs
788 .iter()
789 .any(|r| r.direction == TextDirection::RightToLeft),
790 "the Hebrew span should have produced an RTL run"
791 );
792 }
793
794 #[test]
795 fn run_boundaries_never_split_a_multibyte_character() {
796 let text = format!("a{ARABIC}b{HEBREW}c");
797 for run in analyze_paragraph(&text, TextDirection::Auto).runs {
798 assert!(
799 text.is_char_boundary(run.byte_range.start)
800 && text.is_char_boundary(run.byte_range.end),
801 "run {:?} splits a character in {text:?}",
802 run.byte_range
803 );
804 }
805 }
806
807 #[test]
808 fn rule_l2_over_a_real_paragraph_is_a_permutation() {
809 let text = format!("hello {HEBREW} world {ARABIC} end");
810 let para = analyze_paragraph(&text, TextDirection::Auto);
811 let levels: Vec<u8> = para.runs.iter().map(|r| r.level).collect();
812
813 let mut seen = visual_order(&levels);
814 seen.sort_unstable();
815 assert_eq!(
816 seen,
817 (0..para.runs.len()).collect::<Vec<_>>(),
818 "every run must appear exactly once in the visual order"
819 );
820 }
821
822 #[test]
823 fn empty_text_analyzes_without_panicking() {
824 let para = analyze_paragraph("", TextDirection::RightToLeft);
825 assert!(para.runs.is_empty());
826 assert_eq!(para.para_level, 1);
827 }
828}