From 30b2daadb65057862f4b24b70933bbb532ca4a76 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 06:10:30 -0400 Subject: [PATCH] Text: character spacing leaves kerning on again (only Arial drops it in PowerPoint) A wider probe (Arial, Aptos, Calibri, Segoe UI, Times New Roman, Verdana at 40 pt, each box with one paragraph spaced 2 pt) shows the earlier rule holds for Arial alone: Calibri, Segoe UI and Times New Roman keep "AVATAR" kerned beside and inside spaced runs (121 vs 131 px for Calibri). The rule made spaced Aptos and Calibri text too wide (w15-mixed s9 5.46 -> 7.86), so it is removed; Arial's behaviour stays a known difference. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 41 +++++++--------------------------------- crates/text/src/tests.rs | 11 ++++++----- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 6354d17..63c8808 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -694,17 +694,7 @@ pub fn autonum(scheme: &str, n: u32) -> String { } #[allow(clippy::too_many_arguments)] -fn shape_para( - ctx: &Ctx, - shape: &Shape, - body: &TextBody, - pi: usize, - scale: f64, - opts: &Opts, - number: Option, - no_kern: bool, - vertical: Upright, -) -> ParaShaped { +fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, opts: &Opts, number: Option, vertical: Upright) -> ParaShaped { let db = FontDb::global(); let Some(para) = body.paragraphs.get(pi) else { let face = db.face("Inter", "Regular"); @@ -729,8 +719,7 @@ fn shape_para( let mut char_style: Vec = vec![]; for run in ¶.runs { let rp = resolve::run(ctx, shape, para, &run.props); - let mut st = make_style(ctx, &rp, scale, opts.prompt_color); - st.kern &= !no_kern; + let st = make_style(ctx, &rp, scale, opts.prompt_color); let t: String = match &run.kind { RunKind::Text => run.text.clone(), RunKind::Break => "\u{b}".into(), @@ -1549,8 +1538,7 @@ fn tab_advance(x: f64, stops: &[Tab], default_tab: f64, seg: (f64, f64)) -> f64 pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLayout { let bp = resolve::body(ctx, shape); let autofit = bp.autofit.unwrap_or(AutoFit::None); - let no_kern = spacing_stops_kerning(ctx, shape, body); - let mut l = layout_scaled(ctx, shape, body, &bp, opts, 1.0, 0.0, no_kern); + let mut l = layout_scaled(ctx, shape, body, &bp, opts, 1.0, 0.0); if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink) && l.overflow { @@ -1560,7 +1548,7 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay for _ in 0..12 { let mid = (lo + hi) / 2.0; let red: f64 = ((1.0 - mid) * 0.8_f64).min(0.2); - let t = layout_scaled(ctx, shape, body, &bp, opts, mid, red, no_kern); + let t = layout_scaled(ctx, shape, body, &bp, opts, mid, red); if t.overflow { hi = mid; } else { @@ -1568,28 +1556,13 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay best = Some(t); } } - l = best.unwrap_or_else(|| layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2, no_kern)); + l = best.unwrap_or_else(|| layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2)); } l } -/// Does character spacing switch pair kerning off in this body? PowerPoint stops kerning every run -/// of a text body once one of its runs is spaced out by 0.6 pt or more (measured on Arial at 40 pt: -/// 0.5 pt keeps the pairs kerned, 0.6, 0.8, 1 and 1.5 pt drop them for the whole box, even in -/// paragraphs without spacing). Condensed spacing mostly keeps them (-0.6, -1, -1.5, -2 and -3 pt; -/// -1.2 and -2.5 pt did not, which no simple rule explains, so condensing never stops kerning -/// here: titles condensed by 1 pt are common and kerned). -fn spacing_stops_kerning(ctx: &Ctx, shape: &Shape, body: &TextBody) -> bool { - body.paragraphs.iter().any(|para| { - para.runs.iter().any(|run| { - let s = resolve::run(ctx, shape, para, &run.props).spacing.unwrap_or(0.0); - s >= 0.55 - }) - }) -} - #[allow(clippy::too_many_arguments)] -fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts: &Opts, scale: f64, reduce: f64, no_kern: bool) -> TextLayout { +fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts: &Opts, scale: f64, reduce: f64) -> TextLayout { let r = opts.rect; let vert = bp.vert.unwrap_or(TextDir::Horizontal); // East Asian vertical text: CJK stands upright in text turned a quarter turn; stacked text @@ -1652,7 +1625,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts } None }; - shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number, no_kern, upright_text)); + shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number, upright_text)); } for (pi, p) in shaped_paras.iter().enumerate() { let props = &p.props; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 0b2c6cc..bbcc808 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -122,7 +122,7 @@ fn kerned_width_beside_spacing(family: &str, spc: f64) -> f64 { } #[test] -fn character_spacing_in_the_body_stops_kerning() { +fn character_spacing_in_the_body_keeps_kerning() { let kerns = |f: &FontFace| { let w = |feat: &[deckcraft_fonts::Feature]| deckcraft_fonts::shape(f, "AVATAR", feat, |c| c).iter().map(|g| g.x_advance).sum::(); w(&[]) != w(&deckcraft_fonts::feature("-kern").into_iter().collect::>()) @@ -133,11 +133,12 @@ fn character_spacing_in_the_body_stops_kerning() { }; let (_, off) = glyphs_and_width(&family, "AVATAR WAVE", Some(0.0)); let (_, on) = glyphs_and_width(&family, "AVATAR WAVE", Some(12.0)); - // Small or condensed spacing elsewhere keeps the pairs; 0.6 pt anywhere in the box drops them. - for (spc, kerned) in [(0.0, true), (0.5, true), (-0.99, true), (-1.0, true), (-2.0, true), (0.6, false), (1.0, false), (3.0, false)] { + assert!(on < off); + // Spacing elsewhere in the box leaves the pairs kerned (Calibri, Segoe UI, Times New Roman in + // PowerPoint; only Arial drops them). + for spc in [0.0, 0.5, -1.0, 0.6, 2.0] { let w = kerned_width_beside_spacing(&family, spc); - let want = if kerned { on } else { off }; - assert!((w - want).abs() < 0.01, "{family} spc {spc}: {w} vs {want}"); + assert!((w - on).abs() < 0.01, "{family} spc {spc}: {w} vs {on}"); } }