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