Text: character spacing of 0.6 pt or more (or -1 pt) switches kerning off for the whole box
PowerPoint kerns pairs (a:rPr/@kern) only while no run of the text body is spaced out by 0.6 pt or more or condensed by 1 pt or more; then no run of that box is kerned, even runs without spacing. Measured on Arial 40 pt "AVATAR": spacing 0, 0.01, 0.5, -0.6 and -0.99 pt keep the pairs (146 px wide), 0.6, 0.8, 1, 1.5 and -1 pt drop them in every paragraph of the box (158 px plus the spacing). DeckCraft kerned spaced titles, making "AVATAR"-like words 12 px too narrow at 40 pt. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cfd4f25545
commit
f3f959cabf
|
|
@ -434,7 +434,8 @@ pub fn autonum(scheme: &str, n: u32) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, opts: &Opts, number: Option<u32>) -> ParaShaped {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, opts: &Opts, number: Option<u32>, no_kern: bool) -> ParaShaped {
|
||||
let db = FontDb::global();
|
||||
let Some(para) = body.paragraphs.get(pi) else {
|
||||
let face = db.face("Inter", "Regular");
|
||||
|
|
@ -459,7 +460,8 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64,
|
|||
let mut char_style: Vec<usize> = vec![];
|
||||
for run in ¶.runs {
|
||||
let rp = resolve::run(ctx, shape, para, &run.props);
|
||||
let st = make_style(ctx, &rp, scale, opts.prompt_color);
|
||||
let mut st = make_style(ctx, &rp, scale, opts.prompt_color);
|
||||
st.kern &= !no_kern;
|
||||
let t: String = match &run.kind {
|
||||
RunKind::Text => run.text.clone(),
|
||||
RunKind::Break => "\u{b}".into(),
|
||||
|
|
@ -1073,7 +1075,8 @@ fn tab_advance(x: f64, stops: &[f64], default_tab: 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 mut l = layout_scaled(ctx, shape, body, &bp, opts, 1.0, 0.0);
|
||||
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);
|
||||
if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink)
|
||||
&& l.overflow
|
||||
{
|
||||
|
|
@ -1083,7 +1086,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);
|
||||
let t = layout_scaled(ctx, shape, body, &bp, opts, mid, red, no_kern);
|
||||
if t.overflow {
|
||||
hi = mid;
|
||||
} else {
|
||||
|
|
@ -1091,12 +1094,26 @@ 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));
|
||||
l = best.unwrap_or_else(|| layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2, no_kern));
|
||||
}
|
||||
l
|
||||
}
|
||||
|
||||
fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts: &Opts, scale: f64, reduce: f64) -> TextLayout {
|
||||
/// 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, or condensed by 1 pt or
|
||||
/// more (measured on Arial at 40 pt: 0.5 pt and -0.99 pt keep the pairs kerned, 0.6 pt and -1 pt
|
||||
/// drop them for the whole box).
|
||||
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 || s <= -0.995
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let r = opts.rect;
|
||||
let vert = bp.vert.unwrap_or(TextDir::Horizontal);
|
||||
let rotation = match vert {
|
||||
|
|
@ -1149,7 +1166,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));
|
||||
shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number, no_kern));
|
||||
}
|
||||
for (pi, p) in shaped_paras.iter().enumerate() {
|
||||
let props = &p.props;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,46 @@ fn pairs_are_kerned_only_from_the_kern_size() {
|
|||
assert!((above - off).abs() < 0.01, "40 pt is below a 48 pt threshold");
|
||||
}
|
||||
|
||||
/// Width of the first paragraph "AVATAR WAVE" (kerned from 12 pt) when a second paragraph's run
|
||||
/// has character spacing `spc`.
|
||||
fn kerned_width_beside_spacing(family: &str, spc: f64) -> f64 {
|
||||
let (p, mut sh) = setup("AVATAR WAVE", 2000.0, 300.0);
|
||||
let t = sh.text.as_mut().unwrap();
|
||||
let mut second = t.paragraphs[0].clone();
|
||||
second.runs[0].props.spacing = Some(spc);
|
||||
t.paragraphs.push(second);
|
||||
for para in &mut t.paragraphs {
|
||||
para.props.bullet = Some(Bullet::None);
|
||||
for r in &mut para.runs {
|
||||
r.props.font = Some(family.into());
|
||||
r.props.size = Some(40.0);
|
||||
r.props.kern = Some(12.0);
|
||||
}
|
||||
}
|
||||
let l = lay(&p, &sh, 2000.0, 300.0);
|
||||
l.lines[0].extent.1 - l.lines[0].extent.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_spacing_in_the_body_stops_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::<i32>();
|
||||
w(&[]) != w(&deckcraft_fonts::feature("-kern").into_iter().collect::<Vec<_>>())
|
||||
};
|
||||
let Some(family) = first_family(LATIN_FAMILIES, kerns) else {
|
||||
eprintln!("skipped: no font that kerns AVATAR");
|
||||
return;
|
||||
};
|
||||
let (_, off) = glyphs_and_width(&family, "AVATAR WAVE", Some(0.0));
|
||||
let (_, on) = glyphs_and_width(&family, "AVATAR WAVE", Some(12.0));
|
||||
// Small spacing elsewhere keeps the pairs; 0.6 pt or -1 pt anywhere in the box drops them.
|
||||
for (spc, kerned) in [(0.0, true), (0.5, true), (-0.99, true), (0.6, false), (1.0, false), (-1.0, false), (3.0, false)] {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ligatures_stay_for_scripts_that_need_them() {
|
||||
let has = |f: &[deckcraft_fonts::Feature], tag: &str| f.iter().any(|x| Some(*x) == deckcraft_fonts::feature(tag));
|
||||
|
|
|
|||
Loading…
Reference in New Issue