Skip to main content

retex_lua/
otfnodes.rs

1//! luaotfload's node processor: OpenType shaping applied to node lists.
2//!
3//! A *run* is a sequence of unprocessed glyphs of one OpenType font,
4//! possibly interrupted by discretionaries whose lists hold only such
5//! glyphs. Runs are shaped with [`retex_font::Shaper`] and the glyph nodes
6//! are replaced by the result (glyphs, ligatures with their components,
7//! font kerns). Shaped glyphs are marked as processed (subtype bit 256),
8//! as LuaTeX's `node.protect_glyphs` does.
9//!
10//! Discretionaries take part in shaping as luaotfload does. Observed with
11//! `luatex` (Latin Modern and Linux Libertine, see the regression tests):
12//!
13//! * The unbroken text (each discretionary replaced by its replacement
14//!   list) is shaped as a whole. Its glyphs that come entirely from text
15//!   before a discretionary stay in front of it, those entirely from text
16//!   after it stay behind it; everything in between (kerns across the
17//!   break point, ligatures that span it) becomes the replacement list. So
18//!   a kern between `n` and `t` gives `n{-||[k]}t`, and `of\-fice` gives
19//!   `o{f-|fi|ffi}ce`.
20//! * The pre-break list is the characters the replacement took from the
21//!   left followed by the pre-break text, shaped on their own; the
22//!   post-break list likewise. The glyph outside next to them only adds a
23//!   kern at the boundary: in bold Libertine (no `ffl` ligature)
24//!   `af\-fluent` gives `a{f-|f[k]|ff[k]}luent`, without an `fl` ligature
25//!   in the post-break list.
26//! * When two discretionaries follow each other directly, nothing is
27//!   shaped across them (`W\-\-o` gets no kern).
28
29use std::sync::Arc;
30
31use retex_engine::Engine;
32use retex_engine::nodes::{GlyphData, NodeData, NodeRef, glyph_sub, id, kern_sub};
33use retex_font::{LoadedFont, ShapedItem};
34
35use crate::luaotfload::loaded_font;
36
37/// Protection bit LuaTeX sets in the subtype of processed glyphs.
38const PROTECTED: u16 = 256;
39
40/// The unprocessed glyph `p` in font `font`, if it is one.
41fn is_raw_glyph(e: &Engine, p: NodeRef, font: u32) -> bool {
42    let n = e.nodes.get(p);
43    matches!(&n.data, NodeData::Glyph(g) if g.font == font && n.subtype < PROTECTED)
44}
45
46/// The nodes of the list starting at `head`, if all are unprocessed glyphs
47/// of `font`.
48fn glyph_list(e: &Engine, head: NodeRef, font: u32) -> Option<Vec<NodeRef>> {
49    let mut v = Vec::new();
50    let mut p = head;
51    while !p.is_null() {
52        if !is_raw_glyph(e, p, font) {
53            return None;
54        }
55        v.push(p);
56        p = e.nodes.next(p);
57    }
58    Some(v)
59}
60
61fn disc_lists(e: &Engine, d: NodeRef) -> [NodeRef; 3] {
62    match &e.nodes.get(d).data {
63        NodeData::Disc {
64            pre, post, replace, ..
65        } => [*pre, *post, *replace],
66        _ => [NodeRef::NULL; 3],
67    }
68}
69
70fn set_disc_lists(e: &mut Engine, d: NodeRef, lists: [NodeRef; 3]) {
71    if let NodeData::Disc {
72        pre, post, replace, ..
73    } = &mut e.nodes.get_mut(d).data
74    {
75        [*pre, *post, *replace] = lists;
76    }
77}
78
79/// How many characters of context [`context`] collects on each side.
80const CONTEXT_LEN: usize = 8;
81
82/// The text around a run that contextual lookups can see: from `p`
83/// onwards (or backwards), glue counts as a space and glyphs of `font` as
84/// their characters; anything else ends it. Observed with luaotfload (Inter's
85/// `calt`: `A) v` keeps the default `)`, `A)\kern0pt v`, `A)~v` and
86/// `A)\penalty0 v` get the capital form).
87fn context(e: &Engine, mut p: NodeRef, font: u32, forward: bool) -> Vec<u32> {
88    let mut out = Vec::new();
89    while !p.is_null() && out.len() < CONTEXT_LEN {
90        match &e.nodes.get(p).data {
91            NodeData::Glue { .. } => out.push(0x20),
92            NodeData::Glyph(g) if g.font == font => out.push(g.chr),
93            _ => break,
94        }
95        p = if forward {
96            e.nodes.next(p)
97        } else {
98            e.nodes.prev(p)
99        };
100    }
101    if !forward {
102        out.reverse();
103    }
104    out
105}
106
107fn chr(e: &Engine, p: NodeRef) -> u32 {
108    match &e.nodes.get(p).data {
109        NodeData::Glyph(g) => g.chr,
110        _ => 0,
111    }
112}
113
114/// A run: glyph segments separated by discretionaries
115/// (`segs.len() == discs.len() + 1`).
116struct DiscRun {
117    segs: Vec<Vec<NodeRef>>,
118    discs: Vec<NodeRef>,
119    /// Pre, post and replacement glyphs of each discretionary.
120    lists: Vec<[Vec<NodeRef>; 3]>,
121}
122
123/// Shapes all runs of unprocessed glyphs in the list starting at `head`
124/// (luaotfload's node mode). Returns the (possibly new) head of the list.
125pub fn process_list(e: &mut Engine, head: NodeRef) -> NodeRef {
126    let mut head = head;
127    let mut p = head;
128    while !p.is_null() {
129        let node = e.nodes.get(p);
130        match &node.data {
131            NodeData::Glyph(g) if node.subtype < PROTECTED => {
132                let font = g.font;
133                let Some(lf) = loaded_font(e, font) else {
134                    p = e.nodes.next(p);
135                    continue;
136                };
137                let mut run = DiscRun {
138                    segs: vec![vec![p]],
139                    discs: Vec::new(),
140                    lists: Vec::new(),
141                };
142                let mut q = e.nodes.next(p);
143                while !q.is_null() {
144                    if is_raw_glyph(e, q, font) {
145                        run.segs.last_mut().expect("segment").push(q);
146                    } else if e.nodes.id(q) == id::DISC {
147                        let [pre, post, rep] = disc_lists(e, q);
148                        let (Some(a), Some(b), Some(c)) = (
149                            glyph_list(e, pre, font),
150                            glyph_list(e, post, font),
151                            glyph_list(e, rep, font),
152                        ) else {
153                            break;
154                        };
155                        run.discs.push(q);
156                        run.lists.push([a, b, c]);
157                        run.segs.push(Vec::new());
158                    } else {
159                        break;
160                    }
161                    q = e.nodes.next(q);
162                }
163                let first = p;
164                let ctx = if lf.shaper.uses_context {
165                    (
166                        context(e, e.nodes.prev(first), font, false),
167                        context(e, q, font, true),
168                    )
169                } else {
170                    (Vec::new(), Vec::new())
171                };
172                let new_first = if run.discs.is_empty() {
173                    shape_plain(e, &lf, &run.segs[0], &ctx)
174                } else {
175                    shape_disc_run(e, &lf, &run, &ctx)
176                        .unwrap_or_else(|| shape_separately(e, &lf, &run))
177                };
178                if first == head {
179                    head = new_first;
180                }
181                p = q;
182            }
183            NodeData::Disc { .. } => {
184                let lists = disc_lists(e, p);
185                let new = lists.map(|l| if l.is_null() { l } else { process_list(e, l) });
186                set_disc_lists(e, p, new);
187                p = e.nodes.next(p);
188            }
189            _ => p = e.nodes.next(p),
190        }
191    }
192    head
193}
194
195/// Builds nodes for shaped `items`; `src[i]` is the original glyph node of
196/// input character `i`, whose data and attributes the new glyph inherits.
197fn build(e: &mut Engine, items: &[ShapedItem], src: &[NodeRef]) -> Vec<NodeRef> {
198    let mut out = Vec::with_capacity(items.len());
199    let Some(&last_src) = src.last() else {
200        return out;
201    };
202    let source = |k: usize| src.get(k).copied().unwrap_or(last_src);
203    for it in items {
204        match it {
205            ShapedItem::Glyph(g) => {
206                let s = source(g.cluster);
207                let mut data = match &e.nodes.get(s).data {
208                    NodeData::Glyph(d) => d.clone(),
209                    _ => GlyphData::default(),
210                };
211                let attr = e.nodes.get(s).attr.clone();
212                data.chr = g.code;
213                data.xoffset = g.x_offset;
214                data.yoffset = g.y_offset;
215                data.components = NodeRef::NULL;
216                let mut subtype = glyph_sub::CHARACTER | PROTECTED;
217                if !g.components.is_empty() {
218                    let mut ch = NodeRef::NULL;
219                    let mut tail = NodeRef::NULL;
220                    for (k, &c) in g.components.iter().enumerate() {
221                        let n = e.nodes.copy_node(source(g.cluster + k));
222                        if let NodeData::Glyph(gd) = &mut e.nodes.get_mut(n).data {
223                            gd.chr = c;
224                        }
225                        if ch.is_null() {
226                            ch = n;
227                        } else {
228                            e.nodes.link(tail, n);
229                        }
230                        tail = n;
231                    }
232                    data.components = ch;
233                    // luaotfload keeps the components but does not mark
234                    // the glyph as a ligature (observed subtype 256), so
235                    // box displays show no "(ligature …)".
236                    subtype = PROTECTED;
237                }
238                let n = e.nodes.alloc(id::GLYPH, subtype, NodeData::Glyph(data));
239                e.nodes.get_mut(n).attr = attr;
240                out.push(n);
241            }
242            ShapedItem::Kern(k) => {
243                let attr = e.nodes.get(last_src).attr.clone();
244                let n = e.nodes.new_kern(*k, kern_sub::FONT);
245                e.nodes.get_mut(n).attr = attr;
246                out.push(n);
247            }
248        }
249    }
250    // Kerns take the attributes of the glyph before them.
251    for i in 1..out.len() {
252        if e.nodes.id(out[i]) == id::KERN {
253            let a = e.nodes.get(out[i - 1]).attr.clone();
254            e.nodes.get_mut(out[i]).attr = a;
255        }
256    }
257    out
258}
259
260/// Links `nodes` into a list and returns its head.
261fn link_all(e: &mut Engine, nodes: &[NodeRef]) -> NodeRef {
262    for w in nodes.windows(2) {
263        e.nodes.link(w[0], w[1]);
264    }
265    if let Some(&f) = nodes.first() {
266        e.nodes.get_mut(f).prev = NodeRef::NULL;
267    }
268    if let Some(&l) = nodes.last() {
269        e.nodes.get_mut(l).next = NodeRef::NULL;
270    }
271    nodes.first().copied().unwrap_or(NodeRef::NULL)
272}
273
274/// Frees glyph nodes (each a single node in some list).
275fn free_glyphs(e: &mut Engine, nodes: &[NodeRef]) {
276    for &n in nodes {
277        e.nodes.get_mut(n).next = NodeRef::NULL;
278        e.nodes.flush_list(n);
279    }
280}
281
282/// Replaces the nodes from `first` to `last` (inclusive) of a list by
283/// `new`; returns the node now in the place of `first`.
284fn splice(e: &mut Engine, first: NodeRef, last: NodeRef, new: &[NodeRef]) -> NodeRef {
285    let prev = e.nodes.get(first).prev;
286    let next = e.nodes.next(last);
287    let mut cur = prev;
288    for &n in new {
289        if cur.is_null() {
290            e.nodes.get_mut(n).prev = NodeRef::NULL;
291        } else {
292            e.nodes.link(cur, n);
293        }
294        cur = n;
295    }
296    if cur.is_null() {
297        return next;
298    }
299    e.nodes.link(cur, next);
300    if !next.is_null() {
301        e.nodes.get_mut(next).prev = cur;
302    }
303    new.first().copied().unwrap_or(next)
304}
305
306/// Shapes a run of glyphs without discretionaries.
307/// Context characters before and after a run (see [`context`]).
308type Context = (Vec<u32>, Vec<u32>);
309
310fn shape_plain(e: &mut Engine, lf: &Arc<LoadedFont>, run: &[NodeRef], ctx: &Context) -> NodeRef {
311    let chars: Vec<u32> = run.iter().map(|&n| chr(e, n)).collect();
312    let items = lf
313        .shaper
314        .shape_in_context(&lf.file, &lf.tex, &ctx.0, &chars, &ctx.1);
315    let unchanged = items.len() == run.len()
316        && items.iter().zip(&chars).all(|(it, &c)| {
317            matches!(it, ShapedItem::Glyph(g) if g.code == c && g.components.is_empty()
318                && g.x_offset == 0 && g.y_offset == 0)
319        });
320    let first = run[0];
321    let last = *run.last().expect("non-empty run");
322    if unchanged {
323        for &n in run {
324            e.nodes.get_mut(n).subtype |= PROTECTED;
325        }
326        return first;
327    }
328    let new = build(e, &items, run);
329    let new_first = splice(e, first, last, &new);
330    free_glyphs(e, run);
331    new_first
332}
333
334fn n_chars(it: &ShapedItem) -> usize {
335    match it {
336        ShapedItem::Glyph(g) => g.components.len().max(1),
337        ShapedItem::Kern(_) => 0,
338    }
339}
340
341/// Shapes a run with discretionaries as described in the module
342/// documentation. Returns `None` (leaving the nodes untouched) when the
343/// run cannot be handled that way.
344fn shape_disc_run(
345    e: &mut Engine,
346    lf: &Arc<LoadedFont>,
347    run: &DiscRun,
348    ctx: &Context,
349) -> Option<NodeRef> {
350    let k = run.discs.len();
351    if run.segs[1..k].iter().any(Vec::is_empty) {
352        return None;
353    }
354    // The unbroken text and the start of each replacement in it.
355    let mut usrc: Vec<NodeRef> = Vec::new();
356    let mut starts = Vec::with_capacity(k);
357    for i in 0..k {
358        usrc.extend(&run.segs[i]);
359        starts.push(usrc.len());
360        usrc.extend(&run.lists[i][2]);
361    }
362    usrc.extend(&run.segs[k]);
363    let uchars: Vec<u32> = usrc.iter().map(|&n| chr(e, n)).collect();
364    let has = |c: u32| lf.tex.has_char(c);
365    if retex_font::normalize::normalize(&uchars, has) != uchars {
366        return None;
367    }
368    let shape = |src: &[NodeRef]| {
369        let chars: Vec<u32> = src.iter().map(|&n| chr(e, n)).collect();
370        lf.shaper.shape(&lf.file, &lf.tex, &chars)
371    };
372    let u = lf
373        .shaper
374        .shape_in_context(&lf.file, &lf.tex, &ctx.0, &uchars, &ctx.1);
375    let n = u.len();
376    let is_kern = |i: usize| matches!(u[i], ShapedItem::Kern(_));
377    struct Piece {
378        pre: (Vec<ShapedItem>, Vec<NodeRef>),
379        post: (Vec<ShapedItem>, Vec<NodeRef>),
380        lo: usize,
381        hi: usize,
382    }
383    let mut pieces: Vec<Piece> = Vec::with_capacity(k);
384    let mut prev_hi = 0usize;
385    let glyph_id = |it: &ShapedItem| match it {
386        ShapedItem::Glyph(g) => g.glyph,
387        ShapedItem::Kern(_) => None,
388    };
389    for (&a, lists) in starts.iter().zip(&run.lists) {
390        let [pre, post, rep] = lists;
391        let b = a + rep.len();
392        // The items of `u` entirely before the replacement stay in front
393        // of the discretionary; those entirely after it, behind.
394        let mut lo = 0;
395        while lo < n
396            && match &u[lo] {
397                ShapedItem::Glyph(g) => g.cluster + n_chars(&u[lo]) <= a,
398                ShapedItem::Kern(_) => true,
399            }
400        {
401            lo += 1;
402        }
403        while lo > 0 && is_kern(lo - 1) {
404            lo -= 1;
405        }
406        let mut s = 0;
407        while s < n
408            && match &u[n - 1 - s] {
409                ShapedItem::Glyph(g) => g.cluster >= b,
410                ShapedItem::Kern(_) => true,
411            }
412        {
413            s += 1;
414        }
415        while s > 0 && is_kern(n - s) {
416            s -= 1;
417        }
418        let hi = n - s;
419        if lo < prev_hi || hi < lo {
420            return None;
421        }
422        // The characters the replacement took from either side.
423        let ca = u[..lo]
424            .iter()
425            .filter_map(|it| match it {
426                ShapedItem::Glyph(g) => Some(g.cluster + n_chars(it)),
427                ShapedItem::Kern(_) => None,
428            })
429            .max()
430            .unwrap_or(0)
431            .min(a);
432        let cb = u[hi..]
433            .iter()
434            .filter_map(|it| match it {
435                ShapedItem::Glyph(g) => Some(g.cluster),
436                ShapedItem::Kern(_) => None,
437            })
438            .min()
439            .unwrap_or(usrc.len())
440            .max(b);
441        // Pre-break and post-break texts are shaped on their own; the
442        // glyphs outside only add a kern at the boundary.
443        let mut psrc = usrc[ca..a].to_vec();
444        psrc.extend(pre);
445        let mut qsrc = post.clone();
446        qsrc.extend(&usrc[b..cb]);
447        let mut pre_items = shape(&psrc);
448        let mut post_items = shape(&qsrc);
449        if let (Some(l), Some(r)) = (
450            lo.checked_sub(1).and_then(|i| glyph_id(&u[i])),
451            pre_items.first().and_then(glyph_id),
452        ) {
453            let k = lf.shaper.pair_kern(&lf.file, &lf.tex, l, r);
454            if k != 0 {
455                pre_items.insert(0, ShapedItem::Kern(k));
456            }
457        }
458        if let (Some(l), Some(r)) = (
459            post_items.last().and_then(glyph_id),
460            u.get(hi).and_then(glyph_id),
461        ) {
462            let k = lf.shaper.pair_kern(&lf.file, &lf.tex, l, r);
463            if k != 0 {
464                post_items.push(ShapedItem::Kern(k));
465            }
466        }
467        pieces.push(Piece {
468            pre: (pre_items, psrc),
469            post: (post_items, qsrc),
470            lo,
471            hi,
472        });
473        prev_hi = hi;
474    }
475    // Build the new list.
476    let mut out: Vec<NodeRef> = Vec::new();
477    let mut pos = 0usize;
478    for (i, pc) in pieces.iter().enumerate() {
479        out.extend(build(e, &u[pos..pc.lo], &usrc));
480        let pre = build(e, &pc.pre.0, &pc.pre.1);
481        let post = build(e, &pc.post.0, &pc.post.1);
482        let rep = build(e, &u[pc.lo..pc.hi], &usrc);
483        let d = run.discs[i];
484        let lists = [link_all(e, &pre), link_all(e, &post), link_all(e, &rep)];
485        set_disc_lists(e, d, lists);
486        out.push(d);
487        pos = pc.hi;
488    }
489    out.extend(build(e, &u[pos..], &usrc));
490    // Free the old glyphs, then splice.
491    let first = run.segs[0][0];
492    let last = run
493        .segs
494        .last()
495        .and_then(|s| s.last().copied())
496        .unwrap_or(*run.discs.last().expect("a discretionary"));
497    let new_first = splice(e, first, last, &out);
498    for seg in &run.segs {
499        free_glyphs(e, seg);
500    }
501    for l in &run.lists {
502        for part in l {
503            free_glyphs(e, part);
504        }
505    }
506    Some(new_first)
507}
508
509/// Shapes the segments and discretionary lists of `run` independently.
510fn shape_separately(e: &mut Engine, lf: &Arc<LoadedFont>, run: &DiscRun) -> NodeRef {
511    let mut first = NodeRef::NULL;
512    for (i, seg) in run.segs.iter().enumerate() {
513        if !seg.is_empty() {
514            let f = shape_plain(e, lf, seg, &(Vec::new(), Vec::new()));
515            if i == 0 {
516                first = f;
517            }
518        }
519        if let Some(&d) = run.discs.get(i) {
520            let lists = disc_lists(e, d);
521            let new = lists.map(|l| if l.is_null() { l } else { process_list(e, l) });
522            set_disc_lists(e, d, new);
523        }
524    }
525    first
526}