fonts: port complex-text shaping to harfrust 0.14

This commit is contained in:
Marco Arnulfo
2026-10-23 08:44:31 +02:00
parent 1977a493c7
commit 611eb9f5a9
+36 -11
View File
@@ -8,6 +8,7 @@
use std::ops::Range;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use cadcraft_geom::Vec2;
@@ -182,28 +183,31 @@ struct Glyph {
/// Shape characters `r` of `cs` (one direction, one face) into glyphs in display order.
fn shape_run(face: &Face, cs: &[char], r: Range<usize>, rtl: bool) -> Vec<Glyph> {
let run = cs.get(r.clone()).unwrap_or_default();
let Ok(font) = harfrust::FontRef::from_index(&face.bytes, face.index) else { return Vec::new() };
let data = harfrust::ShaperData::new(&font);
let shaper = data.shaper(&font).build();
let mut buf = harfrust::UnicodeBuffer::new();
// Shares the face's bytes instead of copying them.
let bytes: Arc<dyn AsRef<[u8]> + Send + Sync> = face.bytes.clone();
let Some(font) = harfrust::Font::new(bytes, face.index) else { return Vec::new() };
let shaper = harfrust::ShaperFont::new(&font);
let mut buf = harfrust::Buffer::new();
let arabic = run.iter().any(|c| matches!(*c, '\u{0600}'..='\u{08FF}'));
if arabic && !face.has_joining_features() {
for (c, i) in bidi::presentation_forms(run, |p| face.has_glyph(p)) {
buf.add(c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
buf.push(u32::from(c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
}
} else {
for (i, c) in run.iter().enumerate() {
buf.add(*c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
buf.push(u32::from(*c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
}
}
buf.set_direction(if rtl { harfrust::Direction::RightToLeft } else { harfrust::Direction::LeftToRight });
buf.guess_segment_properties();
let shaped = shaper.shape(buf, harfrust::ShapeOptions::new());
let k = 1000.0 / f64::from(shaper.units_per_em().max(1));
shaped
.glyph_infos()
// Without a plan, shaping fails only on an already shaped buffer or an unset direction.
if harfrust::shape(&shaper, &mut buf, harfrust::ShapeOptions::new()).is_err() {
return Vec::new();
}
let k = 1000.0 / f64::from(font.units_per_em().max(1));
buf.glyph_infos()
.iter()
.zip(shaped.glyph_positions())
.zip(buf.glyph_positions())
.map(|(gi, gp)| Glyph {
gid: gi.glyph_id,
cluster: usize::try_from(gi.cluster).unwrap_or(usize::MAX),
@@ -291,6 +295,27 @@ mod tests {
assert!((crate::line_width("قیمت 25 تومان", 1.0, 1.0) - sh.width).abs() < 1e-9);
}
#[test]
fn devanagari_run_is_reordered_with_line_clusters_and_advances_per_1000_em() {
use skrifa::MetadataProvider;
use skrifa::instance::{LocationRef, Size};
let Some(face) = ttf::fallback_for('\u{0915}') else { return };
let Some(ka) = nominal(&face, '\u{0915}') else { return };
// "कि" after two other characters of the line: the vowel sign i is drawn before the
// consonant (Indic reordering, which needs the script the shaper guesses from the text),
// both glyphs carry the cluster of the consonant's index in the line, not in the run.
let cs: Vec<char> = "ab\u{0915}\u{093F}".chars().collect();
let gs = shape_run(&face, &cs, 2..4, false);
let gids: Vec<u32> = gs.iter().map(|g| g.gid).collect();
assert_eq!(gids.len(), 2, "{gids:?}");
assert!(gids.first() != Some(&ka) && gids.last() == Some(&ka), "vowel sign before ka: {gids:?}");
assert!(gs.iter().all(|g| g.cluster == 2), "{:?}", gs.iter().map(|g| g.cluster).collect::<Vec<_>>());
// Advances come out at 1000 units per em, whatever the font's own em.
let hmtx = face.font().and_then(|f| f.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka)));
let adv = gs.last().map(|g| g.advance);
assert!(matches!((adv, hmtx), (Some(a), Some(h)) if (a - f64::from(h)).abs() < 1e-3), "{adv:?} vs {hmtx:?}");
}
#[test]
fn hostile_rtl_lines_never_panic() {
for s in ["\u{202E}%%u\u{05D0}%%", "\u{2067}\u{0644}\u{0627}\u{2069}%%o", "\u{064E}\u{064E}\u{05D0}", "\u{05D0}\u{E000}\u{10FFFF}"] {