Merge pull request #5245 from dotCooCoo/fix/inline-graphic-manual-kerning

Type: an inline graphic takes its character's manual kerning
This commit is contained in:
Brandon Thomas
2026-11-03 00:18:42 -05:00
committed by GitHub
4 changed files with 73 additions and 9 deletions
+3 -2
View File
@@ -226,8 +226,9 @@ impl Ctx<'_> {
let upright = self.vertical && !self.on_path && g.tcy.is_none() && stands_upright(g);
let lead = if upright { 0.0 } else { g.lead };
let local = match &g.inline {
// Inline graphics: art space to glyph space (no font outline).
Some(ib) => Affine::translate((-lead, -g.bshift)) * ib.xf,
// Inline graphics: art space to glyph space (no font outline), moved along the line by
// right-to-left manual kerning as a glyph is.
Some(ib) => Affine::translate((g.kern_shift - 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.
+19 -7
View File
@@ -401,7 +401,7 @@ pub(crate) fn shape_range(
let b = bytes.end;
if let Some(art) = art {
if let Some(face) = db.style_face(st).or_else(|| db.face_covering('a')) {
out.push(inline_glyph(&face, st, art, source_runs.start, a..b, text, level_at(a)));
out.push(inline_glyph(&face, st, art, source_runs.start, a..b, text, level_at(a), feats.vertical));
}
continue;
}
@@ -795,10 +795,21 @@ fn shaping_script(c: char) -> Script {
///
/// The art is scaled uniformly to `scale` x the font size tall, its left edge on the pen and its
/// vertical centre on the middle of the cap height raised by the inline's shift (see
/// [`InlineArt`]). Its advance is the scaled art width plus tracking; the glyph's ascent and
/// descent grow to the art's extent so a tall graphic opens up its line. A missing symbol takes a
/// square of the same height and draws nothing.
fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usize, range: Range<usize>, text: &str, level: Level) -> SGlyph {
/// [`InlineArt`]). Its advance is the scaled art width plus tracking and its character's manual
/// kerning, which right to left in horizontal type also moves the art right by the value, as it
/// moves a letter's glyph. The glyph's ascent and descent grow to the art's extent so a tall
/// graphic opens up its line. A missing symbol takes a square of the same height and draws nothing.
#[allow(clippy::too_many_arguments)]
fn inline_glyph(
face: &Arc<FontFace>,
st: &CharStyle,
art: &InlineArt,
run: usize,
range: Range<usize>,
text: &str,
level: Level,
vertical: bool,
) -> SGlyph {
let size = if st.size.is_finite() { st.size.max(0.0) } else { 0.0 };
let vs = if st.v_scale.is_finite() { st.v_scale / 100.0 } else { 1.0 };
let km = size / face.upem;
@@ -815,7 +826,8 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
_ => (kurbo::Affine::IDENTITY, h, None),
};
let tracking = st.tracking / 1000.0 * size;
let adv = width + if tracking.is_finite() { tracking } else { 0.0 };
let manual_kern = st.kerning.map(|v| v / 1000.0 * size).filter(|k| k.is_finite()).unwrap_or(0.0);
let adv = width + if tracking.is_finite() { tracking } else { 0.0 } + manual_kern;
SGlyph {
face: face.clone(),
gid: 0,
@@ -825,7 +837,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,
kern_shift: if level.is_rtl() && !vertical { manual_kern } else { 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`).
+50
View File
@@ -248,6 +248,56 @@ fn inline_art_in_a_right_to_left_paragraph_is_placed_by_the_bidi_reordering() {
assert!(inline_rtl && inline < first);
}
/// A manual kerning value on an inline graphic's character spaces it from the next character in
/// reading order by the distance the same value spaces a letter of its size: left to right, right
/// to left and down a vertical column. The character before it keeps its place, and a graphic's 0
/// sets the text as no value does.
#[test]
fn manual_kerning_on_an_inline_graphic_spaces_it_like_a_letter() {
let db = FontDb::global();
// `before`, then the graphic (or `letter` in its place) with `kerning`, then `after`.
let set = |before: &str, letter: Option<&str>, after: &str, kerning: Option<f64>, vertical: bool| {
let mut t = TextObject::point(Point::ZERO, before, style(20.0));
let st = CharStyle { kerning, ..style(20.0) };
t.runs.push(match letter {
Some(c) => TextRun::new(c, st),
None => TextRun::inline(art(1.0), st),
});
t.runs.push(TextRun::new(after, style(20.0)));
t.vertical = vertical;
let l = layout(db, &t);
let ink = |run: usize, last: bool| {
let mut g: Vec<&PositionedGlyph> = l.glyphs.iter().filter(|g| g.run == run).collect();
g.sort_by_key(|g| g.byte);
let g = if last { g.last() } else { g.first() };
g.expect("glyph").xf * Point::ZERO
};
assert_eq!(l.inlines.len(), usize::from(letter.is_none()));
// Distance along the line: across the page, or down the column.
let along = |a: Point, b: Point| if vertical { (b.y - a.y).abs() } else { (b.x - a.x).abs() };
let (previous, middle, next) = (ink(0, true), ink(1, false), ink(2, false));
(along(previous, middle), along(middle, next), l.glyphs.iter().map(|g| (g.origin, g.advance)).collect::<Vec<_>>())
};
for (before, letter, after, vertical) in [("A", "X", "B", false), ("אב", "ג", "דה", false), ("ab", "x", "cd", true)] {
for letter in [None, Some(letter)] {
let (before_plain, after_plain, plain) = set(before, letter, after, Some(0.0), vertical);
let (before_kerned, after_kerned, _) = set(before, letter, after, Some(200.0), vertical);
// 200/1000 em of 20 pt.
assert!((after_kerned - after_plain - 4.0).abs() < 1e-6, "{before:?} {letter:?}: {after_plain} -> {after_kerned}");
assert!((before_kerned - before_plain).abs() < 1e-6, "{before:?} {letter:?}: {before_plain} -> {before_kerned}");
if letter.is_some() {
// A letter's 0 also turns off its font's kerning pairs; a graphic has none.
continue;
}
let (_, _, none) = set(before, letter, after, None, vertical);
assert!(
plain.iter().zip(&none).all(|(a, b)| (a.0 - b.0).hypot() < 1e-9 && (a.1 - b.1).abs() < 1e-9),
"{before:?} {letter:?}: 0 sets the text as no value does"
);
}
}
}
/// Character Alignment: an inline graphic's em box is its run's, whatever its scale. It moves with
/// its run's text when that is smaller than the line's largest characters, and a big graphic in
/// small text doesn't make its line's small characters move.
File diff suppressed because one or more lines are too long