diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index decc7bf..5425c02 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -137,6 +137,9 @@ pub struct FontFace { /// Underline position (top edge, below the baseline) and thickness in font units (`post` /// table; 0.1 and 0.05 em when the font has none). PowerPoint draws single underlines there. pub underline: (f64, f64), + /// Strikethrough position (top edge, above the baseline) and thickness in font units (OS/2; + /// 0.26 and 0.05 em when the font has none). PowerPoint draws single strikethroughs there. + pub strikeout: (f64, f64), pub shaper: harfrust::ShaperData, /// Variable fonts: the named instance's axis settings (user units; empty = default instance), /// the normalised location and the shaper's view of it. @@ -590,6 +593,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords x_height: m.x_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.5), line_split: line_split(&f, m.ascent as f64, -(m.descent as f64), m.units_per_em.max(1) as f64), underline: underline_metrics(m.underline, m.units_per_em.max(1) as f64), + strikeout: strikeout_metrics(m.strikeout, m.units_per_em.max(1) as f64), shaper, coords, location, @@ -613,6 +617,18 @@ fn underline_metrics(d: Option, upem: f64) -> (f64, } } +/// The strikethrough's top edge above the baseline and its thickness, font units, from OS/2 (its +/// position is the top edge: Arial's 530/2048 is where PowerPoint's strikethrough starts, 25 px +/// above the baseline at 96 pt); fonts without a usable one get 0.26 em and 0.05 em. +fn strikeout_metrics(d: Option, upem: f64) -> (f64, f64) { + match d { + Some(d) if d.thickness > 0.0 && (d.thickness as f64) < upem * 0.3 && d.offset > 0.0 && (d.offset as f64) < upem => { + (d.offset as f64, d.thickness as f64) + } + _ => (upem * 0.26, upem * 0.05), + } +} + /// The named style part of a style with axis settings (`Bold {wght:650}` → `Bold`). pub fn base_style(style: &str) -> &str { style.split('{').next().unwrap_or(style).trim() diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 63c8808..125be16 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1999,8 +1999,10 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts } } if let Some(sk) = st.strike { - let t = (st.size * 0.05).max(0.5); - let sy = line.baseline - st.size * 0.3; + // At the strikeout position and thickness of the run's own font. + let k = st.size / st.face.upem.max(1.0); + let t = (st.face.strikeout.1 * k).max(0.5); + let sy = line.baseline - st.face.strikeout.0 * k; out.decos.push(Deco { rect: Rect::new(cx, sy, cx1, sy + t), color: st.color, diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index bbcc808..5276b7f 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1151,3 +1151,18 @@ fn fallback_faces_do_not_move_the_baseline() { assert!((b.0 - plain.0).abs() < 0.01 && (b.1 - plain.1).abs() < 0.01, "{text:?}: {b:?} vs {plain:?}"); } } + +#[test] +fn strikethrough_sits_at_the_font_strikeout_position() { + let (p, mut sh) = setup("Struck", 2000.0, 300.0); + let para = &mut sh.text.as_mut().unwrap().paragraphs[0]; + para.props.bullet = Some(Bullet::None); + para.runs[0].props.size = Some(96.0); + para.runs[0].props.strike = Some(Strike::Single); + let l = lay(&p, &sh, 2000.0, 300.0); + let r = l.runs.iter().find(|r| !r.cells.is_empty()).unwrap(); + let k = 96.0 / r.face.upem; + let d = l.decos.iter().find(|d| !d.behind).expect("a strikethrough"); + assert!((l.lines[0].baseline - d.rect.y0 - r.face.strikeout.0 * k).abs() < 0.01, "top at the strikeout position"); + assert!((d.rect.height() - (r.face.strikeout.1 * k).max(0.5)).abs() < 0.01); +}