From ff16f3fb370a1688e59fbbaf8824302d2b2f1a7b Mon Sep 17 00:00:00 2001 From: Peng Guanwen Date: Thu, 13 Apr 2023 16:44:39 +0800 Subject: Refine linebreak algorithm for better Chinese justification (#701) --- library/src/layout/par.rs | 108 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 25 deletions(-) (limited to 'library/src/layout') diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 0ad9e171..17e07cd0 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -457,22 +457,35 @@ impl<'a> Line<'a> { self.items().skip(start).take(end - start) } - /// How many justifiable glyphs the line contains. + /// How many glyphs are in the text where we can insert additional + /// space when encountering underfull lines. fn justifiables(&self) -> usize { let mut count = 0; for shaped in self.items().filter_map(Item::text) { count += shaped.justifiables(); } + // CJK character at line end should not be adjusted. + if self + .items() + .last() + .and_then(Item::text) + .map(|s| s.cjk_justifiable_at_last()) + .unwrap_or(false) + { + count -= 1; + } + count } - /// How much of the line is stretchable spaces. - fn stretch(&self) -> Abs { - let mut stretch = Abs::zero(); - for shaped in self.items().filter_map(Item::text) { - stretch += shaped.stretch(); - } - stretch + /// How much can the line stretch + fn stretchability(&self) -> Abs { + self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() + } + + /// How much can the line shrink + fn shrinkability(&self) -> Abs { + self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() } /// The sum of fractions in the line. @@ -835,10 +848,9 @@ fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec= Abs::zero() { + attempt.stretchability() + } else { + attempt.shrinkability() + }; + // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0 + // is possible, in which case the line is underfull. + let mut ratio = delta / adjust; + if ratio.is_nan() { + // The line is not stretchable, but it just fits. + // This often happens with monospace fonts and CJK texts. + ratio = 0.0; + } if ratio.is_infinite() { + // The line's not stretchable, we calculate the ratio in another way... ratio = delta / (em / 2.0); + // ...and because it is underfull/overfull, make sure the ratio is at least 1.0. + if ratio > 0.0 { + ratio += 1.0; + } else { + ratio -= 1.0; + } } - // At some point, it doesn't matter any more. - ratio = ratio.min(10.0); - // Determine the cost of the line. let min_ratio = if attempt.justify { MIN_RATIO } else { 0.0 }; let mut cost = if ratio < min_ratio { @@ -883,11 +912,15 @@ fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec Abs::zero() { + // Attempt to reduce the length of the line, using shrinkability. + justification_ratio = (remaining / shrink).max(-1.0); + remaining = (remaining + shrink).min(Abs::zero()); + } else if line.justify && fr.is_zero() { + // Attempt to increase the length of the line, using stretchability. + if stretch > Abs::zero() { + justification_ratio = (remaining / stretch).min(1.0); + remaining = (remaining - stretch).max(Abs::zero()); + } + let justifiables = line.justifiables(); - if justifiables > 0 { - justification = remaining / justifiables as f64; + if justifiables > 0 && remaining > Abs::zero() { + // Underfull line, distribute the extra space. + extra_justification = remaining / justifiables as f64; remaining = Abs::zero(); } } @@ -1275,7 +1333,7 @@ fn commit( } } Item::Text(shaped) => { - let frame = shaped.build(vt, justification); + let frame = shaped.build(vt, justification_ratio, extra_justification); push(&mut offset, frame); } Item::Frame(frame) => { -- cgit v1.2.3