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}