diff --git a/crates/fonts/src/complex.rs b/crates/fonts/src/complex.rs index 6193f3f8..1d76f813 100644 --- a/crates/fonts/src/complex.rs +++ b/crates/fonts/src/complex.rs @@ -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, rtl: bool) -> Vec { 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 + 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 = "ab\u{0915}\u{093F}".chars().collect(); + let gs = shape_run(&face, &cs, 2..4, false); + let gids: Vec = 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::>()); + // 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}"] {