mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 02:30:09 +00:00
Glyphs: a character's glyph is the first of its glyphs in its cluster
Ka, the halant, Ka again and the vowel sign I are shaped as one cluster: the vowel sign, the first Ka's half form, then the second Ka's default glyph. The glyph lookup looked for the character's default glyph across the whole cluster before its alternates, so for the first Ka it found the second Ka's default glyph instead of the half form. It again goes through the cluster in order, and takes the first glyph that is the character's default glyph or one of its alternates and is not the default glyph of another character of the grapheme, so a vowel sign's glyph that the font also lists as an alternate of the consonant is not reported for the consonant.
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