Type: right-to-left manual kerning moves a rotated character along the line

Right-to-left manual kerning was added to the glyph's offset, which
Character Rotation turns, so a rotated character with kerning moved
across the line (up at 90 degrees) instead of away from the next
character in reading order. The shift is now applied to the pen
position along the baseline, outside the rotation, and the rotation
turns the glyph about that moved pen position.
This commit is contained in:
dotCooCoo
2026-11-02 14:32:54 +07:00
committed by bflatastic
parent 8346f052e9
commit 0895eccfe1
5 changed files with 52 additions and 12 deletions
+1
View File
@@ -91,6 +91,7 @@ pub(crate) fn apply_desired(sg: &mut [SGlyph], j: &JustificationOptions, scale_g
g.adv *= g_des;
g.sx *= g_des;
g.dx *= g_des;
g.kern_shift *= g_des;
}
f.width = g.adv;
f.scale = g_des;
+5 -2
View File
@@ -181,6 +181,7 @@ impl Ctx<'_> {
tab.gid = 0;
tab.dx = 0.0;
tab.dy = 0.0;
tab.kern_shift = 0.0;
tab.tcy = None;
tab.vpal = None;
let pen = para.left_indent + para.first_line_indent + marker.iter().map(|g| g.adv).sum::<f64>();
@@ -229,8 +230,9 @@ impl Ctx<'_> {
Some(ib) => Affine::translate((-lead, -g.bshift)) * ib.xf,
// Baseline shift moves the glyph across its line, and Character Rotation then turns it
// about its shifted pen position, so the shift is across the line at any rotation.
// Right-to-left manual kerning moves that pen position along the line.
None => {
Affine::translate((0.0, -g.bshift))
Affine::translate((g.kern_shift, -g.bshift))
* Affine::rotate(-g.rotation.to_radians())
* Affine::translate((g.dx - lead, g.dy))
* Affine::scale_non_uniform(g.sx, g.sy)
@@ -238,7 +240,7 @@ impl Ctx<'_> {
};
// The turning point in font units, which later moves of the glyph's transform keep.
let pivot = match local.determinant() {
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(0.0, -g.bshift),
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(g.kern_shift, -g.bshift),
_ => Point::ZERO,
};
let unturned = if g.inline.is_none() && g.rotation != 0.0 && g.sx != 0.0 && g.sy != 0.0 {
@@ -332,6 +334,7 @@ impl Ctx<'_> {
k.adv = t;
k.dx = 0.0;
k.dy = 0.0;
k.kern_shift = 0.0;
k.len = 0;
k.lead = 0.0;
k.ch = kashida::TATWEEL;
+1
View File
@@ -18,6 +18,7 @@ fn synthetic_glyphs(specs: &[(usize, usize, f64, char)]) -> Vec<SGlyph> {
adv,
dx: 0.0,
dy: 0.0,
kern_shift: 0.0,
sx: 1.0,
sy: 1.0,
bshift: 0.0,
+19 -10
View File
@@ -30,9 +30,12 @@ pub(crate) struct SGlyph {
pub run: usize,
/// Advance in points (tracking, manual kerning and horizontal scale applied).
pub adv: f64,
/// Offset from the pen position, y down (right to left, manual kerning applied).
/// Offset from the pen position, y down.
pub dx: f64,
pub dy: f64,
/// Right to left: the run's manual kerning, which moves the glyph right along the baseline,
/// outside its Character Rotation. 0 otherwise.
pub kern_shift: f64,
/// Outline scale (font units -> points).
pub sx: f64,
pub sy: f64,
@@ -169,6 +172,7 @@ pub(crate) fn hyphen_glyph(g: &SGlyph) -> SGlyph {
h.adv = g.face.advance(gid) * g.sx;
h.dx = 0.0;
h.dy = 0.0;
h.kern_shift = 0.0;
h.byte = g.byte + g.len;
h.len = 0;
h.ch = '-';
@@ -616,6 +620,9 @@ 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.
let left_x = left.dx + left.kern_shift;
let right_x = right.dx + right.kern_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 };
@@ -631,11 +638,11 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<
u64::from(right.gid),
left.sx.to_bits(),
left.sy.to_bits(),
left.dx.to_bits(),
left_x.to_bits(),
left_y.to_bits(),
right.sx.to_bits(),
right.sy.to_bits(),
right.dx.to_bits(),
right_x.to_bits(),
right_y.to_bits(),
advance.to_bits(),
em.to_bits(),
@@ -648,8 +655,8 @@ fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<
profile_correction(
&a.0,
&b.0,
(left.sx, left.sy, left.dx, left_y),
(right.sx, right.sy, right.dx, right_y),
(left.sx, left.sy, left_x, left_y),
(right.sx, right.sy, right_x, right_y),
advance,
em,
left.face.upem,
@@ -812,6 +819,7 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
adv: if adv.is_finite() { adv } else { 0.0 },
dx: 0.0,
dy: 0.0,
kern_shift: 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`).
@@ -1111,16 +1119,16 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
let last_in_cluster = gi + 1 == n || raw[gi + 1].1 as usize != cl;
let ch = first_char(cl);
let mut adv = xa as f64 * k * hs;
let mut dx = xo as f64 * k * hs;
let mut kern_shift = 0.0;
if ch == SOFT_HYPHEN || ch == '\u{2028}' {
adv = 0.0;
} else {
let manual_kern = gst.kerning.map(|v| v / 1000.0 * size).unwrap_or(0.0);
// Manual kerning spaces a character from the next one in reading order. Right to left,
// that one is drawn to its left: the cluster's glyphs move right by the value, so that
// the space its last glyph's advance gains is at the cluster's left.
// that one is drawn to its left: the cluster's glyphs move right along the baseline by
// the value, so that the space its last glyph's advance gains is at the cluster's left.
if level.is_rtl() && !feats.vertical {
dx += manual_kern;
kern_shift = manual_kern;
}
if last_in_cluster {
let tracking = gst.tracking / 1000.0 * size;
@@ -1134,8 +1142,9 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
len: end.saturating_sub(cl).max(1),
run,
adv,
dx,
dx: xo as f64 * k * hs,
dy: -(yo as f64) * k * vs,
kern_shift,
sx: k * hs,
sy: k * vs,
bshift: gst.baseline_shift + pos_shift,
+26
View File
@@ -98,6 +98,32 @@ fn rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order() {
}
}
#[test]
fn rtl_manual_kerning_on_a_rotated_character_moves_it_along_the_baseline() {
// Reading order: alef (bytes 0..2), bet (2..4), gimel (4..6); bet is turned 90 degrees and
// carries the kerning.
let kerned = |value: f64| {
let mut t = TextObject::point(Point::ZERO, "אבג", CharStyle { size: 100.0, ..CharStyle::default() });
edit::style_range(&mut t.runs, 2, 4, |s| {
s.kerning = Some(value);
s.rotation = 90.0;
});
layout(db(), &t)
};
let origin = |lay: &TextLayout, byte: usize| {
let g = lay.glyphs.iter().find(|g| g.byte == byte).unwrap();
assert!(g.rtl && g.gid != 0, "byte {byte} is set right to left with a glyph of its own");
g.xf * Point::ZERO
};
let (plain, spaced) = (kerned(0.0), kerned(200.0));
let moved = origin(&spaced, 2) - origin(&plain, 2);
let gimel = origin(&spaced, 4) - origin(&plain, 4);
assert!((moved.x - gimel.x - 20.0).abs() < 0.01, "kerning on the turned bet spaces it from gimel along the line: {moved:?}, {gimel:?}");
assert!(moved.y.abs() < 0.01, "the turned bet stays on its baseline: {moved:?}");
let alef = origin(&spaced, 0) - origin(&plain, 0);
assert!((alef.x - moved.x).abs() < 0.01 && alef.y.abs() < 0.01, "alef keeps its place next to bet: {alef:?}");
}
#[test]
fn optical_pair_skips_an_invisible_soft_hyphen_with_an_outline() {
use crate::test_fonts::{SOFT_HYPHEN_OUTLINE_FAMILY, soft_hyphen_outline_font};