Merge pull request #1184 from seanth/drop-cap-size

(fix)Drop caps: scale with the cap's point size, as InDesign doesDrop cap size
This commit is contained in:
Brandon Thomas
2026-10-19 02:50:15 -04:00
committed by GitHub
2 changed files with 48 additions and 6 deletions
+17 -6
View File
@@ -1716,7 +1716,8 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
drop_box = Some(DropCapBox {
end: dc.end,
baseline: baseline + dc.drop,
rect: Rect::new(at, baseline - asc, at + dc.width, baseline + dc.drop + desc),
// From its line's top, or its own top when it rises above that line.
rect: Rect::new(at, (baseline - asc).min(baseline + dc.drop - dc.cap), at + dc.width, baseline + dc.drop + desc),
});
}
ruby::annotate(db, &mut styles_tab, &story.text, &mut placed, doc.settings.glyph_fallback, f.vertical);
@@ -3764,11 +3765,15 @@ struct DropCap {
/// Its baseline below the first line's: `lines − 1` leadings of the text after it (grid steps
/// on a baseline grid), wherever line `lines` is set.
drop: f64,
/// Its cap height as set (how far its top sits above its baseline).
cap: f64,
}
/// Take the drop cap (the glyphs before byte `end`) off the front of a paragraph's glyphs and scale
/// it so its cap height reaches from the first line's cap height down to line `lines`' baseline,
/// measured in the leading and cap height of the text after it. A one-line drop cap keeps its own
/// measured in the leading and cap height of the text after it: the size of a cap set at that text's
/// size. As in InDesign, a cap set larger (or smaller) is scaled by its size over that text's,
/// keeping its baseline, so it rises above the first line (or falls short of it). A one-line drop cap keeps its own
/// size when that is smaller. On a baseline grid of increment `grid`, lines are that many grid
/// steps apart. With `align_left` its ink starts at the indent (see [`DropCap::lsb`]). Tabs inside
/// it advance to the stops of `tabs` (stops and the start-side indent; in right-to-left paragraphs
@@ -3796,14 +3801,19 @@ fn split_drop_cap(
let mut after: Vec<Glyph> = glyphs.drain(..spaces).collect();
let end = after.last().map_or(end, |g| g.byte.saturating_add(g.len).max(end));
let body = glyphs.iter().find(|g| g.len > 0 && g.cap > 0.0);
let (lead, cap) = body.map_or((base_leading, base_cap), |g| (g.leading, g.cap));
let (lead, cap, body_size) = body.map_or((base_leading, base_cap, None), |g| (g.leading, g.cap, Some(g.size)));
let lead = match grid.map(|inc| (lead / inc - 1e-6).ceil().max(1.0) * inc) {
Some(pitch) if pitch.is_finite() && pitch > 0.0 => pitch,
_ => lead,
};
let drop = (lines - 1) as f64 * lead;
let own = dc.iter().map(|g| g.cap).fold(0.0, f64::max);
let k = if own > 0.0 { (drop + cap) / own } else { 1.0 };
// The cap's tallest glyph sets its height; its point size over the body's scales the fit.
let (own, own_size) = dc.iter().fold((0.0, 0.0), |(c, s), g| if g.cap > c { (g.cap, g.size) } else { (c, s) });
let ratio = match body_size {
Some(b) if b > 0.0 && own_size > 0.0 && (own_size / b).is_finite() => own_size / b,
_ => 1.0,
};
let k = if own > 0.0 { (drop + cap) / own * ratio } else { 1.0 };
let k = if lines == 1 { k.min(1.0) } else { k };
let k = if k.is_finite() && k > 0.0 { k.min(1000.0) } else { 1.0 };
for g in dc.iter_mut().chain(after.iter_mut()) {
@@ -3844,7 +3854,8 @@ fn split_drop_cap(
}
let width = dc.iter().map(|g| g.adv).sum();
dc.append(&mut after);
Some(DropCap { glyphs: dc, end, lines, width, drop, lsb })
let cap = dc.iter().map(|g| g.cap).fold(0.0, f64::max);
Some(DropCap { glyphs: dc, end, lines, width, drop, lsb, cap })
}
impl DropCap {
+31
View File
@@ -3646,6 +3646,37 @@ fn right_to_left_drop_cap_sets_its_tabs_from_the_right_edge() {
assert!(right <= 260.0 + 1e-6 && right > 255.0, "text ends at {right}");
}
/// The cap's point size scales it, as in InDesign: at the body's size (10 pt) it fills its lines
/// exactly; set larger or smaller it is scaled by its size over the body's, keeping its baseline,
/// so it rises above the first line (or falls short of it). The body's lines don't move, and the
/// cap's box (highlight, caret) reaches the top of a cap that rises above its line.
#[test]
fn drop_cap_scales_with_its_point_size() {
let set = |size: f64| {
let (mut d, sid, _) = drop_doc(&[LOREM; 2].join(" "), drop_cap(3, 1));
d.story_mut(sid).unwrap().format_chars(0..1, |f| f.over.size = Some(size));
let cs = compose_story(&d, sid, &ComposeOptions::default());
all_lines(&cs).into_iter().cloned().collect::<Vec<Line>>()
};
let base = set(10.0);
let (b0, b2) = (base[0].baseline, base[2].baseline);
let body = base[0].glyphs.iter().find(|g| g.byte == 1).unwrap();
let fitted = cap_of(&base[0].glyphs[0]);
assert!((b2 - fitted - (b0 - cap_of(body))).abs() < 0.01, "at the body's size the cap fills its lines");
for (size, scale) in [(15.0, 1.5), (7.5, 0.75)] {
let l = set(size);
let dc = &l[0].glyphs[0];
for (a, b) in l.iter().zip(&base) {
assert!((a.baseline - b.baseline).abs() < 1e-6, "{size} pt: the lines stay where they were");
}
assert!((l[0].baseline + dc.y - b2).abs() < 1e-6, "{size} pt: the cap sits on line 3's baseline");
assert!((cap_of(dc) - fitted * scale).abs() < 0.01, "{size} pt: cap height {} vs {}", cap_of(dc), fitted * scale);
let top = (l[0].baseline - l[0].ascent).min(b2 - cap_of(dc));
assert!((l[0].drop_cap.unwrap().rect.y0 - top).abs() < 1e-6, "{size} pt: the box starts at the line's or the cap's top");
}
assert!(b2 - fitted * 1.5 < b0 - base[0].ascent, "a 15 pt cap rises above the first line");
}
/// A rule in Text Color takes the colour of the paragraph's text: the first character's for the
/// rule above, the last character's for the rule below. A swatch colour is used as is.
#[test]