diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index a5eca2342..643ae68cd 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -2601,14 +2601,16 @@ fn line_metrics( let reference = src.iter().max_by(|a, b| a.size.total_cmp(&b.size)); let mut own_text = false; for g in src { - let shift = g.shift + reference.map_or(0.0, |r| cjk_alignment_shift(g, r)); + let align = reference.map_or(0.0, |r| cjk_alignment_shift(g, r)); + let shift = g.shift + align; let up = shift.max(0.0); asc = asc.max(g.ascent + up); // A list's bullet or number doesn't raise the Ascent offset: only the line's own text does. if !g.list_label { own_text = true; tops.typo_ascent = tops.typo_ascent.max(g.typo_ascent + up); - tops.em = tops.em.max(em_box_top(&g.face, g.size) + up); + // The em box sits at the unshifted baseline: a baseline shift moves only the drawn glyphs. + tops.em = tops.em.max(em_box_top(&g.face, g.size) + align.max(0.0)); } tops.cap = tops.cap.max(g.cap + up); tops.xh = tops.xh.max(g.xh + up); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 631bb639f..3304c1208 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -2451,6 +2451,32 @@ fn em_box_first_baseline_in_a_latin_font_with_a_one_em_typo_box_follows_the_cap_ assert!((first - expected).abs() < 0.01, "{first}, expected {expected}"); } +#[test] +fn em_box_first_baseline_ignores_baseline_shift() { + use designcraft_doc::FirstBaseline; + let db = designcraft_fonts::FontDb::global(); + // Source Sans 3's `BASE` em box tops 0.83 em above the baseline. + db.add_font(test_font("EmBoxShft 830", Some(&[]))); + let (mut d, sid, fid) = doc_with("Hxg Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default()); + let st = d.story_mut(sid).unwrap(); + st.format_chars(0..7, |f| { + f.over.font_family = Some("EmBoxShft 830".into()); + f.over.font_style = Some("Regular".into()); + f.over.size = Some(10.0); + }); + st.format_chars(4..7, |f| f.over.baseline_shift = Some(1.0)); + d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.first_baseline = FirstBaseline::EmboxHeight; + let frames = compose_story(&d, sid, &ComposeOptions::default()).frames; + let line = &frames[0].lines[0]; + // The unshifted run's baseline stays 8.3 pt below the frame top; the shifted run is drawn 1 pt + // higher. + assert!((line.baseline - 36.0 - 8.3).abs() < 0.01, "{}", line.baseline); + let ys: Vec = line.glyphs.iter().map(|g| g.y).collect(); + assert_eq!(ys.len(), 7, "{ys:?}"); + assert!(ys[..3].iter().all(|y| y.abs() < 0.01), "{ys:?}"); + assert!(ys[4..].iter().all(|y| (y + 1.0).abs() < 0.01), "{ys:?}"); +} + #[test] fn ascent_first_baseline_falls_back_to_the_ascent_without_a_usable_typo_ascender() { let db = designcraft_fonts::FontDb::global();