Type: required lam-alef is not an alternate in fonts that put it in liga (#394)

Highlight Alternate Glyphs compares each glyph with required shaping
alone, which turns liga and calt off. Older Arabic fonts (such as the
legacy Arial of the Microsoft core fonts) put the mandatory lam-alef
ligature in liga instead of rlig, so every lam-alef was highlighted.
A lam + alef cluster (any alef form, marks allowed) is now never an
alternate candidate, whatever feature the font uses.

Adds a test font built in code (lam-alef under liga or rlig) and
host-independent regressions. Also two equivalent one-line repairs
strict Clippy needs on main: a nonminimal_bool in the Path Precision
Tools inspect check and is_none() in a gradient panel test.
This commit is contained in:
Don Omondi
2026-10-31 02:52:05 +07:00
committed by bflatastic
parent 0e7e41e786
commit c1971284ce
6 changed files with 104 additions and 7 deletions
+35 -1
View File
@@ -118,7 +118,9 @@ fn arabic_joining_forms_are_not_alternates() {
assert_eq!(alternates(&t), 0);
}
/// #394: the lam-alef ligature is required (`rlig`), so it isn't highlighted either.
/// #394: the lam-alef ligature is required (`rlig`), so it isn't highlighted either. With an
/// older font that files it under `liga` (as the legacy Arial of Microsoft's core fonts does) it
/// is still required: [`lam_alef_in_liga_is_not_an_alternate`] checks that without an OS font.
#[test]
fn lam_alef_is_not_an_alternate() {
// Looking up an installed font loads it, which changes the fonts.
@@ -129,6 +131,38 @@ fn lam_alef_is_not_an_alternate() {
}
}
/// #394: a font whose lam-alef ligature is in `liga` (older Arabic fonts) or `rlig` gets no
/// highlight on it: the ligature is required by the characters, not chosen by an optional
/// feature. Built in code, so it runs whatever fonts the machine has.
#[test]
fn lam_alef_in_liga_is_not_an_alternate() {
use vectorcraft_text::test_fonts::{lam_alef_family, lam_alef_font};
let _adding = crate::tests_fontchange::adding_fonts();
let db = vectorcraft_text::FontDb::global();
for feature in [b"liga", b"rlig"] {
db.add_font(lam_alef_font(feature).unwrap());
let style = vectorcraft_doc::text::CharStyle { size: 36.0, font_family: lam_alef_family(feature).into(), ..Default::default() };
for s in ["\u{0644}\u{0627}", "\u{0644}\u{0623}", "\u{0628}\u{0644}\u{0627}", "\u{0644}\u{064E}\u{0627}"] {
let t = text(s, style.clone());
// The font's ligature glyph stands for the pair.
assert!(glyphs(&t).iter().any(|g| g.1 > 2), "{}: {s} is shaped to one ligature glyph", lam_alef_family(feature));
assert_eq!(alternates(&t), 0, "{}: {s}", lam_alef_family(feature));
}
}
}
/// Only a lam followed by an alef (marks between or after allowed) is the required lam-alef.
#[test]
fn only_lam_alef_clusters_are_the_required_ligature() {
for s in ["\u{0644}\u{0627}", "\u{0644}\u{0622}", "\u{0644}\u{0623}", "\u{0644}\u{0625}", "\u{0644}\u{064E}\u{0627}", "\u{0644}\u{0627}\u{064B}"]
{
assert!(crate::text_geom::is_lam_alef(s), "{s:?}");
}
for s in ["", "\u{0644}", "\u{0627}\u{0644}", "\u{0644}\u{0628}", "\u{0628}\u{0644}\u{0627}", "\u{0644}\u{0627}\u{0627}", "fi", "\u{064E}"] {
assert!(!crate::text_geom::is_lam_alef(s), "{s:?}");
}
}
/// #394: an optional feature the characters turn on (a stylistic set, swash, stylistic or
/// discretionary alternate…) is highlighted on the glyphs it changes, and only on those.
#[test]
+14 -2
View File
@@ -48,7 +48,8 @@ impl TextGeom {
/// The boxes of the glyphs of `t` (the text this geometry is of) that optional OpenType features
/// chose (Preferences › Highlight Alternate Glyphs): candidates the text's required shaping
/// alone doesn't give ([`required_shaping`]). Arabic letters joining, Indic conjuncts and the
/// lam-alef ligature are what the script needs, not alternates. Measured once.
/// lam-alef ligature are what the script needs, not alternates (the lam-alef ligature also when
/// a font files it under `liga`: [`is_lam_alef`]). Measured once.
pub(crate) fn alternate_glyphs(&self, t: &TextObject) -> &BezPath {
self.alternate_glyphs.get_or_init(|| {
let mut out = BezPath::new();
@@ -84,6 +85,17 @@ pub(crate) fn is_opentype_alternate(nominal_gid: u32, shaped_gid: u32, cluster_c
shaped_gid != 0 && (cluster_chars > 1 || shaped_gid != nominal_gid)
}
/// Whether `cluster` is a lam followed by an alef (plain, with madda, or with hamza above or below),
/// with any Arabic marks between or after them: the lam-alef ligature, which the Unicode
/// Standard's Arabic chapter treats as obligatory, not a stylistic choice. Fonts made before
/// `rlig` was common put it in `liga`, which [`required_shaping`] turns off, so it is required
/// by its characters whatever feature the font files it under.
pub(crate) fn is_lam_alef(cluster: &str) -> bool {
let is_mark = |c: &char| matches!(c, '\u{0610}'..='\u{061A}' | '\u{064B}'..='\u{065F}' | '\u{0670}' | '\u{06D6}'..='\u{06DC}' | '\u{06DF}'..='\u{06E4}' | '\u{06E7}' | '\u{06E8}' | '\u{06EA}'..='\u{06ED}');
let mut letters = cluster.chars().filter(|c| !is_mark(c));
matches!((letters.next(), letters.next(), letters.next()), (Some('\u{0644}'), Some('\u{0622}' | '\u{0623}' | '\u{0625}' | '\u{0627}'), None))
}
/// A glyph [`is_opentype_alternate`] marks, in its cluster of the text (`byte`, `len`), with its
/// box.
#[derive(Debug)]
@@ -181,7 +193,7 @@ pub(crate) fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeo
let cluster_chars = cluster.chars().count();
let Some(first) = cluster.chars().next() else { continue };
let nominal = db.face_by_id(g.font_id).map(|f| f.glyph_for(first)).unwrap_or(0);
if is_opentype_alternate(nominal, g.gid, cluster_chars) {
if is_opentype_alternate(nominal, g.gid, cluster_chars) && !is_lam_alef(cluster) {
alternate_candidates.push(AlternateCandidate { byte: g.byte, len: g.len, font_id: g.font_id, gid: g.gid, cell });
}
}