Skip to main content

retex_engine/
expansion.rs

1//! Font expansion (hz): `\adjustspacing`, `\expandglyphsinfont`,
2//! `\efcode` and the `expansion_factor` of glyph and kern nodes.
3//!
4//! A font made expandable with `\expandglyphsinfont f stretch shrink step`
5//! (limits and step in thousandths of the glyph widths) can be widened or
6//! narrowed horizontally to justify a line, so that less glue stretching
7//! or shrinking is needed. LuaTeX keeps no expanded font copies: each glyph
8//! node carries its own `expansion_factor` (in millionths of the width), and
9//! the backend draws it scaled horizontally (LuaTeX manual, *font
10//! expansion*; Hàn Thế Thành's thesis, chapter 4).
11//!
12//! The arithmetic below was established by black-box tests against LuaTeX
13//! 1.22 (fonts with chosen widths and kerns, lines swept across rounding
14//! boundaries one sp at a time):
15//!
16//! * `\expandglyphsinfont` limits the stretch to 1000 and the shrink to
17//!   500, and rounds both down to multiples of the step.
18//! * A glyph of width *w* with `\efcode` *e* (default 1000) can stretch
19//!   by round(*s*·*e*/1000), where *s* = round(*w*·(1000 + stretch)/1000)
20//!   − *w* is the stretch of the glyph at the font's limit; it can shrink
21//!   by round(*s*′·*e*/1000), *s*′ = *w* − round(*w*·(1000 − shrink)/1000).
22//!   Rounding the expanded width rather than the difference matters for
23//!   halves: 400425 sp at 50% stretches by 200213 sp but shrinks by
24//!   200212 sp. A negative `\efcode` counts as 0 here; one above 1000
25//!   counts in full.
26//! * A font kern between two glyphs of an expandable font contributes the
27//!   same amounts computed from the kern *k* and the integer mean *ē* of
28//!   the two `\efcode`s (truncated, not limited to 0…1000): −45875 sp at
29//!   2% stretches by −918 sp and shrinks by −917 sp. Since kerns are
30//!   usually negative, they reduce both the stretchability and the
31//!   shrinkability of the line.
32//! * With `\adjustspacing` ≥ 2 the line breaker adds these amounts to the
33//!   line's stretch and shrink: the shortfall is first taken up by font
34//!   expansion and only the remainder by glue (badness 0 when expansion
35//!   suffices). With 3, kerns do not take part.
36//! * When a line of a paragraph is packaged (any `\adjustspacing` > 0), the
37//!   ratio *r* = round(1000·shortfall/total font stretch or shrink), at most
38//!   1000 in magnitude, sets each glyph's factor in thousandths to
39//!   round(round(*r*·limit·*e*/10⁶)/step)·step (negative when shrinking,
40//!   with the glyph's own `\efcode`, at most the limit in magnitude), and
41//!   each expandable kern's `expansion_factor` (an amount in sp) to its
42//!   full stretch amount when the line stretches, or its full shrink amount
43//!   when it shrinks, whatever the ratio. The glue then takes up the rest.
44//!   If *r* is 0 (the shortfall is within half a thousandth of the font
45//!   stretch or shrink), nothing is expanded and the line keeps its
46//!   natural glue, with badness 0 and no warning. A line with infinitely
47//!   stretchable glue is not expanded when it stretches (it is when it
48//!   shrinks). Explicit `\hbox`es are never expanded.
49//! * An expanded glyph is round(*w*·(10⁶ + factor)/10⁶) wide; a kern is
50//!   wider by its `expansion_factor`. `\showbox` shows neither.
51
52use crate::Engine;
53use crate::nodes::{NodeData, NodeRef, id, kern_sub};
54use crate::primitives_gen::assign_int as ip;
55use crate::protrusion::round_div;
56
57/// The expansion parameters of a font (`\expandglyphsinfont`).
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub struct FontExpansion {
60    /// Maximal stretch, in thousandths.
61    pub stretch: i32,
62    /// Maximal shrink, in thousandths.
63    pub shrink: i32,
64    /// Step, in thousandths.
65    pub step: i32,
66}
67
68impl FontExpansion {
69    /// The expansion parameters given to `\expandglyphsinfont`, if they
70    /// make the font expandable. The stretch limit is at most 1000, the
71    /// shrink limit at most 500, and both are rounded down to multiples
72    /// of the step.
73    pub fn new(stretch: i32, shrink: i32, step: i32) -> Option<FontExpansion> {
74        let step = step.clamp(0, 1000);
75        if step == 0 {
76            return None;
77        }
78        let limit = |l: i32, max: i32| l.clamp(0, max) / step * step;
79        let e = FontExpansion {
80            stretch: limit(stretch, 1000),
81            shrink: limit(shrink, 500),
82            step,
83        };
84        (e.stretch > 0 || e.shrink > 0).then_some(e)
85    }
86
87    /// The stretch and shrink of a glyph or font kern of width `w` with
88    /// `\efcode` `e`: the difference between `w` expanded to the font's
89    /// limit and `w`, scaled by `e`/1000.
90    fn amounts(self, w: i32, e: i64) -> FontStretch {
91        let full = |limit: i32| i64::from(expanded_width(w, limit * 1000)) - i64::from(w);
92        FontStretch {
93            stretch: round_div(full(self.stretch) * e, 1000) as i32,
94            shrink: round_div(-full(-self.shrink) * e, 1000) as i32,
95        }
96    }
97}
98
99/// Width `w` expanded by `ef` millionths.
100#[inline]
101pub fn expanded_width(w: i32, ef: i32) -> i32 {
102    if ef == 0 {
103        w
104    } else {
105        round_div(i64::from(w) * (1_000_000 + i64::from(ef)), 1_000_000) as i32
106    }
107}
108
109/// Stretch and shrink totals of font expansion in a list.
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
111pub struct FontStretch {
112    /// Total stretch (sp).
113    pub stretch: i32,
114    /// Total shrink (sp).
115    pub shrink: i32,
116}
117
118impl Engine {
119    /// The expansion parameters of font `f`, if it is expandable.
120    #[inline]
121    pub fn font_expansion(&self, f: u32) -> Option<FontExpansion> {
122        self.font(f).expansion
123    }
124
125    /// `\efcode` of character `c` in font `f` (1000 by default).
126    pub fn ef_code(&self, f: u32, c: u32) -> i32 {
127        self.font(f).char_codes[2].get(&c).copied().unwrap_or(1000)
128    }
129
130    /// `\expandglyphsinfont f stretch shrink step`.
131    pub fn set_font_expansion(&mut self, f: u32, stretch: i32, shrink: i32, step: i32) {
132        let e = FontExpansion::new(stretch, shrink, step);
133        if let Some(fi) = self.fonts.fonts.get_mut(f as usize) {
134            fi.expansion = e;
135        }
136    }
137
138    /// The stretch and shrink of glyph node `p` through font expansion.
139    pub(crate) fn glyph_font_stretch(&self, p: NodeRef) -> FontStretch {
140        let Some(g) = self.nodes.glyph(p) else {
141            return FontStretch::default();
142        };
143        let Some(x) = self.font_expansion(g.font) else {
144            return FontStretch::default();
145        };
146        // A negative \efcode gives no stretch or shrink (but still a factor).
147        let e = i64::from(self.ef_code(g.font, g.chr).max(0));
148        let w = self.font_char_dimen(g.font, g.chr, 0);
149        x.amounts(w, e)
150    }
151
152    /// The stretch and shrink of kern `k` between nodes `prev` and `next`
153    /// through font expansion (font kerns between two glyphs of an
154    /// expandable font).
155    pub(crate) fn kern_font_stretch(
156        &self,
157        prev: NodeRef,
158        k: NodeRef,
159        next: NodeRef,
160    ) -> FontStretch {
161        if prev.is_null() || next.is_null() || self.nodes.subtype(k) != kern_sub::FONT {
162            return FontStretch::default();
163        }
164        let (Some(a), Some(b)) = (self.nodes.glyph(prev), self.nodes.glyph(next)) else {
165            return FontStretch::default();
166        };
167        let Some(x) = self.font_expansion(a.font) else {
168            return FontStretch::default();
169        };
170        let NodeData::Kern { kern, .. } = self.nodes.data(k) else {
171            return FontStretch::default();
172        };
173        let mean = i64::from((self.ef_code(a.font, a.chr) + self.ef_code(b.font, b.chr)) / 2);
174        x.amounts(*kern, mean)
175    }
176
177    /// Font stretch and shrink of the list `p` (a discretionary text):
178    /// glyphs, and kerns too if `kerns`.
179    pub(crate) fn list_font_stretch(&self, mut p: NodeRef, kerns: bool) -> FontStretch {
180        let mut t = FontStretch::default();
181        let mut prev = NodeRef::NULL;
182        while !p.is_null() {
183            let s = match self.nodes.id(p) {
184                id::GLYPH => self.glyph_font_stretch(p),
185                id::KERN if kerns => self.kern_font_stretch(prev, p, self.nodes.next(p)),
186                _ => FontStretch::default(),
187            };
188            t.stretch += s.stretch;
189            t.shrink += s.shrink;
190            prev = p;
191            p = self.nodes.next(p);
192        }
193        t
194    }
195
196    /// Expands the glyphs of a line of a paragraph (the hlist `list`) so
197    /// that it gets closer to width `width`, before it is packaged. Returns
198    /// true if the line is to be left at its natural size (its glue not
199    /// set), which LuaTeX does when the expansion ratio rounds to 0.
200    pub(crate) fn expand_line(&mut self, list: NodeRef, width: i32) -> bool {
201        let kerns = self.eqtb.int(ip::ADJUSTSPACING) != 3;
202        // Natural width with unexpanded glyphs and kerns, and the font
203        // stretch and shrink.
204        let mut nat: i64 = 0;
205        let mut fs = FontStretch::default();
206        let mut any = false;
207        let mut infinite_stretch = false;
208        let mut prev = NodeRef::NULL;
209        let mut p = list;
210        while !p.is_null() {
211            let next = self.nodes.next(p);
212            match self.nodes.id(p) {
213                id::GLYPH => {
214                    let (f, c) = match self.nodes.data_mut(p) {
215                        NodeData::Glyph(g) => {
216                            g.expansion_factor = 0;
217                            (g.font, g.chr)
218                        }
219                        _ => unreachable!("glyph node without glyph data"),
220                    };
221                    nat += i64::from(self.font_char_dimen(f, c, 0));
222                    if self.font_expansion(f).is_some() {
223                        any = true;
224                        let s = self.glyph_font_stretch(p);
225                        fs.stretch += s.stretch;
226                        fs.shrink += s.shrink;
227                    }
228                }
229                id::KERN => {
230                    if let NodeData::Kern {
231                        kern,
232                        expansion_factor,
233                    } = self.nodes.data_mut(p)
234                    {
235                        *expansion_factor = 0;
236                        nat += i64::from(*kern);
237                    }
238                    if kerns {
239                        let s = self.kern_font_stretch(prev, p, next);
240                        fs.stretch += s.stretch;
241                        fs.shrink += s.shrink;
242                    }
243                }
244                id::DISC => {
245                    // The replacement text of an unbroken discretionary
246                    // is typeset; its glyphs (not its kerns) expand too.
247                    let (_, _, r) = self.nodes.disc_lists(p);
248                    let mut q = r;
249                    while !q.is_null() {
250                        if let NodeData::Glyph(g) = self.nodes.data_mut(q) {
251                            g.expansion_factor = 0;
252                            let f = g.font;
253                            if self.font_expansion(f).is_some() {
254                                any = true;
255                                let s = self.glyph_font_stretch(q);
256                                fs.stretch += s.stretch;
257                                fs.shrink += s.shrink;
258                            }
259                        }
260                        q = self.nodes.next(q);
261                    }
262                    nat += i64::from(self.list_natural_width(r));
263                }
264                _ => {
265                    if let NodeData::Glue { spec, .. } = self.nodes.data(p)
266                        && spec.stretch_order > 0
267                        && spec.stretch != 0
268                    {
269                        infinite_stretch = true;
270                    }
271                    nat += i64::from(self.node_natural_width(p));
272                }
273            }
274            prev = p;
275            p = next;
276        }
277        if !any {
278            return false;
279        }
280        let x = i64::from(width) - nat;
281        if x > 0 && infinite_stretch {
282            // Infinitely stretchable glue takes it all (the last line of a
283            // paragraph): no expansion.
284            return false;
285        }
286        let (ratio, total) = if x > 0 {
287            (1, fs.stretch)
288        } else if x < 0 {
289            (-1, fs.shrink)
290        } else {
291            return false;
292        };
293        if total <= 0 {
294            return false;
295        }
296        let r = (round_div(x.abs() * 1000, i64::from(total))).min(1000) * ratio;
297        if r == 0 {
298            return true;
299        }
300        let mut prev = NodeRef::NULL;
301        let mut p = list;
302        while !p.is_null() {
303            let next = self.nodes.next(p);
304            match self.nodes.id(p) {
305                id::GLYPH => self.set_glyph_expansion(p, r, x > 0),
306                id::DISC => {
307                    let (_, _, rl) = self.nodes.disc_lists(p);
308                    let mut q = rl;
309                    while !q.is_null() {
310                        if self.nodes.id(q) == id::GLYPH {
311                            self.set_glyph_expansion(q, r, x > 0);
312                        }
313                        q = self.nodes.next(q);
314                    }
315                }
316                id::KERN if kerns => {
317                    let s = self.kern_font_stretch(prev, p, next);
318                    let amount = if x > 0 { s.stretch } else { s.shrink };
319                    if let NodeData::Kern {
320                        expansion_factor, ..
321                    } = self.nodes.data_mut(p)
322                    {
323                        *expansion_factor = amount;
324                    }
325                }
326                _ => {}
327            }
328            prev = p;
329            p = next;
330        }
331        false
332    }
333
334    /// Sets the expansion factor of glyph `p` for the ratio `r`
335    /// (thousandths of the full stretch or shrink).
336    fn set_glyph_expansion(&mut self, p: NodeRef, r: i64, stretching: bool) {
337        let Some((f, c)) = self.nodes.glyph(p).map(|g| (g.font, g.chr)) else {
338            return;
339        };
340        let Some(e) = self.font_expansion(f) else {
341            return;
342        };
343        let limit = i64::from(if stretching { e.stretch } else { e.shrink });
344        let code = i64::from(self.ef_code(f, c));
345        let step = i64::from(e.step);
346        // The factor in thousandths, then rounded to a multiple of the step.
347        let n = round_div(round_div(r * limit * code, 1_000_000), step) * step;
348        if let NodeData::Glyph(g) = self.nodes.data_mut(p) {
349            g.expansion_factor = (n.clamp(-limit, limit) * 1000) as i32;
350        }
351    }
352
353    /// The natural width of a node as `hpack` counts it (not glyphs and
354    /// kerns, which [`Engine::expand_line`] handles).
355    fn node_natural_width(&self, p: NodeRef) -> i32 {
356        match self.nodes.data(p) {
357            NodeData::Glue { spec, .. } => spec.width,
358            NodeData::Box(b) => b.width,
359            NodeData::Unset { b, .. } => b.width,
360            NodeData::Rule { width, .. } => *width,
361            NodeData::MarginKern { width, .. } => *width,
362            NodeData::Math { surround, glue } => {
363                if glue.width != 0 || glue.stretch != 0 || glue.shrink != 0 {
364                    glue.width
365                } else {
366                    *surround
367                }
368            }
369            _ => 0,
370        }
371    }
372
373    fn list_natural_width(&self, mut p: NodeRef) -> i32 {
374        let mut w = 0;
375        while !p.is_null() {
376            w += match self.nodes.data(p) {
377                NodeData::Glyph(_) => self.glyph_dimensions(p).0,
378                NodeData::Kern {
379                    kern,
380                    expansion_factor,
381                } => kern + expansion_factor,
382                _ => self.node_natural_width(p),
383            };
384            p = self.nodes.next(p);
385        }
386        w
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::{FontExpansion, FontStretch, expanded_width};
393
394    #[test]
395    fn limits() {
396        let e = |a, b, c| FontExpansion::new(a, b, c).map(|e| (e.stretch, e.shrink, e.step));
397        assert_eq!(e(20, 20, 1), Some((20, 20, 1)));
398        assert_eq!(e(1500, 600, 7), Some((994, 497, 7)));
399        assert_eq!(e(20, 20, 3), Some((18, 18, 3)));
400        assert_eq!(e(20, 0, 20), Some((20, 0, 20)));
401        assert_eq!(e(20, 20, 0), None);
402        assert_eq!(e(0, 0, 1), None);
403    }
404
405    #[test]
406    fn amounts() {
407        let x = |a, b| FontExpansion::new(a, b, 1).unwrap();
408        let st = |stretch, shrink| FontStretch { stretch, shrink };
409        // Halves: the expanded widths are rounded, not the differences.
410        assert_eq!(x(500, 500).amounts(400_425, 1000), st(200_213, 200_212));
411        assert_eq!(x(20, 20).amounts(-45_875, 1000), st(-918, -917));
412        assert_eq!(x(20, 20).amounts(45_875, 1000), st(918, 917));
413        // The \efcode scales the rounded amounts.
414        assert_eq!(x(20, 20).amounts(400_425, 999), st(8001, 8000));
415        assert_eq!(x(20, 20).amounts(-45_875, 749), st(-688, -687));
416        assert_eq!(x(20, 20).amounts(345_886, 1000), st(6918, 6918));
417    }
418
419    #[test]
420    fn expanded_widths() {
421        assert_eq!(expanded_width(491_521, 20_000), 501_351);
422        assert_eq!(expanded_width(491_521, -20_000), 481_691);
423        assert_eq!(expanded_width(327_681, 0), 327_681);
424    }
425}