mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 13:46:52 -06:00
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:
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user