diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index d736de9c..4a2c6639 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -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 = 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 { diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 9d8f6be7..9dd5b189 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -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::>() + }; + 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]