mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 03:48:01 +00:00
Merge pull request #5707 from dotCooCoo/fix/glyph-alternates-overflow-probe
Glyphs: the Glyphs query reports the glyph of an overflowing character
This commit is contained in:
@@ -1192,6 +1192,27 @@ fn own_glyph(
|
||||
})
|
||||
}
|
||||
|
||||
/// The glyph of `face` drawn for the character `ch` at `bytes` of `t` when the character is shaped
|
||||
/// alone in the writing direction of `t` and the direction its paragraph sets it in, with the rest
|
||||
/// of its grapheme in the grapheme's own styles: the glyph of a character that is not laid out
|
||||
/// (overflowing text).
|
||||
fn shaped_alone(t: &TextObject, ch: char, bytes: &std::ops::Range<usize>, face: &vectorcraft_text::FontFace) -> Option<u32> {
|
||||
use vectorcraft_doc::ParaDirection;
|
||||
let plain = t.plain_text();
|
||||
let end = edit::next_cluster(&plain, bytes.start).max(bytes.end);
|
||||
let mut single = TextObject::point(Point::ZERO, "", CharStyle::default());
|
||||
single.runs = edit::slice_runs(&t.runs, bytes.start, end);
|
||||
single.vertical = t.vertical;
|
||||
// The bidi level of the character in its paragraph (a parenthesis is mirrored at an odd one).
|
||||
let before = plain.get(..bytes.start).unwrap_or_default();
|
||||
let (paragraph, start) = (before.matches('\n').count(), before.rfind('\n').map_or(0, |i| i + 1));
|
||||
let stop = plain.get(bytes.start..).and_then(|after| after.find('\n')).map_or(plain.len(), |i| bytes.start + i);
|
||||
let rtl = vectorcraft_text::char_is_rtl(plain.get(start..stop).unwrap_or_default(), t.para_at(paragraph).direction, bytes.start - start);
|
||||
single.para.direction = Some(if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight });
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &single);
|
||||
own_glyph(&single.plain_text(), &lay, ch, &(0..bytes.len()), face).flatten()
|
||||
}
|
||||
|
||||
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`"))?;
|
||||
@@ -1220,21 +1241,12 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// 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 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();
|
||||
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(&grapheme, &vectorcraft_text::layout(db, &single), ch, &(0..bytes.len()), &face).flatten()
|
||||
})
|
||||
}
|
||||
};
|
||||
let unchosen = holder.as_ref().filter(|_| !styled).and_then(|_| {
|
||||
let mut probe = t.clone();
|
||||
edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe);
|
||||
own_glyph(&plain, &lay, ch, &bytes, &face).unwrap_or_else(|| shaped_alone(&probe, ch, &bytes, &face))
|
||||
});
|
||||
let substituted = unchosen.is_some_and(|g| g != default);
|
||||
let choice = match gid.filter(|g| *g != default || substituted) {
|
||||
None => None,
|
||||
@@ -1283,7 +1295,13 @@ fn glyph_alternates(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let default = face.glyph_for(ch);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
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);
|
||||
// A character that is not laid out (overflowing text) reports the glyph it is drawn with when
|
||||
// it is shaped alone, as `text.setGlyphAlternate` finds it.
|
||||
let chosen = match own_glyph(&t.plain_text(), &lay, ch, &bytes, &face) {
|
||||
Some(own) => own.or(first),
|
||||
None => shaped_alone(&t, ch, &bytes, &face),
|
||||
}
|
||||
.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,72 @@ fn the_default_glyph_chosen_in_overflowing_vertical_type_is_kept() {
|
||||
assert_eq!(held, [g('!')], "the default glyph is kept as the choice");
|
||||
}
|
||||
|
||||
/// A character that overflows its area reports as `chosen` the glyph it is drawn with when it is
|
||||
/// shaped alone: the glyph `salt` draws, a chosen default glyph, and the `vert` glyph of vertical
|
||||
/// type.
|
||||
#[test]
|
||||
fn the_glyph_query_shapes_an_overflowing_character() {
|
||||
use vectorcraft_text::test_fonts::{VERTICAL_PUNCTUATION_FAMILY, alternates_family, alternates_font, vertical_punctuation_font};
|
||||
let _fonts = crate::adding_fonts();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
db.add_font(alternates_font(true).expect("the test font builds"));
|
||||
db.add_font(vertical_punctuation_font(true).expect("the test font builds"));
|
||||
let face = db.face(alternates_family(true), "Regular").expect("the test face");
|
||||
let g = |c| face.glyph_for(c);
|
||||
let mut s = session();
|
||||
let chosen = |s: &mut Session, id: NodeId, start: usize| {
|
||||
s.execute("text.glyphAlternates", &json!({"id": id.0, "start": start})).unwrap()["chosen"].clone()
|
||||
};
|
||||
|
||||
let area = json!({"x": 0, "y": 300, "text": "xa", "size": 20, "font": alternates_family(true), "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": 2, "features": ["salt"]})).unwrap();
|
||||
assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows");
|
||||
assert_eq!(chosen(&mut s, boxed, 1), json!(g('b')), "the glyph salt draws");
|
||||
s.execute("text.setGlyphAlternate", &json!({"id": boxed.0, "start": 1, "gid": g('a')})).unwrap();
|
||||
assert_eq!(chosen(&mut s, boxed, 1), json!(g('a')), "the chosen default glyph");
|
||||
|
||||
let vface = db.face(VERTICAL_PUNCTUATION_FAMILY, "Regular").expect("the test face");
|
||||
let text =
|
||||
json!({"x": 0, "y": 0, "text": "!!", "size": 20, "font": VERTICAL_PUNCTUATION_FAMILY, "vertical": true, "area": {"width": 4, "height": 4}});
|
||||
let vertical = NodeId(s.execute("text.create", &text).unwrap()["id"].as_u64().unwrap());
|
||||
assert!(vectorcraft_text::layout(db, &obj(&s, vertical)).glyphs.is_empty(), "the text overflows");
|
||||
assert_eq!(chosen(&mut s, vertical, 0), json!(vface.glyph_for('I')), "vert draws ! as I");
|
||||
}
|
||||
|
||||
/// A parenthesis set right to left is drawn mirrored: in a paragraph whose direction is right to
|
||||
/// left, and after Hebrew in a paragraph of automatic direction, but not when its character
|
||||
/// direction is left to right. When it overflows its area, the glyph query shapes it in the same
|
||||
/// direction and reports the glyph the text draws when it fits.
|
||||
#[test]
|
||||
fn the_glyph_query_shapes_an_overflowing_character_in_its_paragraph_direction() {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let face = db.face("Source Sans 3", "Regular").expect("the bundled face");
|
||||
let mut s = session();
|
||||
let create = |s: &mut Session, text: &str, rtl: bool, char_ltr: bool, height: f64| {
|
||||
let spec = json!({"x": 0, "y": 0, "text": text, "size": 20, "font": "Source Sans 3", "area": {"width": 200, "height": height}});
|
||||
let id = NodeId(s.execute("text.create", &spec).unwrap()["id"].as_u64().unwrap());
|
||||
if rtl {
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "direction": "rightToLeft", "start": 2, "end": 3})).unwrap();
|
||||
}
|
||||
if char_ltr {
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "charDirection": "leftToRight", "start": 2, "end": 3})).unwrap();
|
||||
}
|
||||
id
|
||||
};
|
||||
for (text, rtl, char_ltr, at, drawn) in [("a\n(", true, false, 2, ')'), ("a\n\u{5D0}(", false, false, 4, ')'), ("a\n(", true, true, 2, '(')] {
|
||||
let open = create(&mut s, text, rtl, char_ltr, 200.0);
|
||||
let lay = vectorcraft_text::layout(db, &obj(&s, open));
|
||||
assert_eq!(lay.glyphs.iter().find(|g| g.byte == at).map(|g| g.gid), Some(face.glyph_for(drawn)), "{text:?}: the parenthesis drawn");
|
||||
|
||||
let boxed = create(&mut s, text, rtl, char_ltr, 30.0);
|
||||
let lay = vectorcraft_text::layout(db, &obj(&s, boxed));
|
||||
assert!(lay.glyphs.iter().all(|g| g.byte < 2), "{text:?}: the second paragraph overflows");
|
||||
let r = s.execute("text.glyphAlternates", &json!({"id": boxed.0, "start": at})).unwrap();
|
||||
assert_eq!(r["chosen"], json!(face.glyph_for(drawn)), "{text:?}: the glyph drawn when it fits");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -813,6 +813,12 @@ pub fn paragraph_is_rtl(text: &str, direction: Option<vectorcraft_doc::ParaDirec
|
||||
layout::is_rtl(layout::para_bidi(text, direction).as_ref())
|
||||
}
|
||||
|
||||
/// Is the character at byte `at` of paragraph `text` set right to left: its bidi level is odd, with
|
||||
/// the paragraph's `direction` (None: from its first strong character)?
|
||||
pub fn char_is_rtl(text: &str, direction: Option<vectorcraft_doc::ParaDirection>, at: usize) -> bool {
|
||||
layout::para_bidi(text, direction).is_some_and(|info| info.levels.get(at).is_some_and(|level| level.is_rtl()))
|
||||
}
|
||||
|
||||
/// Text stored in visual order (as PDF content draws it, each glyph where it is drawn), back in
|
||||
/// logical order: the order of its characters (indices into `visual.chars()`) and whether its
|
||||
/// paragraph runs right to left; laid out that way, it reads as drawn. `None` when nothing in it is
|
||||
|
||||
Reference in New Issue
Block a user