Merge pull request #5628 from dotCooCoo/fix/rtl-kerning-whole-point-widths

Type: right-to-left manual kerning keeps whole-point spacing on both sides
This commit is contained in:
BFlat
2026-11-04 22:54:15 +07:00
committed by GitHub
3 changed files with 47 additions and 6 deletions
+15 -5
View File
@@ -1,14 +1,24 @@
use super::flow::flow;
use super::region::Region;
use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, is_cjk, punct};
use super::{Affine, Ctx, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, continues_cluster, is_cjk, punct};
/// System Layout on or Fractional Widths off: each character that takes space advances by a whole
/// number of points, at least one, so small type does not collapse onto itself. A zero advance (a
/// tate-chu-yoko continuation, a zero-width mark) stays zero.
/// tate-chu-yoko continuation, a zero-width mark) stays zero. Right to left, a cluster with manual
/// kerning moves by the difference the kerning makes to its rounded advance, so the rounding
/// changes the space before the next character in reading order and not the space after the one
/// before it.
pub(super) fn whole_character_widths(glyphs: &mut [SGlyph]) {
for g in glyphs {
if g.adv.is_finite() && g.adv > 0.0 {
g.adv = g.adv.round().max(1.0);
let whole = |adv: f64| if adv.is_finite() && adv > 0.0 { adv.round().max(1.0) } else { adv };
for cluster in glyphs.chunk_by_mut(|a, b| continues_cluster(Some(a), Some(b))) {
// The cluster's last glyph carries the manual kerning in its advance.
let (adv, shift) = cluster.last().map_or((0.0, 0.0), |g| (g.adv, g.kern_shift));
let rounded_shift = Some(whole(adv) - whole(adv - shift)).filter(|s| s.is_finite()).unwrap_or(shift);
for g in cluster {
g.adv = whole(g.adv);
if shift != 0.0 {
g.kern_shift = rounded_shift;
}
}
}
}
+2 -1
View File
@@ -34,7 +34,8 @@ pub(crate) struct SGlyph {
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.
/// outside its Character Rotation; with whole-point advances, the difference the kerning makes
/// to the cluster's rounded advance. 0 otherwise.
pub kern_shift: f64,
/// Outline scale (font units -> points).
pub sx: f64,
+30
View File
@@ -98,6 +98,36 @@ fn rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order() {
}
}
#[test]
fn rtl_manual_kerning_with_whole_point_widths_keeps_the_previous_character_in_place() {
// Reading order: alef (bytes 0..2), bet (2..4), gimel (4..6); bet carries the kerning, which
// is a fraction of a point.
for (system_layout, fractional_widths) in [(true, true), (false, false)] {
for value in [3.0, 4.0, 6.0, 7.0] {
let kerned = |value: f64| {
let mut t = TextObject::point(Point::ZERO, "אבג", CharStyle { size: 100.0, ..CharStyle::default() });
t.system_layout = system_layout;
t.fractional_widths = fractional_widths;
edit::style_range(&mut t.runs, 2, 4, |s| s.kerning = Some(value));
layout(db(), &t)
};
let ink = |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).x
};
let (plain, spaced) = (kerned(0.0), kerned(value));
let gap = |lay: &TextLayout, right: usize, left: usize| ink(lay, right) - ink(lay, left);
assert!(
(gap(&spaced, 0, 2) - gap(&plain, 0, 2)).abs() < 1e-9,
"alef, before bet in reading order, keeps its place (kerning {value}, System Layout {system_layout})"
);
let wider = gap(&spaced, 2, 4) - gap(&plain, 2, 4);
assert!((wider - wider.round()).abs() < 1e-9, "bet moves from gimel by whole points: {wider} (kerning {value})");
}
}
}
#[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