mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 03:41:28 +00:00
Merge pull request #5697 from dotCooCoo/fix/glyph-alternates-own-glyph
Glyphs: the Glyphs query reports a consonant's glyph, not its vowel sign's
This commit is contained in:
@@ -1149,6 +1149,35 @@ fn glyph_target(t: &TextObject, p: &Value, c: &str) -> Result<(char, std::ops::R
|
||||
Err(bad(c, format!("no character at byte {start}")))
|
||||
}
|
||||
|
||||
/// The glyph of `face` that `lay` draws for the character `ch` at `bytes` of `plain`, the laid-out
|
||||
/// 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.
|
||||
fn own_glyph(
|
||||
plain: &str,
|
||||
lay: &vectorcraft_text::TextLayout,
|
||||
ch: char,
|
||||
bytes: &std::ops::Range<usize>,
|
||||
face: &vectorcraft_text::FontFace,
|
||||
) -> Option<Option<u32>> {
|
||||
lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len))?;
|
||||
let grapheme = edit::next_cluster(plain, bytes.start).max(bytes.end) - bytes.start;
|
||||
let cluster: Vec<u32> = lay
|
||||
.glyphs
|
||||
.iter()
|
||||
.filter(|g| g.byte == bytes.start && [bytes.len(), grapheme].contains(&g.len) && g.font_id == face.id())
|
||||
.map(|g| g.gid)
|
||||
.collect();
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.setGlyphAlternate";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
@@ -1174,40 +1203,21 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The glyph the character is drawn with when it holds no choice, drawn alone in its face: a
|
||||
// feature of its style (`salt`, a stylistic set) can draw another glyph than the default one.
|
||||
// The default glyph is then kept as a choice, so that it is drawn. A character that is not laid
|
||||
// out (overflowing text) is shaped alone in its style and writing direction. A character that starts a longer
|
||||
// grapheme (a letter and its combining marks) is drawn in the grapheme's cluster, and is shaped
|
||||
// with the grapheme. 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.
|
||||
// out (overflowing text) is shaped alone in its writing direction, with the rest of its grapheme
|
||||
// in the grapheme's own styles.
|
||||
let plain = t.plain_text();
|
||||
let grapheme = plain.get(bytes.start..edit::next_cluster(&plain, bytes.start).max(bytes.end)).map_or_else(|| ch.to_string(), str::to_owned);
|
||||
let listed = face.alternates(ch);
|
||||
let own_glyph = |lay: &vectorcraft_text::TextLayout, at: usize| {
|
||||
let cluster: Vec<u32> = lay
|
||||
.glyphs
|
||||
.iter()
|
||||
.filter(|g| g.byte == at && [bytes.len(), grapheme.len()].contains(&g.len) && g.font_id == face.id())
|
||||
.map(|g| g.gid)
|
||||
.collect();
|
||||
match cluster[..] {
|
||||
[one] => Some(one),
|
||||
_ => cluster.into_iter().find(|&g| g == default || listed.iter().any(|a| a.gid == g)),
|
||||
}
|
||||
};
|
||||
let alone_in_face = |lay: &vectorcraft_text::TextLayout| {
|
||||
let found = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len));
|
||||
found.map(|_| own_glyph(lay, bytes.start))
|
||||
};
|
||||
let unchosen = match holder.as_ref().filter(|_| !styled) {
|
||||
None => None,
|
||||
Some((_, run)) => {
|
||||
let mut probe = t.clone();
|
||||
edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None);
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
alone_in_face(&vectorcraft_text::layout(db, &probe)).unwrap_or_else(|| {
|
||||
let st = CharStyle { glyph_alternate: None, ..run.style.clone() };
|
||||
let mut single = TextObject::point(Point::ZERO, &grapheme, st);
|
||||
own_glyph(&plain, &vectorcraft_text::layout(db, &probe), ch, &bytes, &face).unwrap_or_else(|| {
|
||||
let mut single = TextObject::point(Point::ZERO, &grapheme, run.style.clone());
|
||||
single.runs = edit::slice_runs(&probe.runs, bytes.start, bytes.start + grapheme.len());
|
||||
single.vertical = t.vertical;
|
||||
own_glyph(&vectorcraft_text::layout(db, &single), 0)
|
||||
own_glyph(&grapheme, &vectorcraft_text::layout(db, &single), ch, &(0..bytes.len()), &face).flatten()
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -1230,10 +1240,9 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let shown = || {
|
||||
let mut probe = t.clone();
|
||||
edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = Some(c.clone()));
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe);
|
||||
lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len)).map(|g| g.gid)
|
||||
own_glyph(&plain, &vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe), ch, &bytes, &face)
|
||||
};
|
||||
if styled || shown().is_some_and(|g| g != c.gid) {
|
||||
if styled || shown().is_some_and(|g| g != Some(c.gid)) {
|
||||
return Err(bad(
|
||||
C,
|
||||
format!("glyph {} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system)", c.gid),
|
||||
@@ -1259,7 +1268,8 @@ fn glyph_alternates(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (ch, bytes, face) = glyph_target(&t, p, C)?;
|
||||
let default = face.glyph_for(ch);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let chosen = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len)).map_or(default, |g| g.gid);
|
||||
let first = lay.glyphs.iter().find(|g| g.byte <= bytes.start && bytes.start < g.byte.saturating_add(g.len)).map(|g| g.gid);
|
||||
let chosen = own_glyph(&t.plain_text(), &lay, ch, &bytes, &face).flatten().or(first).unwrap_or(default);
|
||||
let alternates: Vec<Value> = face.alternates(ch).iter().map(|a| json!({"gid": a.gid, "feature": String::from_utf8_lossy(&a.feature)})).collect();
|
||||
Ok(json!({"char": ch.to_string(), "font": face.family, "style": face.style, "default": default, "chosen": chosen, "alternates": alternates}))
|
||||
}
|
||||
|
||||
@@ -2561,6 +2561,96 @@ fn the_default_glyph_chosen_in_overflowing_vertical_type_is_kept() {
|
||||
assert_eq!(held, [g('!')], "the default glyph is kept as the choice");
|
||||
}
|
||||
|
||||
/// A vowel sign drawn before its consonant (Devanagari Ka with the vowel sign I) is in the
|
||||
/// consonant's cluster, drawn first: with `salt`, the glyph reported for the consonant is the one
|
||||
/// the feature draws, not the vowel sign's, and the default glyph chosen for it is drawn.
|
||||
#[test]
|
||||
fn the_glyph_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{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());
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": text.len(), "features": ["salt"]})).unwrap();
|
||||
let gids = |s: &Session| vectorcraft_text::layout(db, &obj(s, id)).glyphs.iter().map(|gl| (gl.gid, gl.byte, gl.len)).collect::<Vec<_>>();
|
||||
assert_eq!(gids(&s), [(g('i'), 0, 6), (g('b'), 0, 6)], "the vowel sign is drawn first, in the cluster of Ka as salt draws it");
|
||||
|
||||
let query = s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap();
|
||||
assert_eq!(query["chosen"], json!(g('b')), "the glyph salt draws for Ka");
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('b')));
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": g('a')})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('a')));
|
||||
assert!(gids(&s).iter().any(|&(gl, byte, _)| (gl, byte) == (g('a'), 0)), "Ka draws its default glyph: {:?}", gids(&s));
|
||||
assert_eq!(s.execute("text.glyphAlternates", &json!({"id": id.0, "start": 0})).unwrap()["chosen"], json!(g('a')));
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn the_glyph_of_an_overflowing_character_is_shaped_with_its_grapheme() {
|
||||
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}";
|
||||
let point = json!({"x": 100, "y": 100, "text": text, "size": 20, "font": MATRA_FAMILY});
|
||||
let id = NodeId(s.execute("text.create", &point).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": text.len(), "features": ["salt"]})).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('N'), 0, 6)], "Ka and the nukta make the ligature");
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('N')));
|
||||
|
||||
let area = json!({"x": 0, "y": 300, "text": text, "size": 20, "font": MATRA_FAMILY, "area": {"width": 200, "height": 4}});
|
||||
let boxed = NodeId(s.execute("text.create", &area).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("text.setRangeStyle", &json!({"id": boxed.0, "start": 0, "end": text.len(), "features": ["salt"]})).unwrap();
|
||||
assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows");
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": boxed.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('N')), "the overflowing Ka is shaped with its nukta");
|
||||
}
|
||||
|
||||
/// A grapheme whose characters have different styles is shaped in those styles when it overflows:
|
||||
/// a nukta of another size does not make the `nukt` ligature with Ka, so `gid: null` reports the
|
||||
/// glyph `salt` draws for Ka, as it does for the laid-out text.
|
||||
#[test]
|
||||
fn an_overflowing_grapheme_keeps_the_styles_of_its_characters() {
|
||||
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}";
|
||||
let styled = |s: &mut Session, area: Option<Value>| {
|
||||
let mut spec = json!({"x": 0, "y": 300, "text": text, "size": 20, "font": MATRA_FAMILY});
|
||||
if let Some(area) = area {
|
||||
spec["area"] = area;
|
||||
}
|
||||
let id = NodeId(s.execute("text.create", &spec).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": text.len(), "features": ["salt"]})).unwrap();
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 3, "end": text.len(), "size": 30})).unwrap();
|
||||
id
|
||||
};
|
||||
let open = styled(&mut s, None);
|
||||
let lay = vectorcraft_text::layout(db, &obj(&s, open));
|
||||
assert_eq!(lay.glyphs.iter().find(|gl| gl.byte == 0).map(|gl| gl.gid), Some(g('b')), "the nukta of another size makes no ligature");
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": open.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('b')));
|
||||
|
||||
let boxed = styled(&mut s, Some(json!({"width": 200, "height": 4})));
|
||||
assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows");
|
||||
let r = s.execute("text.setGlyphAlternate", &json!({"id": boxed.0, "start": 0, "gid": null})).unwrap();
|
||||
assert_eq!(r["gid"], json!(g('b')), "the overflowing Ka is shaped apart from the nukta of another size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() {
|
||||
use vectorcraft_text::test_fonts::{alternates_family, alternates_font};
|
||||
|
||||
@@ -423,6 +423,57 @@ pub fn half_form_font(half: bool) -> Option<Vec<u8>> {
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`matra_font`].
|
||||
pub const MATRA_FAMILY: &str = "Matratest Sans";
|
||||
|
||||
/// A test-only cmap over bundled Source Sans 3: Devanagari Ka uses a, the vowel sign I (drawn
|
||||
/// before its consonant) uses i and the nukta uses the period. The `GSUB` table's `dev2` script has
|
||||
/// a `nukt` ligature of Ka and the nukta into N, and a `salt` single substitution of Ka by b.
|
||||
/// Family [`MATRA_FAMILY`]. `None` when it can't be built.
|
||||
pub fn matra_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 = [('क', 'a'), ('\u{093F}', 'i'), ('\u{093C}', '.')];
|
||||
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 = MATRA_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Matratest Sans Regular"), (5, "Version 1.000"), (6, "MatratestSans-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('a')?;
|
||||
let ligature = LigatureSet::new(vec![Ligature::new(gid('N')?, vec![gid('.')?])]);
|
||||
let nukt = LigatureSubstFormat1::new(CoverageFormat1::new(vec![ka]).into(), vec![ligature]);
|
||||
let salt = SingleSubstFormat2::new(CoverageFormat1::new(vec![ka]).into(), vec![gid('b')?]);
|
||||
// Features sorted by tag, as OpenType requires: `nukt`, then `salt`.
|
||||
let features = FeatureList::new(vec![
|
||||
FeatureRecord::new(Tag::new(b"nukt"), Feature::new(None, vec![0])),
|
||||
FeatureRecord::new(Tag::new(b"salt"), Feature::new(None, vec![1])),
|
||||
]);
|
||||
let lookups = SubstitutionLookupList::new(vec![
|
||||
SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![nukt])),
|
||||
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])), 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 [`lam_alef_font`] whose lam-alef ligature is in `feature`.
|
||||
pub fn lam_alef_family(feature: &[u8; 4]) -> &'static str {
|
||||
match feature {
|
||||
|
||||
Reference in New Issue
Block a user