From fa2de347c4c4457f19c19ee8e5fcbc05634f084a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=B3=D0=B4=D0=B0=20=D0=9C=2E=D0=93=2E?= Date: Mon, 12 Oct 2026 16:57:59 +0300 Subject: [PATCH] Text: exact line spacing ends at the last line's descent With exact line spacing (a:lnSpc/a:spcPts) PowerPoint puts the baseline at 80 % of the line pitch and ends the text at the last line's descent, not a full pitch below it. We drew every line a full pitch tall, so table rows whose cells use exact spacing grew taller than PowerPoint's (35.6 pt instead of 33 pt for 12 pt type at 26.59 pt), pushing tables and text boxes down and tripping shrink-on-overflow early. Measured on PowerPoint's own PDF of a real deck: rows now come out at 33.5 pt against PowerPoint's 32.9-33.8 pt. --- crates/text/src/lib.rs | 11 +++++++++-- crates/text/src/tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 225f4afb..349a4016 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -879,6 +879,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts let mut numbering: Vec = vec![0; 9]; let mut all_lines: Vec<(LineInfo, LinePlace, f64, Option<(String, f64, f64)>)> = vec![]; let mut prev_after = 0.0; + // How far the last line's full pitch reaches past the text (exact spacing; see below). + let mut tail = 0.0; let mut shaped_paras: Vec = Vec::with_capacity(body.paragraphs.len()); for (pi, para) in body.paragraphs.iter().enumerate() { let lvl = para.level.min(8) as usize; @@ -947,7 +949,11 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts }; let ratio = if natural > 0.0 { lh / natural } else { 1.0 }; let top = y; - let baseline = top + asc * ratio; + // Exact spacing (`spcPts`), as PowerPoint sets it: the baseline sits at 80 % of the + // line pitch, and the text ends at the last line's descent rather than a full pitch. + let exact = matches!(props.line_spacing, Some(Spacing::Pts(_))); + let baseline = if exact { top + lh * 0.8 } else { top + asc * ratio }; + tail = if exact { (lh * 0.2 - desc).max(-lh) } else { 0.0 }; y += lh; let x0 = if li == 0 { first_x } else { rest_x }; let avail = (col_w - x0 - mr).max(0.0); @@ -1084,7 +1090,8 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts out.para_lines.push((first_line_idx, raw.len())); prev_after = spacing_pts(props.space_after, p.lead_size, reduce).max(0.0); } - let total = y; + // The text ends at the last line's descent under exact spacing. + let total = (y - tail).max(0.0); out.content_height = total; // Columns: distribute lines by height. let col_h = if ncols > 1 { inner.height() } else { f64::INFINITY }; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index f3d5db5d..c524b43e 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -54,6 +54,33 @@ fn shrink_on_overflow() { assert!(!l.overflow || l.font_scale <= 0.11); } +#[test] +fn exact_line_spacing_ends_at_the_last_descent() { + // 12 pt text at exactly 26.59 pt per line (a table cell PowerPoint sizes to 24.3 pt of text): + // the baseline at 80 % of the pitch, the text ending at the last line's descent. + let (p, mut sh) = setup("One\nTwo", 600.0, 300.0); + for para in &mut sh.text.as_mut().unwrap().paragraphs { + para.props.line_spacing = Some(Spacing::Pts(26.59)); + para.props.bullet = Some(Bullet::None); + for r in &mut para.runs { + r.props.size = Some(12.0); + } + } + let l = lay(&p, &sh, 600.0, 300.0); + let first = &l.lines[0]; + assert!((first.baseline - first.top - 26.59 * 0.8).abs() < 0.01, "{:?}", first.baseline - first.top); + // The text ends at the last line's descent: above its full pitch by 20 % of it less the + // descent (~2.3 pt for 12 pt type). + let short = (l.lines[1].bottom - l.lines[0].top) - l.content_height; + assert!(short > 1.0 && short < 26.59 * 0.2, "{short}"); + // Proportional spacing keeps full pitches. + for para in &mut sh.text.as_mut().unwrap().paragraphs { + para.props.line_spacing = Some(Spacing::Pct(1.0)); + } + let l = lay(&p, &sh, 600.0, 300.0); + assert!((l.content_height - (l.lines[1].bottom - l.lines[0].top)).abs() < 0.01); +} + #[test] fn caret_and_hit_roundtrip() { let (p, sh) = setup("Hello world", 600.0, 100.0);