mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 03:06:56 -06:00
Layout: Em Box Height first baseline offset
The first baseline sits the em box top below the frame's top inset: the font's BASE em box (idtp, else ideo + 1 em), else 0.88 em, for the largest size on the first line. IDML FirstBaselineOffset="EmboxHeight" imports and exports as itself; the footnote options list it and the frame and footnote option commands take emboxHeight.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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
|
||||
),
|
||||
|
||||
@@ -489,7 +489,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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
|
||||
),
|
||||
|
||||
@@ -122,7 +122,7 @@ pub struct FontFace {
|
||||
/// Basic Multilingual Plane coverage bitset, built on first use.
|
||||
bmp: std::sync::OnceLock<Box<[u64]>>,
|
||||
/// 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<String>)>,
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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#"<TextFramePreference FirstBaselineOffset="EmboxHeight"/>"#, "");
|
||||
assert_eq!(first(&d), FirstBaseline::EmboxHeight);
|
||||
assert_eq!(first(&import_idml(&export_idml(&d)).unwrap()), FirstBaseline::EmboxHeight);
|
||||
let unknown = inset_fixture(r#"<TextFramePreference FirstBaselineOffset="SomethingElse"/>"#, "");
|
||||
assert_eq!(first(&unknown), FirstBaseline::Ascent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inset_property_keeps_precedence_over_attribute() {
|
||||
for (property, expected) in [
|
||||
|
||||
@@ -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:"));
|
||||
|
||||
@@ -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", "页面"]),
|
||||
|
||||
@@ -296,6 +296,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Ascent", "الصعود"),
|
||||
("Cap Height", "ارتفاع الأحرف الكبيرة"),
|
||||
("x Height", "ارتفاع x"),
|
||||
("Em Box Height", "ارتفاع مربع em"),
|
||||
("Fixed", "ثابت"),
|
||||
("Never (continuous)", "أبدًا (ترقيم متصل)"),
|
||||
("Spread", "صفحتان متقابلتان"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -960,6 +960,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cap Height", "大文字の高さ"),
|
||||
("Leading", "行送り"),
|
||||
("x Height", "x の高さ"),
|
||||
("Em Box Height", "仮想ボディの高さ"),
|
||||
("Fixed", "固定"),
|
||||
("Never (continuous)", "振り直しなし (通し番号)"),
|
||||
("Page", "ページ"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -958,6 +958,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cap Height", "Висота великих літер"),
|
||||
("Leading", "Міжрядковий інтервал"),
|
||||
("x Height", "Висота малих літер"),
|
||||
("Em Box Height", "Висота em-квадрата"),
|
||||
("Fixed", "Фіксований"),
|
||||
("Never (continuous)", "Ніколи (наскрізна нумерація)"),
|
||||
("Page", "Сторінка"),
|
||||
|
||||
Reference in New Issue
Block a user