diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 7e2163ddc..2b780fcab 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -939,7 +939,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let col = cur.split_rect(col); // Estimate slots for the breaker with the paragraph's base leading. let est_asc = base_chars.size * 0.75; - let est_tops = Tops { typo_ascent: est_asc, cap: est_asc * 0.72, xh: est_asc * 0.5 }; + let est_tops = Tops { typo_ascent: est_asc, cap: est_asc * 0.72, xh: est_asc * 0.5, em: base_chars.size * 0.88 }; let est_first = cur.next_baseline(f, col, base_leading, est_asc, est_tops, &pp); let est_jump = cur.jump(f, base_leading, est_asc, est_tops); let slots = estimate_slots(f, col, est_first, est_jump, base_leading, base_chars.size, &glyphs[g0..], &pp, line_no); @@ -2093,25 +2093,34 @@ impl Cursor { fn first_baseline_offset(f: &FrameSpec, lead: f64, asc: f64, tops: Tops) -> f64 { let off = match f.opts.first_baseline { // Vertical frames measure from the ascent. - FirstBaseline::Ascent if f.vertical => asc, + FirstBaseline::Ascent | FirstBaseline::EmboxHeight if f.vertical => asc, FirstBaseline::CapHeight if f.vertical => asc * 0.72, FirstBaseline::XHeight if f.vertical => asc * 0.5, FirstBaseline::Ascent => tops.typo_ascent, FirstBaseline::CapHeight => tops.cap, FirstBaseline::XHeight => tops.xh, + FirstBaseline::EmboxHeight => tops.em, FirstBaseline::Leading => lead, FirstBaseline::Fixed => 0.0, }; off.max(f.opts.first_baseline_min) } -/// A line's tallest typographic ascender, cap height and x height above its baseline: the -/// first baseline offsets. +/// A line's tallest typographic ascender, cap height, x height and em box top above its baseline: +/// the first baseline offsets. #[derive(Clone, Copy, Default)] struct Tops { typo_ascent: f64, cap: f64, xh: f64, + em: f64, +} + +/// The em box top above the baseline at `size`: the font's `BASE` em box, else the default +/// ideographic em box, 0.88 em above the baseline. +fn em_box_top(face: &designcraft_fonts::FontFace, size: f64) -> f64 { + let em = face.declared_em_box().filter(|_| face.upem > 0.0).map_or(0.88, |(top, _)| top / face.upem); + em * size } /// Em box relative to the baseline. Font ascender/descender proportions locate it; @@ -2160,7 +2169,7 @@ fn line_metrics( if src.is_empty() { let face = face(); let k = base_size / face.upem; - let tops = Tops { typo_ascent: face.typo_ascent * k, cap: face.cap_height * k, xh: face.x_height * k }; + let tops = Tops { typo_ascent: face.typo_ascent * k, cap: face.cap_height * k, xh: face.x_height * k, em: em_box_top(&face, base_size) }; return (face.ascent * k, tops, face.descent * k, base_leading); } let mut asc: f64 = 0.0; @@ -2177,6 +2186,7 @@ fn line_metrics( 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); } tops.cap = tops.cap.max(g.cap + up); tops.xh = tops.xh.max(g.xh + up); @@ -2187,6 +2197,7 @@ fn line_metrics( // Only a label (an empty list paragraph): as an empty paragraph. let face = face(); tops.typo_ascent = face.typo_ascent * base_size / face.upem; + tops.em = em_box_top(&face, base_size); } (asc, tops, desc, lead) } diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 302374d8e..9a686701a 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1695,6 +1695,47 @@ fn ascent_first_baseline_uses_the_typographic_ascender() { assert!(close(first(&d), top + 16.0), "{}", first(&d)); } +#[test] +fn em_box_first_baseline_uses_the_base_em_box_else_0_88_em() { + use designcraft_doc::FirstBaseline; + let db = designcraft_fonts::FontDb::global(); + // The test font's `BASE` table puts the em box bottom (`ideo`) at -170 units: its top is 0.83 em. + let mut no_base = typo_test_font("EmBoxTop 0700", Some(700)); + let tables = u16::from_be_bytes([no_base[4], no_base[5]]) as usize; + let rec = (0..tables).map(|t| 12 + 16 * t).find(|&r| &no_base[r..r + 4] == b"BASE").unwrap(); + // Still sorted between its neighbours, so the table directory stays valid. + no_base[rec..rec + 4].copy_from_slice(b"BASX"); + db.add_font(no_base); + db.add_font(typo_test_font("EmBoxBase 700", Some(700))); + assert_eq!(db.face("EmBoxTop 0700", "Regular").declared_em_box(), None); + assert_eq!(db.face("EmBoxBase 700", "Regular").declared_em_box(), Some((830.0, -170.0))); + let (mut d, sid, fid) = doc_with("Hxg Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default()); + let mut first = |family: &str, sizes: [f64; 2], kind: FirstBaseline| { + let st = d.story_mut(sid).unwrap(); + for (range, size) in [(0..4, sizes[0]), (4..7, sizes[1])] { + st.format_chars(range, |f| { + f.over.font_family = Some(family.into()); + f.over.font_style = Some("Regular".into()); + f.over.size = Some(size); + }); + } + d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.first_baseline = kind; + compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0].baseline - 36.0 + }; + let close = |a: f64, b: f64| (a - b).abs() < 0.01; + // Ascent: the 0.7 em typographic ascender; Em Box Height: the 0.88 em default em box top. + let asc = first("EmBoxTop 0700", [10.0; 2], FirstBaseline::Ascent); + assert!(close(asc, 7.0), "{asc}"); + let emb = first("EmBoxTop 0700", [10.0; 2], FirstBaseline::EmboxHeight); + assert!(close(emb, 8.8), "{emb}"); + // A mixed first line takes its largest size. + let emb = first("EmBoxTop 0700", [10.0, 20.0], FirstBaseline::EmboxHeight); + assert!(close(emb, 17.6), "{emb}"); + // A font's `BASE` em box wins over the default. + let emb = first("EmBoxBase 700", [10.0; 2], FirstBaseline::EmboxHeight); + assert!(close(emb, 8.3), "{emb}"); +} + #[test] fn ascent_first_baseline_falls_back_to_the_ascent_without_a_usable_typo_ascender() { let db = designcraft_fonts::FontDb::global(); diff --git a/crates/doc/src/item.rs b/crates/doc/src/item.rs index 9b191a094..7faf6e435 100644 --- a/crates/doc/src/item.rs +++ b/crates/doc/src/item.rs @@ -281,6 +281,8 @@ pub enum FirstBaseline { Leading, XHeight, Fixed, + /// The top of the em box: the font's `BASE` em box, else 0.88 em above the baseline. + EmboxHeight, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/engine/src/cmd/notes.rs b/crates/engine/src/cmd/notes.rs index 4d7b0f13f..1ef8d9954 100644 --- a/crates/engine/src/cmd/notes.rs +++ b/crates/engine/src/cmd/notes.rs @@ -22,7 +22,7 @@ pub fn specs() -> Vec { "Document Footnote Options…", ["Type"], None, - "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|symbols, startAt?, restart?: never|page|spread|section, prefix?, suffix?, affixIn?: none|reference|text|both, refPosition?: superscript|subscript|normal|otSuperscript, refCharStyle?, paraStyle?, separator?, spaceBefore?, spaceBetween?, firstBaseline?, firstBaselineMin?, spanColumns?, rule?: {on, weight, color, tint, width, offset, leftIndent}} → the options (no params: just read them)", + "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|symbols, startAt?, restart?: never|page|spread|section, prefix?, suffix?, affixIn?: none|reference|text|both, refPosition?: superscript|subscript|normal|otSuperscript, refCharStyle?, paraStyle?, separator?, spaceBefore?, spaceBetween?, firstBaseline?: ascent|capHeight|leading|xHeight|emboxHeight|fixed, firstBaselineMin?, spanColumns?, rule?: {on, weight, color, tint, width, offset, leftIndent}} → the options (no params: just read them)", has_doc, options ), diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 17c44c27a..3d4dd9beb 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -489,7 +489,7 @@ pub fn specs() -> Vec { "Text Frame Options…", ["Object"], Some("Cmd+B"), - "{columns?, gutter?, inset?: number|[t,l,b,r] (null keeps that side), verticalJustification?: top|center|bottom|justify, firstBaseline?, autoSize?, ignoreWrap?, balanceColumns?, columnRule?: bool, columnRuleWeight? (pt), columnRuleColor? (swatch name), columnRuleTint? (0..1), columnRuleOffset? (pt, horizontal), columnRuleTopInset? (pt), columnRuleBottomInset? (pt), vertical?: bool (sets the story direction of the frames' stories, as Type ▸ Story Direction), ids?}", + "{columns?, gutter?, inset?: number|[t,l,b,r] (null keeps that side), verticalJustification?: top|center|bottom|justify, firstBaseline?: ascent|capHeight|leading|xHeight|emboxHeight|fixed, autoSize?, ignoreWrap?, balanceColumns?, columnRule?: bool, columnRuleWeight? (pt), columnRuleColor? (swatch name), columnRuleTint? (0..1), columnRuleOffset? (pt, horizontal), columnRuleTopInset? (pt), columnRuleBottomInset? (pt), vertical?: bool (sets the story direction of the frames' stories, as Type ▸ Story Direction), ids?}", has_selection, text_frame_options ), diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index d42f76f91..a47118c61 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -122,7 +122,7 @@ pub struct FontFace { /// 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)>, + 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)>, } @@ -267,7 +267,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) in font units, y up, when the font declares one in `BASE`. + pub fn declared_em_box(&self) -> Option<(f64, f64)> { + let (b, declared) = *self.em.get_or_init(|| crate::vertical::em_box(self)); + declared.then_some(b) } /// 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..c6c65d3d2 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 is true for an em box from `BASE`. +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; @@ -71,7 +72,7 @@ pub(crate) fn em_box(face: &FontFace) -> (f64, f64) { 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)) + .map_or(((upem * 0.88, upem * -0.12), false), |b| (b, false)) } /// The em box from the `BASE` table's horizontal axis: the Han script's values, else the default diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index cdf8294ce..5a47f194b 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -424,10 +424,11 @@ pub fn first_baseline_out(f: FirstBaseline) -> &'static str { FirstBaseline::Leading => "LeadingOffset", FirstBaseline::XHeight => "XHeight", FirstBaseline::Fixed => "FixedHeight", + FirstBaseline::EmboxHeight => "EmboxHeight", } } pub fn first_baseline_in(s: &str) -> FirstBaseline { - [FirstBaseline::CapHeight, FirstBaseline::Leading, FirstBaseline::XHeight, FirstBaseline::Fixed] + [FirstBaseline::CapHeight, FirstBaseline::Leading, FirstBaseline::XHeight, FirstBaseline::Fixed, FirstBaseline::EmboxHeight] .into_iter() .find(|m| first_baseline_out(*m) == s) .unwrap_or(FirstBaseline::Ascent) diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 630256579..bfc71fd0b 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -378,6 +378,17 @@ fn imports_scalar_inset_properties_like_lists_and_attributes() { assert_eq!(frame_inset(&round_trip), [9.0; 4]); } +#[test] +fn em_box_first_baseline_offset_round_trips() { + use designcraft_doc::FirstBaseline; + let first = |d: &Document| d.spreads[0].items[0].text_frame().unwrap().options.first_baseline; + let d = inset_fixture(r#""#, ""); + assert_eq!(first(&d), FirstBaseline::EmboxHeight); + assert_eq!(first(&import_idml(&export_idml(&d)).unwrap()), FirstBaseline::EmboxHeight); + let unknown = inset_fixture(r#""#, ""); + assert_eq!(first(&unknown), FirstBaseline::Ascent); +} + #[test] fn inset_property_keeps_precedence_over_attribute() { for (property, expected) in [ diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2c9438c06..aad3c0ab3 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3373,7 +3373,14 @@ fn footnote_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ui, d, "firstBaseline", - &[("ascent", "Ascent"), ("capHeight", "Cap Height"), ("leading", "Leading"), ("xHeight", "x Height"), ("fixed", "Fixed")], + &[ + ("ascent", "Ascent"), + ("capHeight", "Cap Height"), + ("leading", "Leading"), + ("xHeight", "x Height"), + ("emboxHeight", "Em Box Height"), + ("fixed", "Fixed"), + ], ); ui.end_row(); crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Min:")); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 7decaa685..5a0ad3975 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1941,6 +1941,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cap Height", ["Cap Height", "Cap Height", "Cap Height", "Cap Height", "大写字母高度"]), ("Leading", ["Leading", "Leading", "Leading", "Leading", "行距"]), ("x Height", ["x Height", "x Height", "x Height", "x Height", "x 字高"]), + ("Em Box Height", ["Em Box Height", "Em Box Height", "Em Box Height", "Em Box Height", "全角字框高度"]), ("Fixed", ["Fixed", "Fixed", "Fixed", "Fixed", "固定"]), ("Never (continuous)", ["Never (continuous)", "Never (continuous)", "Never (continuous)", "Never (continuous)", "从不(连续编号)"]), ("Page", ["Page", "Page", "Page", "Page", "页面"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 82623db73..78a0d48c9 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -296,6 +296,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Ascent", "الصعود"), ("Cap Height", "ارتفاع الأحرف الكبيرة"), ("x Height", "ارتفاع x"), + ("Em Box Height", "ارتفاع مربع em"), ("Fixed", "ثابت"), ("Never (continuous)", "أبدًا (ترقيم متصل)"), ("Spread", "صفحتان متقابلتان"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index c91afd6ba..937f4a06e 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -961,6 +961,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cap Height", "Altezza maiuscole"), ("Leading", "Interlinea"), ("x Height", "Altezza x"), + ("Em Box Height", "Altezza riquadro em"), ("Fixed", "Fisso"), ("Never (continuous)", "Mai (continua)"), ("Page", "Pagina"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index be6e2c4da..705a12391 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -960,6 +960,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cap Height", "大文字の高さ"), ("Leading", "行送り"), ("x Height", "x の高さ"), + ("Em Box Height", "仮想ボディの高さ"), ("Fixed", "固定"), ("Never (continuous)", "振り直しなし (通し番号)"), ("Page", "ページ"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index d25717dc6..785fc1343 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -297,6 +297,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Ascent", "Ascendente"), ("Cap Height", "Altura da caixa alta"), ("x Height", "Altura do x"), + ("Em Box Height", "Altura da caixa eme"), ("Fixed", "Fixo"), ("Never (continuous)", "Nunca (contínuo)"), ("Spread", "Página dupla"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 9a9fb89ba..62d16200d 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -958,6 +958,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cap Height", "Висота великих літер"), ("Leading", "Міжрядковий інтервал"), ("x Height", "Висота малих літер"), + ("Em Box Height", "Висота em-квадрата"), ("Fixed", "Фіксований"), ("Never (continuous)", "Ніколи (наскрізна нумерація)"), ("Page", "Сторінка"),