Skip to main content

retex_engine/
protrusion.rs

1//! Character protrusion (margin kerning): `\protrudechars`, `\lpcode`,
2//! `\rpcode`, margin kern nodes, `\leftmarginkern` and `\rightmarginkern`.
3//!
4//! A character at the start (end) of a line may stick out into the left
5//! (right) margin by `\lpcode` (`\rpcode`) thousandths of the font's quad.
6//! With `\protrudechars` > 0, the post-line-break pass puts a margin kern
7//! of minus that amount at the start (end) of each line; with
8//! `\protrudechars` > 1 the line breaker counts the protrusion when it
9//! measures candidate lines (LuaTeX manual, *protrusion*; Hàn Thế Thành's
10//! thesis, chapter 3).
11//!
12//! Which character counts as "at the edge" was established by black-box
13//! tests against LuaTeX 1.22. Searching from the line end inwards, these
14//! nodes are passed over (the list in the LuaTeX manual): glue without
15//! width, stretch and shrink, penalties, empty discretionaries, font kerns
16//! and kerns of width zero, rules of zero width, height and depth, math
17//! nodes without surround, direction and local paragraph nodes, empty
18//! zero-sized hlists, inserts, marks, adjusts, boundaries and whatsits. A
19//! non-empty hlist is searched recursively. Anything else ends the search
20//! without a protruding character.
21//!
22//! The line breaker searches a little differently from the post-line-break
23//! pass:
24//!
25//! * at the start of a line it first skips what the post-line-break pass
26//!   will prune (glue, explicit kerns, penalties and math nodes after the
27//!   break), starting at the break node itself;
28//! * after a break at a discretionary, the post-break text counts only if
29//!   it starts with a character (nothing in it is skipped); an empty
30//!   post-break text continues the search at the discretionary node,
31//!   which only an empty discretionary lets through;
32//! * before a break at a discretionary, likewise the pre-break text counts
33//!   only if it ends with a character; an empty one continues before the
34//!   discretionary;
35//! * the last line of a paragraph gets no right protrusion in line
36//!   breaking, whatever `\parfillskip` is (the post-line-break pass still
37//!   adds its margin kern).
38
39use crate::Engine;
40use crate::nodes::{NodeData, NodeRef, id, kern_sub};
41
42/// Subtype of a left margin kern node.
43pub const LEFT_SIDE: u16 = 0;
44/// Subtype of a right margin kern node.
45pub const RIGHT_SIDE: u16 = 1;
46
47/// The result of searching for a protruding character.
48enum Found {
49    /// A glyph.
50    Char(NodeRef),
51    /// The search ended at a node that stops protrusion.
52    Blocked,
53    /// The list ended without a decision.
54    End,
55}
56
57/// Rounds `a / b` to the nearest integer, halves away from zero (`b` > 0).
58pub(crate) fn round_div(a: i64, b: i64) -> i64 {
59    if a >= 0 {
60        (2 * a + b) / (2 * b)
61    } else {
62        -((-2 * a + b) / (2 * b))
63    }
64}
65
66impl Engine {
67    /// The protrusion of glyph `p` into the left (`left`) or right margin,
68    /// in sp: the `\lpcode`/`\rpcode` of its character in thousandths of
69    /// the font's quad. The margin kern is minus this amount.
70    pub fn char_protrusion(&self, p: NodeRef, left: bool) -> i32 {
71        let Some(g) = self.nodes.glyph(p) else {
72            return 0;
73        };
74        let fi = self.font(g.font);
75        let code = fi.char_codes[usize::from(!left)]
76            .get(&g.chr)
77            .copied()
78            .unwrap_or(0);
79        if code == 0 {
80            return 0;
81        }
82        let quad = i64::from(self.quad(g.font));
83        round_div(i64::from(code) * quad, 1000) as i32
84    }
85
86    /// Whether node `p` is passed over when looking for a protruding
87    /// character.
88    fn protrusion_skippable(&self, p: NodeRef) -> bool {
89        match self.nodes.data(p) {
90            NodeData::Glue { spec, .. } => spec.width == 0 && spec.stretch == 0 && spec.shrink == 0,
91            NodeData::Penalty { .. } => true,
92            NodeData::Disc {
93                pre, post, replace, ..
94            } => pre.is_null() && post.is_null() && replace.is_null(),
95            NodeData::Kern { kern, .. } => *kern == 0 || self.nodes.subtype(p) == kern_sub::FONT,
96            NodeData::Rule {
97                width,
98                height,
99                depth,
100                ..
101            } => *width == 0 && *height == 0 && *depth == 0,
102            NodeData::Math { surround, glue } => {
103                *surround == 0 && glue.width == 0 && glue.stretch == 0 && glue.shrink == 0
104            }
105            NodeData::Dir { .. }
106            | NodeData::LocalPar { .. }
107            | NodeData::Ins { .. }
108            | NodeData::Mark { .. }
109            | NodeData::Adjust { .. }
110            | NodeData::Boundary { .. }
111            | NodeData::Whatsit(_) => true,
112            NodeData::Box(b) => {
113                self.nodes.id(p) == id::HLIST
114                    && b.head.is_null()
115                    && b.width == 0
116                    && b.height == 0
117                    && b.depth == 0
118            }
119            _ => false,
120        }
121    }
122
123    /// Whether `p` is pruned at the start of a line after a break.
124    fn pruned_after_break(&self, p: NodeRef) -> bool {
125        match self.nodes.id(p) {
126            id::GLUE | id::PENALTY | id::MATH => true,
127            id::KERN => matches!(self.nodes.subtype(p), kern_sub::USER | kern_sub::ITALIC),
128            _ => false,
129        }
130    }
131
132    /// Searches forwards from `p` for the character at the left edge.
133    fn protchar_left_from(&self, mut p: NodeRef) -> Found {
134        while !p.is_null() {
135            match self.nodes.id(p) {
136                id::GLYPH => return Found::Char(p),
137                id::HLIST if !self.nodes.list_ptr(p).is_null() => {
138                    match self.protchar_left_from(self.nodes.list_ptr(p)) {
139                        Found::End => {}
140                        f => return f,
141                    }
142                }
143                _ if self.protrusion_skippable(p) => {}
144                _ => return Found::Blocked,
145            }
146            p = self.nodes.next(p);
147        }
148        Found::End
149    }
150
151    /// Searches the list `head` for the character at its right edge.
152    fn protchar_right_in(&self, head: NodeRef) -> Found {
153        // Collect the list, then walk it backwards (inner lists are
154        // short and their back links are not maintained everywhere).
155        let mut v = Vec::new();
156        let mut p = head;
157        while !p.is_null() {
158            v.push(p);
159            p = self.nodes.next(p);
160        }
161        self.protchar_right_rev(v.into_iter().rev())
162    }
163
164    fn protchar_right_rev(&self, it: impl Iterator<Item = NodeRef>) -> Found {
165        for p in it {
166            match self.nodes.id(p) {
167                id::GLYPH => return Found::Char(p),
168                id::HLIST if !self.nodes.list_ptr(p).is_null() => {
169                    match self.protchar_right_in(self.nodes.list_ptr(p)) {
170                        Found::End => {}
171                        f => return f,
172                    }
173                }
174                _ if self.protrusion_skippable(p) => {}
175                _ => return Found::Blocked,
176            }
177        }
178        Found::End
179    }
180
181    /// Searches backwards from `p` (inclusive) in a list with valid back
182    /// links, stopping at `stop` (exclusive).
183    fn protchar_right_back(&self, p: NodeRef, stop: NodeRef) -> Found {
184        let mut q = p;
185        let it = std::iter::from_fn(move || {
186            if q.is_null() || q == stop {
187                return None;
188            }
189            let r = q;
190            q = self.nodes.prev(q);
191            Some(r)
192        });
193        self.protchar_right_rev(it)
194    }
195
196    fn found_left(&self, f: Found) -> i32 {
197        match f {
198            Found::Char(g) => self.char_protrusion(g, true),
199            _ => 0,
200        }
201    }
202
203    fn found_right(&self, f: Found) -> i32 {
204        match f {
205            Found::Char(g) => self.char_protrusion(g, false),
206            _ => 0,
207        }
208    }
209
210    /// Line breaking: the left protrusion of a line that starts after the
211    /// break at `brk` (`None` for the first line, whose first node is
212    /// `first`).
213    pub(crate) fn lb_left_protrusion(&self, brk: Option<NodeRef>, first: NodeRef) -> i32 {
214        let mut p = match brk {
215            None => return self.found_left(self.protchar_left_from(first)),
216            Some(b) if b.is_null() => return 0,
217            Some(b) => b,
218        };
219        if self.nodes.id(p) == id::DISC {
220            let (_, post, _) = self.nodes.disc_lists(p);
221            if !post.is_null() {
222                return if self.nodes.id(post) == id::GLYPH {
223                    self.char_protrusion(post, true)
224                } else {
225                    0
226                };
227            }
228        }
229        while !p.is_null() && self.pruned_after_break(p) {
230            p = self.nodes.next(p);
231        }
232        self.found_left(self.protchar_left_from(p))
233    }
234
235    /// Line breaking: the right protrusion of a line that ends with a
236    /// break at `cur_p` (null for the end of the paragraph). Back links of
237    /// the main list must be valid; `head` is the list head.
238    pub(crate) fn lb_right_protrusion(&self, cur_p: NodeRef, head: NodeRef) -> i32 {
239        if cur_p.is_null() {
240            // The final break: LuaTeX counts no right protrusion here (even
241            // with a zero \parfillskip).
242            return 0;
243        }
244        if self.nodes.id(cur_p) == id::DISC {
245            let (pre, _, _) = self.nodes.disc_lists(cur_p);
246            if !pre.is_null() {
247                let t = self.nodes.tail_of(pre);
248                return if self.nodes.id(t) == id::GLYPH {
249                    self.char_protrusion(t, false)
250                } else {
251                    0
252                };
253            }
254        }
255        let p = self.nodes.prev(cur_p);
256        self.found_right(self.protchar_right_back(p, head))
257    }
258
259    /// Makes the back links of the list after `head` consistent.
260    pub(crate) fn fix_back_links(&mut self, head: NodeRef) {
261        let mut prev = head;
262        let mut p = self.nodes.next(head);
263        while !p.is_null() {
264            self.nodes.set_prev(p, prev);
265            prev = p;
266            p = self.nodes.next(p);
267        }
268    }
269
270    fn new_margin_kern(&mut self, width: i32, side: u16) -> NodeRef {
271        self.new_node(
272            id::MARGIN_KERN,
273            side,
274            NodeData::MarginKern {
275                width,
276                glyph: NodeRef::NULL,
277            },
278        )
279    }
280
281    /// Post-line-break: the left margin kern for a line starting with
282    /// `line`, or null if there is no protrusion.
283    pub(crate) fn left_margin_kern(&mut self, line: NodeRef) -> NodeRef {
284        let w = self.found_left(self.protchar_left_from(line));
285        if w == 0 {
286            return NodeRef::NULL;
287        }
288        self.new_margin_kern(-w, LEFT_SIDE)
289    }
290
291    /// Post-line-break: inserts the right margin kern of the line that
292    /// starts after `pred` just before its node `q` (the break node, or
293    /// the last node of the paragraph).
294    pub(crate) fn insert_right_margin_kern(&mut self, pred: NodeRef, q: NodeRef) {
295        let mut v = Vec::new();
296        let mut p = self.nodes.next(pred);
297        while !p.is_null() && p != q {
298            v.push(p);
299            p = self.nodes.next(p);
300        }
301        if p != q || q.is_null() {
302            return;
303        }
304        let w = self.found_right(self.protchar_right_rev(v.iter().rev().copied()));
305        if w == 0 {
306            return;
307        }
308        let k = self.new_margin_kern(-w, RIGHT_SIDE);
309        let before = v.last().copied().unwrap_or(pred);
310        self.nodes.link(before, k);
311        self.nodes.link(k, q);
312    }
313
314    /// `\leftmarginkern n`, `\rightmarginkern n`: the width of the margin
315    /// kern at the start (end) of hbox `n`, passing over glue and
316    /// penalties; `None` if there is none (expands to `0pt`). A void box
317    /// or a vbox is a fatal error in LuaTeX.
318    pub fn margin_kern(&mut self, n: u32, left: bool) -> Option<i32> {
319        let b = self
320            .eqtb
321            .boxes
322            .get(n as usize)
323            .map_or(NodeRef::NULL, |b| b.val);
324        if b.is_null() || self.nodes.id(b) != id::HLIST {
325            self.luatex_fatal("marginkern", "a non-empty hbox expected");
326        }
327        let mut v = Vec::new();
328        let mut p = self.nodes.list_ptr(b);
329        while !p.is_null() {
330            v.push(p);
331            p = self.nodes.next(p);
332        }
333        if !left {
334            v.reverse();
335        }
336        let want = if left { LEFT_SIDE } else { RIGHT_SIDE };
337        for p in v {
338            match self.nodes.data(p) {
339                NodeData::MarginKern { width, .. } => {
340                    return (self.nodes.subtype(p) == want).then_some(*width);
341                }
342                NodeData::Glue { .. } | NodeData::Penalty { .. } => {}
343                _ => return None,
344            }
345        }
346        None
347    }
348
349    /// Removes the margin kerns from the top level of the list `p`
350    /// (unboxing an hbox drops them, as in LuaTeX); returns the new head.
351    pub fn strip_margin_kerns(&mut self, mut head: NodeRef) -> NodeRef {
352        while !head.is_null() && self.nodes.id(head) == id::MARGIN_KERN {
353            let n = self.nodes.next(head);
354            self.nodes.free(head);
355            head = n;
356        }
357        if head.is_null() {
358            return head;
359        }
360        self.nodes.set_prev(head, NodeRef::NULL);
361        let mut prev = head;
362        let mut p = self.nodes.next(head);
363        while !p.is_null() {
364            let n = self.nodes.next(p);
365            if self.nodes.id(p) == id::MARGIN_KERN {
366                self.nodes.free(p);
367                self.nodes.set_next(prev, n);
368                if !n.is_null() {
369                    self.nodes.set_prev(n, prev);
370                }
371            } else {
372                prev = p;
373            }
374            p = n;
375        }
376        head
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::round_div;
383
384    #[test]
385    fn rounding_halves_away_from_zero() {
386        assert_eq!(round_div(3_276_805, 10), 327_681);
387        assert_eq!(round_div(-3_276_805, 10), -327_681);
388        assert_eq!(round_div(1_966_083, 10), 196_608);
389        assert_eq!(round_div(7, 2), 4);
390        assert_eq!(round_div(-7, 2), -4);
391    }
392}