From 4014dac6c2d37ce5197a2684caa0fd9ad2f3da86 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 16:22:17 +0000 Subject: [PATCH] Compose: under the Japanese composers a line sits the upper line's leading below it Under the Aki Below leading model a line's leading is the space below it, so the next line's reference point sits the upper line's leading below the upper line's (InDesign's Japanese-composer output measures this in four corpus documents). The first line in a frame or column places itself with its own leading; space before and after adds on top, and the baseline grid snaps the result. The other leading models advance by the lower line's leading as before. --- crates/compose/src/lib.rs | 35 ++++++++++++++++++++++++++++------- crates/compose/src/tests.rs | 31 +++++++++++++++++++++++++++++++ docs/cjk.md | 2 +- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index d7e564af1..239b148f0 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -489,6 +489,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C last_baseline: None, last_descent: 0.0, last_reference: 0.0, + aki_below: None, pending: 0.0, pi: 0, band: Band::default(), @@ -922,8 +923,9 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let (s, e) = (g0 + b.start, g0 + b.end); let line_glyphs = &glyphs[s..e.max(s)]; let (asc, desc, lead) = line_metrics(line_glyphs, &glyphs, s, base_leading, base_chars.size, db, &base_chars); - let reference = cjk_line_reference(line_glyphs); - let mut baseline = cur.next_baseline(f, col, lead, asc, &pp); + let (reference, model) = cjk_line_reference(line_glyphs); + let advance = if cur.last_baseline.is_some() { cur.aki_below.unwrap_or(lead) } else { lead }; + let mut baseline = cur.next_baseline(f, col, advance, asc, &pp); if cur.last_baseline.is_some() { baseline += cur.last_reference - reference; } @@ -1095,6 +1097,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C } cur.last_baseline = Some(baseline); cur.last_reference = reference; + cur.aki_below = (model == designcraft_doc::cjk::LeadingModel::AkiBelow).then_some(lead); cur.last_descent = desc; cur.pending = 0.0; line_no += 1; @@ -1138,6 +1141,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C baseline: l.baseline, descent: l.descent, reference: cur.last_reference, + aki_below: cur.aki_below, pending: pp.space_after, }; cur.band = Band { para: pi + 1, line0: ft.lines.len(), above: Some(above) }; @@ -1728,6 +1732,9 @@ struct Cursor { last_baseline: Option, last_descent: f64, last_reference: f64, + /// The line above's leading when it sets the advance to the next line: under the Japanese + /// composers' Aki Below model the leading is the space below its line. + aki_below: Option, /// Space before/after waiting to be added to the next line. pending: f64, /// The paragraph being laid. @@ -1794,6 +1801,7 @@ struct Resume { last_baseline: Option, last_descent: f64, last_reference: f64, + aki_below: Option, pending: f64, } @@ -1837,6 +1845,7 @@ struct Above { baseline: f64, descent: f64, reference: f64, + aki_below: Option, /// The spanning paragraph's space after. pending: f64, } @@ -1916,7 +1925,13 @@ impl Cursor { self.split_part(); } fn state(&self) -> Resume { - Resume { last_baseline: self.last_baseline, last_descent: self.last_descent, last_reference: self.last_reference, pending: self.pending } + Resume { + last_baseline: self.last_baseline, + last_descent: self.last_descent, + last_reference: self.last_reference, + aki_below: self.aki_below, + pending: self.pending, + } } /// Start a split block (or its part in a new column) here. fn split_part(&mut self) { @@ -1936,6 +1951,7 @@ impl Cursor { self.last_baseline = r.last_baseline; self.last_descent = r.last_descent; self.last_reference = r.last_reference; + self.aki_below = r.aki_below; self.pending = r.pending; } /// Within a split block, the next sub-column when there's one to go to. @@ -1959,10 +1975,12 @@ impl Cursor { self.last_baseline = Some(a.baseline); self.last_descent = a.descent; self.last_reference = a.reference; + self.aki_below = a.aki_below; self.pending = a.pending; } None => { self.last_baseline = None; + self.aki_below = None; self.pending = 0.0; } } @@ -2039,16 +2057,19 @@ fn cjk_alignment_shift(g: &Glyph, reference: &Glyph) -> f64 { A::IcfBottom => g.descent - reference.descent, } } -fn cjk_line_reference(line: &[Glyph]) -> f64 { +/// The point leading measures a line from, relative to its baseline, and the leading model of +/// the line's largest character, which sets both. +fn cjk_line_reference(line: &[Glyph]) -> (f64, designcraft_doc::cjk::LeadingModel) { use designcraft_doc::cjk::LeadingModel as L; - let Some(g) = line.iter().max_by(|a, b| a.size.total_cmp(&b.size)) else { return 0.0 }; + let Some(g) = line.iter().max_by(|a, b| a.size.total_cmp(&b.size)) else { return (0.0, L::Roman) }; let (top, bottom) = cjk_em_box(g); - match g.leading_model { + let reference = match g.leading_model { L::Roman => 0.0, L::AkiBelow => bottom, L::AkiAbove => top, L::Center | L::CenterDown => (top + bottom) / 2.0, - } + }; + (reference, g.leading_model) } fn line_metrics( diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 914c1c8fc..f48f455b4 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1499,6 +1499,37 @@ fn cjk_center_leading_measures_em_centers_across_different_sizes() { assert!((lines[1].baseline - lines[0].baseline - 30.0).abs() < 1e-6); } +#[test] +fn cjk_aki_below_sets_the_upper_lines_leading_below_it() { + let lines_with = |para: ParaAttrs| { + let (mut d, sid, _) = doc_with("A\nB", Rect::new(0.0, 0.0, 300.0, 200.0), para); + let st = d.story_mut(sid).unwrap(); + st.format_chars(0..3, |f| f.over.leading_model = Some(designcraft_doc::cjk::LeadingModel::AkiBelow)); + st.format_chars(0..2, |f| { + f.over.size = Some(24.0); + f.over.leading = Some(designcraft_doc::Leading::Points(40.0)); + }); + st.format_chars(2..3, |f| { + f.over.size = Some(12.0); + f.over.leading = Some(designcraft_doc::Leading::Points(20.0)); + }); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + all_lines(&cs).into_iter().cloned().collect::>() + }; + let lines = lines_with(japanese()); + assert_eq!(lines.len(), 2); + let em_bottom = |l: &Line| { + let g = &l.glyphs[0]; + let (a, b) = g.face.vertical_metrics(); + l.baseline + b / (a + b) * g.face.units_per_em() * g.sy + }; + let gap = em_bottom(&lines[1]) - em_bottom(&lines[0]); + assert!((gap - 40.0).abs() < 1e-6, "{gap}"); + // The other composers advance by the lower line's leading, baseline to baseline. + let lines = lines_with(ParaAttrs::default()); + assert!((lines[1].baseline - lines[0].baseline - 20.0).abs() < 1e-6); +} + #[test] fn cjk_em_center_alignment_moves_small_characters() { let line_with = |para: ParaAttrs| { diff --git a/docs/cjk.md b/docs/cjk.md index 72b9f7ab7..1dda1b55e 100644 --- a/docs/cjk.md +++ b/docs/cjk.md @@ -37,7 +37,7 @@ presets or tables are copied. Our default mojikumi and kinsoku tables are built | Korean line breaking | at spaces (Hangul everywhere, hanja in Korean text); per-paragraph character-based breaking (`koreanCharBreaks`); DesignCraft-only, not in IDML | | Mojikumi | tables and paragraph references kept through IDML; not applied (Preflight says so) | | Aki, tsume, jidori | done ([cjk-typography.md](cjk-typography.md)) | -| Character alignment, leading model | em box top / centre / bottom, ICF from ascender and descender, roman baseline; aki above / below, centre (centre down as centre); with the Japanese composers (`Composer::Japanese`, `JapaneseSingleLine`, IDML `HL Composer J` / `HL Single J`), the others set lines baseline to baseline on the Roman baseline | +| Character alignment, leading model | em box top / centre / bottom, ICF from ascender and descender, roman baseline; aki above / below, centre (centre down as centre); under aki below a line's leading is the space to the line below it; with the Japanese composers (`Composer::Japanese`, `JapaneseSingleLine`, IDML `HL Composer J` / `HL Single J`), the others set lines baseline to baseline on the Roman baseline | | Tate-chu-yoko | manual, with offsets; no auto, 3+ digits overflow the em | | Ruby | group ruby only, fixed 50 % size, no options, doesn't affect leading | | Kenten | the IDML kinds and a custom character, drawn as characters; no position, size, alignment, colour or font |