From e6878edac280597e06274fa7dd04d29e96a36bd3 Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Thu, 15 Oct 2026 08:37:40 +0200 Subject: [PATCH] Shaping: Arabic keeps contextual alternates Contextual alternates are part of how many Arabic fonts join (connecting strokes, tatweel and letter variants). InDesign keeps them in Arabic text when Contextual Alternates is off, so `calt` off is now dropped for Arabic runs. --- crates/fonts/src/lib.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index f31f59422..64b21d689 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -195,6 +195,20 @@ pub fn shape_with_context( out } +/// `features` as a run in `script` takes them. Contextual alternates are part of how Arabic joins +/// (connecting strokes, tatweel and letter variants): InDesign keeps them in Arabic text with +/// Contextual Alternates off, so `calt` off is dropped there. +fn script_features(features: &[Feature], script: unicode_script::Script) -> std::borrow::Cow<'_, [Feature]> { + use unicode_script::Script; + let calt = Tag::new(b"calt"); + let off = |f: &Feature| f.tag == calt && f.value == 0; + if script == Script::Arabic && features.iter().any(off) { + features.iter().filter(|f| !off(f)).copied().collect::>().into() + } else { + features.into() + } +} + fn shape_dir( face: &FontFace, text: &str, @@ -205,6 +219,8 @@ fn shape_dir( context: ShapeContext<'_>, ) -> Vec { let mut out = Vec::with_capacity(text.len()); + let features = script_features(features, script); + let features = &features[..]; let shaped = face.hb().map(|hb| { let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build(); let mut buf = UnicodeBuffer::new(); @@ -332,6 +348,16 @@ pub fn first_glyph(face: &FontFace, chars: &[char]) -> u32 { mod tests { use super::*; + #[test] + fn arabic_keeps_contextual_alternates() { + use unicode_script::Script; + let off = feature("-calt").unwrap(); + let liga = feature("-liga").unwrap(); + let tags = |s: Script| script_features(&[off, liga], s).iter().map(|f| (f.tag, f.value)).collect::>(); + assert_eq!(tags(Script::Arabic), vec![(liga.tag, 0)]); + assert_eq!(tags(Script::Latin), vec![(off.tag, 0), (liga.tag, 0)]); + } + #[test] fn arabic_script_runs_do_not_inherit_the_hebrew_shaper() { use unicode_script::Script;