Text: a fallback face (emoji, symbols, a missing script) no longer moves the line's baseline

Line height and baseline were the largest over every face on the line, fallback faces included,
so a line with an emoji, a math symbol or Thai in an Aptos run sat 1-5 px lower than PowerPoint's
and pushed the lines below it down. PowerPoint keeps the run's own font metrics (measured: the
second line of six boxes starts at the same height whatever the first line holds). Faces that are
the run's Latin, East Asian, complex-script or symbol font still count.

w14-xml s8 (baselines) 5.54 -> 3.21, w09-scripts s1 10.27 -> 8.39, s2 9.37 -> 7.46,
w15-mixed s8 13.65 -> 8.77; w15-mixed s2 (Arabic) 6.57 -> 7.05.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 06:07:56 -04:00
parent 232501a9f3
commit 85a7f0ce3e
2 changed files with 21 additions and 1 deletions

View File

@ -1684,7 +1684,11 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
for c in p.cells.get(rl.start..rl.end.max(rl.start)).unwrap_or(&[]) {
if let (Some(st), Some(face)) = (p.styles.get(c.style), p.faces.get(c.face)) {
let sz = st.size;
let (a, d) = line_metrics(face, sz);
// The run's own fonts set the line; a fallback face (emoji, a symbol or a
// script the run's fonts lack) does not move the baseline in PowerPoint.
let own = Arc::ptr_eq(face, &st.face)
|| [st.ea.as_deref(), st.cs.as_deref(), st.sym.as_deref()].into_iter().flatten().any(|f| face.family.eq_ignore_ascii_case(f));
let (a, d) = line_metrics(if own { face } else { &st.face }, sz);
asc = asc.max(a);
desc = desc.max(d);
size = size.max(sz);

View File

@ -1134,3 +1134,19 @@ fn symbols_after_an_emoji_keep_a_text_face() {
}
}
}
#[test]
fn fallback_faces_do_not_move_the_baseline() {
// PowerPoint keeps a line's baseline and pitch from the run's own font when an emoji, a symbol
// or a script the font lacks is drawn from another face.
let base = |text: &str| {
let (p, sh) = bidi_setup(vec![Run::new(text)], 2000.0, false, Align::Left);
let l = lay(&p, &sh, 2000.0, 300.0);
(l.lines[0].baseline, l.lines[0].bottom)
};
let plain = base("Hxg Hxg");
for text in ["Hxg \u{1F600} Hxg", "Hxg \u{0E2A}\u{0E27}\u{0E31}\u{0E2A}", "Hxg \u{222B}\u{2211}"] {
let b = base(text);
assert!((b.0 - plain.0).abs() < 0.01 && (b.1 - plain.1).abs() < 0.01, "{text:?}: {b:?} vs {plain:?}");
}
}