mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:42:42 +00:00
Merge pull request #7678 from dotCooCoo/fix/optical-keeps-tsume
Type: Optical kerning keeps the space Tsume takes out
This commit is contained in:
@@ -336,6 +336,8 @@ impl Ctx<'_> {
|
||||
tab.kern_shift = 0.0;
|
||||
tab.aki_shift = 0.0;
|
||||
tab.aki = 0.0;
|
||||
tab.tsume_shift = 0.0;
|
||||
tab.tsume = 0.0;
|
||||
tab.tcy = None;
|
||||
tab.wari = None;
|
||||
tab.vpal = None;
|
||||
@@ -653,6 +655,8 @@ impl Ctx<'_> {
|
||||
k.kern_shift = 0.0;
|
||||
k.aki_shift = 0.0;
|
||||
k.aki = 0.0;
|
||||
k.tsume_shift = 0.0;
|
||||
k.tsume = 0.0;
|
||||
k.len = 0;
|
||||
k.lead = 0.0;
|
||||
k.ch = kashida::TATWEEL;
|
||||
@@ -713,6 +717,8 @@ mod emit_kashida_mark_last {
|
||||
kern_shift: 0.0,
|
||||
aki_shift: 0.0,
|
||||
aki: 0.0,
|
||||
tsume_shift: 0.0,
|
||||
tsume: 0.0,
|
||||
sx: face.upem.recip() * 48.0,
|
||||
sy: face.upem.recip() * 48.0,
|
||||
bshift: 0.0,
|
||||
@@ -779,6 +785,8 @@ mod emit_kashida_mark_last {
|
||||
kern_shift: 0.0,
|
||||
aki_shift: 0.0,
|
||||
aki: 0.0,
|
||||
tsume_shift: 0.0,
|
||||
tsume: 0.0,
|
||||
sx: face.upem.recip() * 48.0,
|
||||
sy: face.upem.recip() * 48.0,
|
||||
bshift: 0.0,
|
||||
|
||||
@@ -22,6 +22,8 @@ fn synthetic_glyphs(specs: &[(usize, usize, f64, char)]) -> Vec<SGlyph> {
|
||||
kern_shift: 0.0,
|
||||
aki_shift: 0.0,
|
||||
aki: 0.0,
|
||||
tsume_shift: 0.0,
|
||||
tsume: 0.0,
|
||||
sx: 1.0,
|
||||
sy: 1.0,
|
||||
bshift: 0.0,
|
||||
|
||||
@@ -49,6 +49,12 @@ pub(crate) struct SGlyph {
|
||||
/// The Insert Aki Left and Right `adv` holds, in points: on the last glyph of its cluster, 0
|
||||
/// on the others. Justification does not stretch or narrow it.
|
||||
pub aki: f64,
|
||||
/// The space Tsume ([`tighten`]) took before the cluster's ink, in points, on every glyph of
|
||||
/// its cluster: `dx` is that much smaller.
|
||||
pub tsume_shift: f64,
|
||||
/// The space Tsume took out of `adv`, in points: on the last glyph of its cluster, 0 on the
|
||||
/// others.
|
||||
pub tsume: f64,
|
||||
/// Outline scale (font units -> points).
|
||||
pub sx: f64,
|
||||
pub sy: f64,
|
||||
@@ -232,6 +238,8 @@ pub(crate) fn hyphen_glyph(g: &SGlyph) -> SGlyph {
|
||||
h.kern_shift = 0.0;
|
||||
h.aki_shift = 0.0;
|
||||
h.aki = 0.0;
|
||||
h.tsume_shift = 0.0;
|
||||
h.tsume = 0.0;
|
||||
h.byte = g.byte + g.len;
|
||||
h.len = 0;
|
||||
h.ch = '-';
|
||||
@@ -838,8 +846,8 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<
|
||||
let mut cache = HashMap::new();
|
||||
let mut corrections: HashMap<[u64; 15], Option<f64>> = HashMap::new();
|
||||
let mut references: HashMap<u32, Option<f64>> = HashMap::new();
|
||||
// The Insert Aki a cluster's last glyph holds does not make a mark its base.
|
||||
let shaped_advances: Vec<f64> = glyphs.iter().map(|g| g.adv - g.aki).collect();
|
||||
// The Insert Aki and Tsume a cluster's last glyph holds do not make a mark its base.
|
||||
let shaped_advances: Vec<f64> = glyphs.iter().map(|g| g.adv - g.aki + g.tsume).collect();
|
||||
for i in 0..glyphs.len().saturating_sub(1) {
|
||||
let Some((a, b)) = glyphs.get(i).zip(glyphs.get(i + 1)) else { continue };
|
||||
if a.level != b.level {
|
||||
@@ -897,10 +905,10 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<
|
||||
continue;
|
||||
}
|
||||
// Neither glyph is rotated, so a right-to-left manual kerning shift adds to the offset.
|
||||
// Insert Aki is measured as no space: the pair is spaced as it would be without it, and
|
||||
// the Aki stays on top of that.
|
||||
let left_x = left.dx + left.kern_shift - left.aki_shift;
|
||||
let right_x = right.dx + right.kern_shift - right.aki_shift;
|
||||
// Insert Aki is measured as no space, and Tsume as taking none: the pair is spaced as it
|
||||
// would be without them, and they stay on top of that.
|
||||
let left_x = left.dx + left.kern_shift - left.aki_shift + left.tsume_shift;
|
||||
let right_x = right.dx + right.kern_shift - right.aki_shift + right.tsume_shift;
|
||||
let left_y = left.dy - left.bshift;
|
||||
let right_y = right.dy - right.bshift;
|
||||
let Some(left_style) = runs.get(left.run).map(|(_, s)| *s) else { continue };
|
||||
@@ -909,7 +917,7 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<
|
||||
let tracking = left_style.tracking / 1000.0 * em * spacing_scale(left_style, false);
|
||||
// A manual kerning value on the left glyph is part of its advance and, right to left, of
|
||||
// its offset too (it spaces the left glyph from its left neighbor): it is not taken off.
|
||||
let advance = glyphs[cluster_start..=cluster_end].iter().map(|g| g.adv - g.aki).sum::<f64>() - tracking;
|
||||
let advance = glyphs[cluster_start..=cluster_end].iter().map(|g| g.adv - g.aki + g.tsume).sum::<f64>() - tracking;
|
||||
let key = [
|
||||
u64::from(left.face.id()),
|
||||
u64::from(left.gid),
|
||||
@@ -1280,6 +1288,8 @@ fn inline_glyph(
|
||||
kern_shift: if level.is_rtl() && !vertical { manual_kern } else { before } + aki_shift,
|
||||
aki_shift,
|
||||
aki: if adv.is_finite() { aki_left + aki_right } else { 0.0 },
|
||||
tsume_shift: 0.0,
|
||||
tsume: 0.0,
|
||||
// Not used to draw (the art has its own transform), but they make the glyph's em box
|
||||
// its run's em: Character Alignment and Top-to-Top leading treat an inline graphic as a
|
||||
// character of its run's size, whatever its scale (see `layout::glyph_em`).
|
||||
@@ -1788,6 +1798,8 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
kern_shift,
|
||||
aki_shift,
|
||||
aki: aki_in_adv,
|
||||
tsume_shift: 0.0,
|
||||
tsume: 0.0,
|
||||
sx: k * hs,
|
||||
sy: k * vs,
|
||||
bshift: gst.baseline_shift + pos_shift,
|
||||
@@ -1826,8 +1838,9 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
/// drawn along the line, the last one with `own` as its width before the tracking, kerning and
|
||||
/// Insert Aki its advance holds. `tsume` percent of the blank space between the cluster's own width
|
||||
/// and its ink, on each side, comes out of the cluster's advance, and its glyphs are drawn the
|
||||
/// left side's share earlier. A cluster without ink, such as a space, or with a width that is not
|
||||
/// positive, keeps its place and width.
|
||||
/// left side's share earlier ([`SGlyph::tsume`] and [`SGlyph::tsume_shift`] hold both, for Optical
|
||||
/// kerning to measure the pair without them). A cluster without ink, such as a space, or with a
|
||||
/// width that is not positive, keeps its place and width.
|
||||
fn tighten(glyphs: &mut [SGlyph], own: f64, tsume: f64) {
|
||||
let Some(last) = glyphs.len().checked_sub(1) else { return };
|
||||
let share = if tsume.is_nan() { 0.0 } else { tsume.clamp(0.0, MAX_TSUME) / 100.0 };
|
||||
@@ -1848,9 +1861,11 @@ fn tighten(glyphs: &mut [SGlyph], own: f64, tsume: f64) {
|
||||
let right = (pen - hi).clamp(0.0, pen) * share;
|
||||
for g in glyphs.iter_mut() {
|
||||
g.dx -= left;
|
||||
g.tsume_shift = left;
|
||||
}
|
||||
if let Some(g) = glyphs.get_mut(last) {
|
||||
g.adv -= left + right;
|
||||
g.tsume = left + right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -381,6 +381,44 @@ fn check_spaced_harakat(db: &FontDb, family: &str) {
|
||||
assert!((length(&b) - length(&a) - 12.0).abs() < 1e-6, "{family}: {} → {}", length(&a), length(&b));
|
||||
}
|
||||
|
||||
/// Right to left, with harakat coloured apart, Optical kerning keeps the Tsume of each letter's
|
||||
/// cluster as it does in "HOH": the glyphs move as far with Optical kerning as without it.
|
||||
#[test]
|
||||
fn optical_kerning_keeps_the_tsume_of_a_letter_with_coloured_harakat() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(crate::test_fonts::lam_alef_font(b"rlig").unwrap()), 1);
|
||||
check_optical_tsume(&db, crate::test_fonts::lam_alef_family(b"rlig"));
|
||||
if let Some(face) = arabic_face("optical_kerning_keeps_the_tsume_of_a_letter_with_coloured_harakat") {
|
||||
check_optical_tsume(FontDb::global(), &face.family);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_optical_tsume(db: &FontDb, family: &str) {
|
||||
const WORD: &str = "بَبَ"; // ب 0, fatha 2, ب 4, fatha 6
|
||||
let set = |optical: bool, letter: Option<usize>| {
|
||||
let mut t = text_with_harakat(WORD, family, Some(red()));
|
||||
crate::edit::style_range(&mut t.runs, 0, WORD.len(), |st| st.optical_kerning = optical);
|
||||
if let Some(at) = letter {
|
||||
crate::edit::style_range(&mut t.runs, at, at + 2, |st| st.tsume = 100.0);
|
||||
}
|
||||
t
|
||||
};
|
||||
// Each glyph's place and advance: where a mark's ink leaves no blank space on the left, the
|
||||
// Tsume of the line's first cluster only narrows its advance and moves no glyph.
|
||||
let xs = |t: &TextObject| layout(db, t).glyphs.iter().flat_map(|g| [g.xf.as_coeffs()[4], g.advance]).collect::<Vec<_>>();
|
||||
assert!(xs(&set(true, None)).iter().zip(xs(&set(false, None))).any(|(a, b)| (a - b).abs() > 1e-6), "{family}: Optical moves the pair");
|
||||
// Each letter in turn: the first one's cluster ends the pair, the second one's starts it.
|
||||
for letter in [0, 4] {
|
||||
let moved = |optical: bool| xs(&set(optical, Some(letter))).iter().zip(xs(&set(optical, None))).map(|(a, b)| a - b).collect::<Vec<_>>();
|
||||
let (without, with_optical) = (moved(false), moved(true));
|
||||
assert!(without.iter().any(|m| m.abs() > 1e-3), "{family} {letter}: Tsume moves or narrows the glyphs: {without:?}");
|
||||
assert!(
|
||||
without.len() == with_optical.len() && without.iter().zip(&with_optical).all(|(a, b)| (a - b).abs() < 1e-6),
|
||||
"{family} {letter}: {without:?} {with_optical:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_chars_need_the_same_setting_and_each_character_in_its_cluster() {
|
||||
// بَبَب right to left: final beh (cluster 8), fatha and medial beh (cluster 4), fatha and
|
||||
|
||||
@@ -112,6 +112,25 @@ fn the_font_kerning_is_added_after_the_tsume() {
|
||||
assert!(near(&before_kerned, &before_h) && before_h[0] < -10.0, "{before_kerned:?} {before_h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_kerning_keeps_the_tsume() {
|
||||
// Optical kerning spaces each pair as without the Tsume, and the Tsume stays on top: the
|
||||
// characters move back as far with Optical kerning as without it.
|
||||
let plain = point("HOH", style(100.0));
|
||||
let optical = point("HOH", CharStyle { optical_kerning: true, ..style(100.0) });
|
||||
let xs = |t: &TextObject| {
|
||||
let l = layout(db(), t);
|
||||
[0, 1, 2].map(|i| drawn_x(&l, i))
|
||||
};
|
||||
// The test font's Optical spacing differs from its own for these pairs.
|
||||
assert!(!near(&xs(&optical), &xs(&plain)), "{:?} {:?}", xs(&optical), xs(&plain));
|
||||
for at in [0, 1, 2] {
|
||||
let without = moves(&plain, &with(&plain, at, |s| s.tsume = 100.0), &[0, 1, 2]);
|
||||
let with_optical = moves(&optical, &with(&optical, at, |s| s.tsume = 100.0), &[0, 1, 2]);
|
||||
assert!(without.iter().any(|m| *m < -1.0) && near(&with_optical, &without), "{at}: {without:?} {with_optical:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_letter_with_its_mark_takes_the_tsume_of_the_letter() {
|
||||
let plain = point("H\u{301}O", style(100.0));
|
||||
|
||||
Reference in New Issue
Block a user