diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 3dd7c25b3..56f21361d 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -2018,10 +2018,20 @@ fn cjk_em_box(g: &Glyph) -> (f64, f64) { let top = -height * g.ascent / (g.ascent + g.descent).max(1e-9); (top, top + height) } +/// Em box that character alignment lines up, relative to the baseline (y down) at the glyph's +/// size: the font's `BASE` em box, else 0.88 em above and 0.12 em below the baseline. A font's +/// ascender/descender proportions put a Latin em box's centre too low for this. +fn alignment_em_box(g: &Glyph) -> (f64, f64) { + let upem = g.face.units_per_em(); + let (top, bottom) = g.face.base_em_box().unwrap_or((upem * 0.88, upem * -0.12)); + let k = g.sy.abs(); + (-top * k, -bottom * k) +} +/// Upward shift that puts `g`'s em box (or ICF) edge or centre where the reference glyph's is. fn cjk_alignment_shift(g: &Glyph, reference: &Glyph) -> f64 { use designcraft_doc::cjk::CharacterAlignment as A; - let (t, b) = cjk_em_box(g); - let (rt, rb) = cjk_em_box(reference); + let (t, b) = alignment_em_box(g); + let (rt, rb) = alignment_em_box(reference); match g.character_alignment { A::Baseline => 0.0, A::EmTop => t - rt, diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 0d1fe2d27..7f3630d54 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1490,17 +1490,39 @@ fn cjk_center_leading_measures_em_centers_across_different_sizes() { } #[test] -fn cjk_em_center_alignment_moves_small_characters() { - let (mut d, sid, _) = doc_with("AB", Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default()); - d.story_mut(sid).unwrap().format_chars(0..1, |f| f.over.size = Some(24.0)); - d.story_mut(sid).unwrap().format_chars(1..2, |f| f.over.character_alignment = Some(designcraft_doc::cjk::CharacterAlignment::EmCenter)); - let cs = compose_story(&d, sid, &ComposeOptions::default()); - let line = &cs.frames[0].lines[0]; - let center = |g: &PlacedGlyph| { - let (a, b) = g.face.vertical_metrics(); - g.y + (b - a) / (2.0 * (a + b)) * g.face.units_per_em() * g.sy +fn smaller_cjk_run_aligns_to_the_line_em_box_centre() { + use designcraft_doc::cjk::CharacterAlignment; + let text = "Ab 甲乙 cd"; + let run = "Ab ".len().."Ab 甲乙".len(); + // Em box centre (y down) of a placed glyph: its font's BASE em box, else 0.88/-0.12 em. + let centre = |l: &Line, g: &PlacedGlyph| { + let upem = g.face.units_per_em(); + let (top, bottom) = g.face.base_em_box().unwrap_or((upem * 0.88, upem * -0.12)); + l.baseline + g.y - (top + bottom) / 2.0 * g.sy }; - assert!((center(&line.glyphs[0]) - center(&line.glyphs[1])).abs() < 1e-6); + for alignment in [CharacterAlignment::EmCenter, CharacterAlignment::Baseline] { + let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default()); + d.story_mut(sid).unwrap().format_chars(0..text.len(), |f| { + f.over.size = Some(10.0); + f.over.character_alignment = Some(alignment); + }); + d.story_mut(sid).unwrap().format_chars(run.clone(), |f| f.over.size = Some(6.0)); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let line = &cs.frames[0].lines[0]; + let latin = &line.glyphs[0]; + let cjk: Vec<&PlacedGlyph> = line.glyphs.iter().filter(|g| run.contains(&g.byte)).collect(); + assert_eq!(cjk.len(), 2); + assert!(latin.y.abs() < 1e-9); + for g in cjk { + match alignment { + CharacterAlignment::EmCenter => { + assert!((centre(line, g) - centre(line, latin)).abs() < 0.01, "{} vs {}", centre(line, g), centre(line, latin)); + assert!(g.y < -1.0, "a 6 pt run rises in a 10 pt line: {}", g.y); + } + _ => assert!(g.y.abs() < 1e-9, "Roman baseline keeps the run on the baseline: {}", g.y), + } + } + } } #[test] diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 14441e144..0b87f23a1 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -117,8 +117,8 @@ pub struct FontFace { pub instance: Option, /// Basic Multilingual Plane coverage bitset, built on first use. bmp: std::sync::OnceLock>, - /// The ideographic em box, read on first use. - em: std::sync::OnceLock<(f64, f64)>, + /// The ideographic em box and whether the `BASE` table gave it, read on first use. + em: std::sync::OnceLock<((f64, f64), bool)>, /// The font menu group and native family name, read on first use. group: std::sync::OnceLock<(FontGroup, Option)>, } @@ -263,7 +263,12 @@ impl FontFace { /// -120): `BASE` `idtp`/`ideo`, else the OS/2 typo ascender/descender, else the ascender and /// descender, the last two centred on one em. pub fn em_box(&self) -> (f64, f64) { - *self.em.get_or_init(|| crate::vertical::em_box(self)) + self.em.get_or_init(|| crate::vertical::em_box(self)).0 + } + /// The em box (top, bottom) the font's `BASE` table gives, in font units, y up; None without one. + pub fn base_em_box(&self) -> Option<(f64, f64)> { + let (em, from_base) = *self.em.get_or_init(|| crate::vertical::em_box(self)); + from_base.then_some(em) } /// The font menu group the font is for and, for a CJK group, its family name in that language /// (see [`crate::FamilyInfo`]); read from the font once. diff --git a/crates/fonts/src/vertical.rs b/crates/fonts/src/vertical.rs index d360b6f2b..80f2e9833 100644 --- a/crates/fonts/src/vertical.rs +++ b/crates/fonts/src/vertical.rs @@ -55,11 +55,12 @@ impl<'a> VMetrics<'a> { /// The ideographic em box (top, bottom) in font units, y up: `BASE` `idtp`/`ideo`, else the OS/2 /// typo ascender and descender, else the ascender and descender; the last two centred on one em. -pub(crate) fn em_box(face: &FontFace) -> (f64, f64) { +/// The flag says whether `BASE` gave it. +pub(crate) fn em_box(face: &FontFace) -> ((f64, f64), bool) { let upem = face.upem; let font = face.skrifa(); if let Some(b) = font.as_ref().and_then(|f| base_em_box(f, upem)) { - return b; + return (b, true); } let centred = |asc: f64, desc: f64| { let span = asc - desc; @@ -68,10 +69,12 @@ pub(crate) fn em_box(face: &FontFace) -> (f64, f64) { (top, top - upem) }) }; - font.and_then(|f| f.os2().ok()) + let em = font + .and_then(|f| f.os2().ok()) .and_then(|t| centred(f64::from(t.s_typo_ascender()), f64::from(t.s_typo_descender()))) .or_else(|| centred(face.ascent, -face.descent)) - .unwrap_or((upem * 0.88, upem * -0.12)) + .unwrap_or((upem * 0.88, upem * -0.12)); + (em, false) } /// The em box from the `BASE` table's horizontal axis: the Han script's values, else the default