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.
This commit is contained in:
aiongg
2026-10-10 16:22:17 +00:00
parent 5dc8383a50
commit 4014dac6c2
3 changed files with 60 additions and 8 deletions
+28 -7
View File
@@ -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<f64>,
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<f64>,
/// 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<f64>,
last_descent: f64,
last_reference: f64,
aki_below: Option<f64>,
pending: f64,
}
@@ -1837,6 +1845,7 @@ struct Above {
baseline: f64,
descent: f64,
reference: f64,
aki_below: Option<f64>,
/// 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(
+31
View File
@@ -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::<Vec<_>>()
};
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| {
+1 -1
View File
@@ -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 |