From 7f9adfac22fc2b095ef0b859908ea02e39bcdd4b Mon Sep 17 00:00:00 2001 From: diquah <57377930+diquah@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:20:46 -0700 Subject: [PATCH] Add microjustification --- crates/typst-layout/src/inline/line.rs | 18 +++++++++++++++++- crates/typst-layout/src/inline/mod.rs | 6 ++++++ crates/typst-layout/src/inline/shaping.rs | 21 +++++++++++++++++++++ crates/typst-library/src/model/par.rs | 11 +++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 659d33f4a..66a1c4d45 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -71,6 +71,16 @@ impl Line<'_> { count } + /// How many glyphs are in the text where we can insert micro-amounts + /// of additional space when encountering underfull lines. + fn microjustifiables(&self) -> usize { + let mut count = 0; + for shaped in self.items.iter().filter_map(Item::text) { + count += shaped.microjustifiables(); + } + count + } + /// How much the line can stretch. pub fn stretchability(&self) -> Abs { self.items @@ -472,6 +482,7 @@ pub fn commit( let fr = line.fr(); let mut justification_ratio = 0.0; let mut extra_justification = Abs::zero(); + let mut extra_microjustification = Abs::zero(); let shrinkability = line.shrinkability(); let stretchability = line.stretchability(); @@ -487,9 +498,13 @@ pub fn commit( } let justifiables = line.justifiables(); + let microjustifiables = line.microjustifiables(); + if justifiables > 0 && remaining > Abs::zero() { // Underfull line, distribute the extra space. - extra_justification = remaining / justifiables as f64; + extra_microjustification = (remaining / microjustifiables as f64).min(p.config.microjustification); + + extra_justification = (remaining - extra_microjustification * microjustifiables as f64) / justifiables as f64; remaining = Abs::zero(); } } @@ -531,6 +546,7 @@ pub fn commit( &p.spans, justification_ratio, extra_justification, + extra_microjustification, ); push(&mut offset, frame); } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 5ef820d07..87e6d3db0 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -183,11 +183,13 @@ fn configuration( situation: Option, ) -> Config { let justify = base.justify; + let microjustification = ParElem::microjustification_in(shared); let font_size = TextElem::size_in(shared); let dir = TextElem::dir_in(shared); Config { justify, + microjustification, linebreaks: base.linebreaks.unwrap_or_else(|| { if justify { Linebreaks::Optimized @@ -267,6 +269,10 @@ struct ConfigBase { struct Config { /// Whether to justify text. justify: bool, + + /// The maximum allowed kerning adjustment for microjustification. + microjustification: Abs, + /// How to determine line breaks. linebreaks: Linebreaks, /// The indent the first line of a paragraph should have. diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 8236d1e36..1f57f1b34 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -83,6 +83,8 @@ pub struct ShapedGlyph { pub c: char, /// Whether this glyph is justifiable for CJK scripts. pub is_justifiable: bool, + /// Whether this glyph is allowed additional kerning for microjustification. + pub is_microjustifiable: bool, /// The script of the glyph. pub script: Script, } @@ -107,6 +109,11 @@ impl ShapedGlyph { self.is_justifiable } + /// Whether the glyph is microjustifiable. + pub fn is_microjustifiable(&self) -> bool { + self.is_microjustifiable + } + /// Whether the glyph is part of Chinese or Japanese script (i.e. CJ, not CJK). pub fn is_cj_script(&self) -> bool { is_cj_script(self.c, self.script) @@ -216,6 +223,7 @@ impl<'a> ShapedText<'a> { spans: &SpanMapper, justification_ratio: f64, extra_justification: Abs, + extra_microjustification: Abs, ) -> Frame { let (top, bottom) = self.measure(engine); let size = Size::new(self.width, top + bottom); @@ -261,6 +269,10 @@ impl<'a> ShapedText<'a> { justification_right += Em::from_length(extra_justification, self.size) } + if shaped.is_microjustifiable() { + justification_right += + Em::from_length(extra_microjustification, self.size) + } frame.size_mut().x += justification_left.at(self.size) + justification_right.at(self.size); @@ -375,6 +387,12 @@ impl<'a> ShapedText<'a> { self.glyphs.iter().filter(|g| g.is_justifiable()).count() } + /// How many glyphs are in the text that are allowed extra kerning for the + /// use of justification when encountering underfull lines. + pub fn microjustifiables(&self) -> usize { + self.glyphs.iter().filter(|g| g.is_microjustifiable()).count() + } + /// Whether the last glyph is a CJK character which should not be justified /// on line end. pub fn cjk_justifiable_at_last(&self) -> bool { @@ -496,6 +514,7 @@ impl<'a> ShapedText<'a> { safe_to_break: true, c: '-', is_justifiable: false, + is_microjustifiable: false, script: Script::Common, }; match side { @@ -881,6 +900,7 @@ fn shape_segment<'a>( x_advance, Adjustability::default().stretchability, ), + is_microjustifiable: true, script, }); } else { @@ -973,6 +993,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { x_advance, Adjustability::default().stretchability, ), + is_microjustifiable: false, script, }); }; diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index cf31b5195..a03c335fb 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -138,6 +138,17 @@ pub struct ParElem { #[default(false)] pub justify: bool, + /// The maximum amount of kerning that is allowed to be used to further + /// justify an existing line. When this value is nonzero, additional + /// justification will be applied to individual glyphs. + /// + /// Note that microjustifications are applied only after a line of text has + /// been constructed. This means that the layout will *not* be affected by + /// microjustifications, but the internal kerning of a line will be. + #[resolve] + #[default(Em::new(0.0).into())] + pub microjustification: Length, + /// How to determine line breaks. /// /// When this property is set to `{auto}`, its default value, optimized line