mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge pull request #5712 from dotCooCoo/fix/glyph-cluster-order
Glyphs: a character's glyph is the first of its glyphs in its cluster
This commit is contained in:
@@ -1153,9 +1153,11 @@ 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, 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.
|
||||
/// is the first in cluster order that is its default glyph or one of its alternates and not the
|
||||
/// default glyph of another character of the grapheme; else the first that 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,
|
||||
@@ -1176,18 +1178,24 @@ fn own_glyph(
|
||||
_ => {
|
||||
// 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))
|
||||
})
|
||||
// In cluster order: a conjunct's half form comes before the default glyph of its
|
||||
// second consonant, which can be the same character. A glyph drawn for another
|
||||
// character (a vowel sign drawn first) is skipped even when the font lists it as an
|
||||
// alternate of this one.
|
||||
let own = glyphs_of(ch);
|
||||
let mut others: Vec<char> = plain.get(bytes.end..bytes.start + grapheme).unwrap_or_default().chars().filter(|&c| c != ch).collect();
|
||||
others.sort_unstable();
|
||||
others.dedup();
|
||||
let their_defaults: Vec<u32> = others.iter().map(|&c| face.glyph_for(c)).collect();
|
||||
cluster
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|g| own.contains(g) && !their_defaults.contains(g))
|
||||
.or_else(|| cluster.iter().copied().find(|g| own.contains(g)))
|
||||
.or_else(|| {
|
||||
let theirs: Vec<u32> = others.iter().flat_map(|&c| glyphs_of(c)).collect();
|
||||
cluster.iter().copied().find(|g| !theirs.contains(g))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2677,6 +2677,39 @@ fn the_ligature_of_a_consonant_after_its_vowel_sign_is_reported() {
|
||||
assert_eq!(r["gid"], json!(g('N')), "the ligature is drawn for Ka");
|
||||
}
|
||||
|
||||
/// Ka, the halant, Ka again and the vowel sign I are one cluster: the vowel sign drawn first, the
|
||||
/// first Ka's half form, which the font lists as an alternate of Ka, then the second Ka's default
|
||||
/// glyph. The glyph reported for the first Ka is its half form, the first of its glyphs in the
|
||||
/// cluster, not the second Ka's default glyph. The font also lists the vowel sign's glyph as an
|
||||
/// alternate of Ka; drawn for the vowel sign, it is not reported for Ka.
|
||||
#[test]
|
||||
fn the_first_of_a_characters_glyphs_in_its_cluster_is_reported() {
|
||||
use vectorcraft_text::test_fonts::{CONJUNCT_FAMILY, conjunct_font, source_sans_gid};
|
||||
let _fonts = crate::adding_fonts();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
db.add_font(conjunct_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{94D}क\u{93F}";
|
||||
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": text, "size": 20, "font": CONJUNCT_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, 12), (g('k'), 0, 12), (g('K'), 0, 12)], "the vowel sign, the half form, then the second Ka");
|
||||
|
||||
assert_eq!(s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap()["chosen"], json!(g('k')));
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('k')), "the half form is drawn for the first Ka");
|
||||
|
||||
let text = "क\u{93F}";
|
||||
let r = s.execute("text.create", &json!({"x": 100, "y": 200, "text": text, "size": 20, "font": CONJUNCT_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, 6), (g('K'), 0, 6)], "the vowel sign, then Ka");
|
||||
assert_eq!(s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap()["chosen"], json!(g('K')));
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('K')), "Ka's default glyph, not the vowel sign's");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -423,6 +423,61 @@ pub fn half_form_font(half: bool) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`conjunct_font`].
|
||||
pub const CONJUNCT_FAMILY: &str = "Conjuncttest Sans";
|
||||
|
||||
/// A test-only cmap over bundled Source Sans 3: Devanagari Ka uses K, the halant uses a period and
|
||||
/// the vowel sign I (drawn before its syllable) uses i. The `GSUB` table's `dev2` script has an
|
||||
/// `aalt` single substitution of K by k and a `salt` one of K by i, which list k and the vowel
|
||||
/// sign's glyph as alternates of Ka, and a `half` ligature of K and the period into k (Ka's half
|
||||
/// form). Family [`CONJUNCT_FAMILY`]. `None` when it can't be built.
|
||||
pub fn conjunct_font() -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
use write_fonts::tables::gsub::{
|
||||
Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SingleSubstFormat2, SubstitutionLookup, SubstitutionLookupList,
|
||||
};
|
||||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||||
use write_fonts::types::GlyphId16;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mapping = [('क', 'K'), ('\u{094D}', '.'), ('\u{093F}', 'i')];
|
||||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||||
let family = CONJUNCT_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Conjuncttest Sans Regular"), (6, "ConjuncttestSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
let gid = |c| Some(GlyphId16::new(u16::try_from(charmap.map(c)?.to_u32()).ok()?));
|
||||
let ka = gid('K')?;
|
||||
let aalt = SingleSubstFormat2::new(CoverageFormat1::new(vec![ka]).into(), vec![gid('k')?]);
|
||||
let half =
|
||||
LigatureSubstFormat1::new(CoverageFormat1::new(vec![ka]).into(), vec![LigatureSet::new(vec![Ligature::new(gid('k')?, vec![gid('.')?])])]);
|
||||
let salt = SingleSubstFormat2::new(CoverageFormat1::new(vec![ka]).into(), vec![gid('i')?]);
|
||||
// Features sorted by tag, as OpenType requires: `aalt`, `half`, then `salt`.
|
||||
let features = FeatureList::new(vec![
|
||||
FeatureRecord::new(Tag::new(b"aalt"), Feature::new(None, vec![0])),
|
||||
FeatureRecord::new(Tag::new(b"half"), Feature::new(None, vec![1])),
|
||||
FeatureRecord::new(Tag::new(b"salt"), Feature::new(None, vec![2])),
|
||||
]);
|
||||
let lookups = SubstitutionLookupList::new(vec![
|
||||
SubstitutionLookup::Single(Lookup::new(Default::default(), vec![aalt.into()])),
|
||||
SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![half])),
|
||||
SubstitutionLookup::Single(Lookup::new(Default::default(), vec![salt.into()])),
|
||||
]);
|
||||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"dev2"), Script::new(Some(LangSys::new(vec![0, 1, 2])), vec![]))]);
|
||||
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`matra_font`].
|
||||
pub const MATRA_FAMILY: &str = "Matratest Sans";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user