Merge pull request #5701 from dotCooCoo/fix/glyph-cluster-ligature

Glyphs: a consonant's ligature in a cluster with its vowel sign is its glyph
This commit is contained in:
BFlat
2026-11-05 05:32:35 +07:00
committed by GitHub
2 changed files with 39 additions and 3 deletions
+17 -3
View File
@@ -1153,7 +1153,9 @@ fn glyph_target(t: &TextObject, p: &Value, c: &str) -> Result<(char, std::ops::R
/// text; `None` when the character is not laid out (overflowing text). A character that starts a
/// longer grapheme (a letter and its combining marks) is drawn in the grapheme's cluster. Of a
/// cluster of several glyphs (a mark, a vowel sign drawn before the letter), the character's glyph
/// is its default glyph or one of its alternates; `Some(None)` when the cluster holds neither.
/// is its default glyph or one of its alternates, else the first glyph that is not the default glyph
/// or an alternate of another character of the grapheme (a ligature of the letter and a mark);
/// `Some(None)` when the cluster holds none of these.
fn own_glyph(
plain: &str,
lay: &vectorcraft_text::TextLayout,
@@ -1172,8 +1174,20 @@ fn own_glyph(
Some(match cluster[..] {
[one] => Some(one),
_ => {
let (default, listed) = (face.glyph_for(ch), face.alternates(ch));
cluster.into_iter().find(|&g| g == default || listed.iter().any(|a| a.gid == g))
// The glyphs of each character are looked up once: a grapheme can repeat a mark.
let glyphs_of = |c: char| face.alternates(c).iter().map(|a| a.gid).chain([face.glyph_for(c)]).collect::<Vec<u32>>();
let default = face.glyph_for(ch);
let listed = || {
let own = glyphs_of(ch);
cluster.iter().copied().find(|g| own.contains(g))
};
cluster.iter().copied().find(|&g| g == default).or_else(listed).or_else(|| {
let mut others: Vec<char> = plain.get(bytes.end..bytes.start + grapheme).unwrap_or_default().chars().collect();
others.sort_unstable();
others.dedup();
let theirs: Vec<u32> = others.into_iter().flat_map(glyphs_of).collect();
cluster.iter().copied().find(|g| !theirs.contains(g))
})
}
})
}
+22
View File
@@ -2589,6 +2589,28 @@ fn the_glyph_of_a_consonant_after_its_vowel_sign_is_reported() {
assert_eq!(s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap()["chosen"], json!(g('a')));
}
/// Ka with a nukta and the vowel sign I is one cluster: the vowel sign drawn first, then the
/// ligature of Ka and the nukta, which is neither Ka's default glyph nor one of its alternates. The
/// glyph reported for Ka is the ligature, not the vowel sign.
#[test]
fn the_ligature_of_a_consonant_after_its_vowel_sign_is_reported() {
use vectorcraft_text::test_fonts::{MATRA_FAMILY, matra_font, source_sans_gid};
let _fonts = crate::adding_fonts();
let db = vectorcraft_text::FontDb::global();
db.add_font(matra_font().expect("the test font builds"));
let g = |c| u32::from(source_sans_gid(c).expect("a Source Sans glyph"));
let mut s = session();
let text = "क\u{93C}\u{93F}";
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": text, "size": 20, "font": MATRA_FAMILY})).unwrap();
let id = NodeId(r["id"].as_u64().unwrap());
let gids: Vec<_> = vectorcraft_text::layout(db, &obj(&s, id)).glyphs.iter().map(|gl| (gl.gid, gl.byte, gl.len)).collect();
assert_eq!(gids, [(g('i'), 0, 9), (g('N'), 0, 9)], "the vowel sign is drawn first, then the ligature");
assert_eq!(s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap()["chosen"], json!(g('N')));
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": null})).unwrap();
assert_eq!(r["gid"], json!(g('N')), "the ligature is drawn for Ka");
}
/// A character that overflows its area is shaped with its grapheme: the nukta after Ka makes the
/// font's `nukt` ligature, which `salt` does not change, so `gid: null` reports the ligature as it
/// does for the laid-out text.