rustybuzz/hb/
aat_layout_morx_table.rs

1use ttf_parser::{apple_layout, morx, FromData, GlyphId, LazyArray32};
2
3use super::aat_layout::*;
4use super::aat_map::{hb_aat_map_builder_t, hb_aat_map_t};
5use super::buffer::hb_buffer_t;
6use super::ot_shape_plan::hb_ot_shape_plan_t;
7use super::{hb_font_t, hb_glyph_info_t};
8
9// Chain::compile_flags in harfbuzz
10pub fn compile_flags(face: &hb_font_t, builder: &hb_aat_map_builder_t) -> Option<hb_aat_map_t> {
11    let mut map = hb_aat_map_t::default();
12
13    for chain in face.tables().morx.as_ref()?.chains {
14        let mut flags = chain.default_flags;
15        for feature in chain.features {
16            // Check whether this type/setting pair was requested in the map,
17            // and if so, apply its flags.
18
19            if builder.has_feature(feature.kind, feature.setting) {
20                flags &= feature.disable_flags;
21                flags |= feature.enable_flags;
22            } else if feature.kind == HB_AAT_LAYOUT_FEATURE_TYPE_LETTER_CASE as u16
23                && feature.setting == u16::from(HB_AAT_LAYOUT_FEATURE_SELECTOR_SMALL_CAPS)
24            {
25                // Deprecated. https://github.com/harfbuzz/harfbuzz/issues/1342
26                let ok = builder.has_feature(
27                    HB_AAT_LAYOUT_FEATURE_TYPE_LOWER_CASE as u16,
28                    u16::from(HB_AAT_LAYOUT_FEATURE_SELECTOR_LOWER_CASE_SMALL_CAPS),
29                );
30                if ok {
31                    flags &= feature.disable_flags;
32                    flags |= feature.enable_flags;
33                }
34            }
35        }
36
37        map.chain_flags.push(flags);
38    }
39
40    Some(map)
41}
42
43// Chain::apply in harfbuzz
44pub fn apply(plan: &hb_ot_shape_plan_t, face: &hb_font_t, buffer: &mut hb_buffer_t) -> Option<()> {
45    for (chain_idx, chain) in face.tables().morx.as_ref()?.chains.into_iter().enumerate() {
46        let flags = plan.aat_map.chain_flags[chain_idx];
47        for subtable in chain.subtables {
48            if subtable.feature_flags & flags == 0 {
49                continue;
50            }
51
52            if !subtable.coverage.is_all_directions()
53                && buffer.direction.is_vertical() != subtable.coverage.is_vertical()
54            {
55                continue;
56            }
57
58            // Buffer contents is always in logical direction.  Determine if
59            // we need to reverse before applying this subtable.  We reverse
60            // back after if we did reverse indeed.
61            //
62            // Quoting the spec:
63            // """
64            // Bits 28 and 30 of the coverage field control the order in which
65            // glyphs are processed when the subtable is run by the layout engine.
66            // Bit 28 is used to indicate if the glyph processing direction is
67            // the same as logical order or layout order. Bit 30 is used to
68            // indicate whether glyphs are processed forwards or backwards within
69            // that order.
70            //
71            // Bit 30   Bit 28   Interpretation for Horizontal Text
72            //      0        0   The subtable is processed in layout order
73            //                   (the same order as the glyphs, which is
74            //                   always left-to-right).
75            //      1        0   The subtable is processed in reverse layout order
76            //                   (the order opposite that of the glyphs, which is
77            //                   always right-to-left).
78            //      0        1   The subtable is processed in logical order
79            //                   (the same order as the characters, which may be
80            //                   left-to-right or right-to-left).
81            //      1        1   The subtable is processed in reverse logical order
82            //                   (the order opposite that of the characters, which
83            //                   may be right-to-left or left-to-right).
84
85            let reverse = if subtable.coverage.is_logical() {
86                subtable.coverage.is_backwards()
87            } else {
88                subtable.coverage.is_backwards() != buffer.direction.is_backward()
89            };
90
91            if reverse {
92                buffer.reverse();
93            }
94
95            apply_subtable(&subtable.kind, buffer, face);
96
97            if reverse {
98                buffer.reverse();
99            }
100        }
101    }
102
103    Some(())
104}
105
106trait driver_context_t<T: FromData> {
107    fn in_place(&self) -> bool;
108    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<T>) -> bool;
109    fn is_actionable(
110        &self,
111        entry: &apple_layout::GenericStateEntry<T>,
112        buffer: &hb_buffer_t,
113    ) -> bool;
114    fn transition(
115        &mut self,
116        entry: &apple_layout::GenericStateEntry<T>,
117        buffer: &mut hb_buffer_t,
118    ) -> Option<()>;
119}
120
121const START_OF_TEXT: u16 = 0;
122
123fn drive<T: FromData>(
124    machine: &apple_layout::ExtendedStateTable<T>,
125    c: &mut dyn driver_context_t<T>,
126    buffer: &mut hb_buffer_t,
127) {
128    if !c.in_place() {
129        buffer.clear_output();
130    }
131
132    let mut state = START_OF_TEXT;
133    buffer.idx = 0;
134    loop {
135        let class = if buffer.idx < buffer.len {
136            machine
137                .class(buffer.info[buffer.idx].as_glyph())
138                .unwrap_or(1)
139        } else {
140            u16::from(apple_layout::class::END_OF_TEXT)
141        };
142
143        let entry: apple_layout::GenericStateEntry<T> = match machine.entry(state, class) {
144            Some(v) => v,
145            None => break,
146        };
147
148        let next_state = entry.new_state;
149
150        // Conditions under which it's guaranteed safe-to-break before current glyph:
151        //
152        // 1. There was no action in this transition; and
153        //
154        // 2. If we break before current glyph, the results will be the same. That
155        //    is guaranteed if:
156        //
157        //    2a. We were already in start-of-text state; or
158        //
159        //    2b. We are epsilon-transitioning to start-of-text state; or
160        //
161        //    2c. Starting from start-of-text state seeing current glyph:
162        //
163        //        2c'. There won't be any actions; and
164        //
165        //        2c". We would end up in the same state that we were going to end up
166        //             in now, including whether epsilon-transitioning.
167        //
168        //    and
169        //
170        // 3. If we break before current glyph, there won't be any end-of-text action
171        //    after previous glyph.
172        //
173        // This triples the transitions we need to look up, but is worth returning
174        // granular unsafe-to-break results. See eg.:
175        //
176        //   https://github.com/harfbuzz/harfbuzz/issues/2860
177
178        let is_safe_to_break_extra = || {
179            // 2c
180            let wouldbe_entry = match machine.entry(START_OF_TEXT, class) {
181                Some(v) => v,
182                None => return false,
183            };
184
185            // 2c'
186            if c.is_actionable(&wouldbe_entry, &buffer) {
187                return false;
188            }
189
190            // 2c"
191            return next_state == wouldbe_entry.new_state
192                && c.can_advance(&entry) == c.can_advance(&wouldbe_entry);
193        };
194
195        let is_safe_to_break = || {
196            // 1
197            if c.is_actionable(&entry, &buffer) {
198                return false;
199            }
200
201            // 2
202            let ok = state == START_OF_TEXT
203                || (!c.can_advance(&entry) && next_state == START_OF_TEXT)
204                || is_safe_to_break_extra();
205            if !ok {
206                return false;
207            }
208
209            // 3
210            let end_entry = match machine.entry(state, u16::from(apple_layout::class::END_OF_TEXT))
211            {
212                Some(v) => v,
213                None => return false,
214            };
215            return !c.is_actionable(&end_entry, &buffer);
216        };
217
218        if !is_safe_to_break() && buffer.backtrack_len() > 0 && buffer.idx < buffer.len {
219            buffer.unsafe_to_break_from_outbuffer(
220                Some(buffer.backtrack_len() - 1),
221                Some(buffer.idx + 1),
222            );
223        }
224
225        c.transition(&entry, buffer);
226
227        state = next_state;
228
229        if buffer.idx >= buffer.len || !buffer.successful {
230            break;
231        }
232
233        if c.can_advance(&entry) {
234            buffer.next_glyph();
235        } else {
236            if buffer.max_ops <= 0 {
237                buffer.next_glyph();
238            }
239            buffer.max_ops -= 1;
240        }
241    }
242
243    if !c.in_place() {
244        buffer.sync();
245    }
246}
247
248fn apply_subtable(kind: &morx::SubtableKind, buffer: &mut hb_buffer_t, face: &hb_font_t) {
249    match kind {
250        morx::SubtableKind::Rearrangement(ref table) => {
251            let mut c = RearrangementCtx { start: 0, end: 0 };
252
253            drive::<()>(table, &mut c, buffer);
254        }
255        morx::SubtableKind::Contextual(ref table) => {
256            let mut c = ContextualCtx {
257                mark_set: false,
258                face_if_has_glyph_classes:
259                    matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
260                        .then_some(face),
261                mark: 0,
262                table,
263            };
264
265            drive::<morx::ContextualEntryData>(&table.state, &mut c, buffer);
266        }
267        morx::SubtableKind::Ligature(ref table) => {
268            let mut c = LigatureCtx {
269                table,
270                match_length: 0,
271                match_positions: [0; LIGATURE_MAX_MATCHES],
272            };
273
274            drive::<u16>(&table.state, &mut c, buffer);
275        }
276        morx::SubtableKind::NonContextual(ref lookup) => {
277            let face_if_has_glyph_classes =
278                matches!(face.tables().gdef, Some(gdef) if gdef.has_glyph_classes())
279                    .then_some(face);
280            for info in &mut buffer.info {
281                if let Some(replacement) = lookup.value(info.as_glyph()) {
282                    info.glyph_id = u32::from(replacement);
283                    if let Some(face) = face_if_has_glyph_classes {
284                        info.set_glyph_props(face.glyph_props(GlyphId(replacement)));
285                    }
286                }
287            }
288        }
289        morx::SubtableKind::Insertion(ref table) => {
290            let mut c = InsertionCtx {
291                mark: 0,
292                glyphs: table.glyphs,
293            };
294
295            drive::<morx::InsertionEntryData>(&table.state, &mut c, buffer);
296        }
297    }
298}
299
300struct RearrangementCtx {
301    start: usize,
302    end: usize,
303}
304
305impl RearrangementCtx {
306    const MARK_FIRST: u16 = 0x8000;
307    const DONT_ADVANCE: u16 = 0x4000;
308    const MARK_LAST: u16 = 0x2000;
309    const VERB: u16 = 0x000F;
310}
311
312impl driver_context_t<()> for RearrangementCtx {
313    fn in_place(&self) -> bool {
314        true
315    }
316
317    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<()>) -> bool {
318        entry.flags & Self::DONT_ADVANCE == 0
319    }
320
321    fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<()>, _: &hb_buffer_t) -> bool {
322        entry.flags & Self::VERB != 0 && self.start < self.end
323    }
324
325    fn transition(
326        &mut self,
327        entry: &apple_layout::GenericStateEntry<()>,
328        buffer: &mut hb_buffer_t,
329    ) -> Option<()> {
330        let flags = entry.flags;
331
332        if flags & Self::MARK_FIRST != 0 {
333            self.start = buffer.idx;
334        }
335
336        if flags & Self::MARK_LAST != 0 {
337            self.end = (buffer.idx + 1).min(buffer.len);
338        }
339
340        if flags & Self::VERB != 0 && self.start < self.end {
341            // The following map has two nibbles, for start-side
342            // and end-side. Values of 0,1,2 mean move that many
343            // to the other side. Value of 3 means move 2 and
344            // flip them.
345            const MAP: [u8; 16] = [
346                0x00, // 0  no change
347                0x10, // 1  Ax => xA
348                0x01, // 2  xD => Dx
349                0x11, // 3  AxD => DxA
350                0x20, // 4  ABx => xAB
351                0x30, // 5  ABx => xBA
352                0x02, // 6  xCD => CDx
353                0x03, // 7  xCD => DCx
354                0x12, // 8  AxCD => CDxA
355                0x13, // 9  AxCD => DCxA
356                0x21, // 10 ABxD => DxAB
357                0x31, // 11 ABxD => DxBA
358                0x22, // 12 ABxCD => CDxAB
359                0x32, // 13 ABxCD => CDxBA
360                0x23, // 14 ABxCD => DCxAB
361                0x33, // 15 ABxCD => DCxBA
362            ];
363
364            let m = MAP[usize::from(flags & Self::VERB)];
365            let l = 2.min(m >> 4) as usize;
366            let r = 2.min(m & 0x0F) as usize;
367            let reverse_l = 3 == (m >> 4);
368            let reverse_r = 3 == (m & 0x0F);
369
370            if self.end - self.start >= l + r {
371                buffer.merge_clusters(self.start, (buffer.idx + 1).min(buffer.len));
372                buffer.merge_clusters(self.start, self.end);
373
374                let mut buf = [hb_glyph_info_t::default(); 4];
375
376                for (i, glyph_info) in buf[..l].iter_mut().enumerate() {
377                    *glyph_info = buffer.info[self.start + i];
378                }
379
380                for i in 0..r {
381                    buf[i + 2] = buffer.info[self.end - r + i];
382                }
383
384                if l > r {
385                    for i in 0..(self.end - self.start - l - r) {
386                        buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
387                    }
388                } else if l < r {
389                    for i in (0..(self.end - self.start - l - r)).rev() {
390                        buffer.info[self.start + r + i] = buffer.info[self.start + l + i];
391                    }
392                }
393
394                for i in 0..r {
395                    buffer.info[self.start + i] = buf[2 + i];
396                }
397
398                for i in 0..l {
399                    buffer.info[self.end - l + i] = buf[i];
400                }
401
402                if reverse_l {
403                    buffer.info.swap(self.end - 1, self.end - 2);
404                }
405
406                if reverse_r {
407                    buffer.info.swap(self.start, self.start + 1);
408                }
409            }
410        }
411
412        Some(())
413    }
414}
415
416struct ContextualCtx<'a> {
417    mark_set: bool,
418    face_if_has_glyph_classes: Option<&'a hb_font_t<'a>>,
419    mark: usize,
420    table: &'a morx::ContextualSubtable<'a>,
421}
422
423impl ContextualCtx<'_> {
424    const SET_MARK: u16 = 0x8000;
425    const DONT_ADVANCE: u16 = 0x4000;
426}
427
428impl driver_context_t<morx::ContextualEntryData> for ContextualCtx<'_> {
429    fn in_place(&self) -> bool {
430        true
431    }
432
433    fn can_advance(
434        &self,
435        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
436    ) -> bool {
437        entry.flags & Self::DONT_ADVANCE == 0
438    }
439
440    fn is_actionable(
441        &self,
442        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
443        buffer: &hb_buffer_t,
444    ) -> bool {
445        if buffer.idx == buffer.len && !self.mark_set {
446            return false;
447        }
448
449        entry.extra.mark_index != 0xFFFF || entry.extra.current_index != 0xFFFF
450    }
451
452    fn transition(
453        &mut self,
454        entry: &apple_layout::GenericStateEntry<morx::ContextualEntryData>,
455        buffer: &mut hb_buffer_t,
456    ) -> Option<()> {
457        // Looks like CoreText applies neither mark nor current substitution for
458        // end-of-text if mark was not explicitly set.
459        if buffer.idx == buffer.len && !self.mark_set {
460            return Some(());
461        }
462
463        let mut replacement = None;
464
465        if entry.extra.mark_index != 0xFFFF {
466            let lookup = self.table.lookup(u32::from(entry.extra.mark_index))?;
467            replacement = lookup.value(buffer.info[self.mark].as_glyph());
468        }
469
470        if let Some(replacement) = replacement {
471            buffer.unsafe_to_break(Some(self.mark), Some((buffer.idx + 1).min(buffer.len)));
472            buffer.info[self.mark].glyph_id = u32::from(replacement);
473
474            if let Some(face) = self.face_if_has_glyph_classes {
475                buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
476            }
477        }
478
479        replacement = None;
480        let idx = buffer.idx.min(buffer.len - 1);
481        if entry.extra.current_index != 0xFFFF {
482            let lookup = self.table.lookup(u32::from(entry.extra.current_index))?;
483            replacement = lookup.value(buffer.info[idx].as_glyph());
484        }
485
486        if let Some(replacement) = replacement {
487            buffer.info[idx].glyph_id = u32::from(replacement);
488
489            if let Some(face) = self.face_if_has_glyph_classes {
490                buffer.info[self.mark].set_glyph_props(face.glyph_props(GlyphId(replacement)));
491            }
492        }
493
494        if entry.flags & Self::SET_MARK != 0 {
495            self.mark_set = true;
496            self.mark = buffer.idx;
497        }
498
499        Some(())
500    }
501}
502
503struct InsertionCtx<'a> {
504    mark: u32,
505    glyphs: LazyArray32<'a, GlyphId>,
506}
507
508impl InsertionCtx<'_> {
509    const SET_MARK: u16 = 0x8000;
510    const DONT_ADVANCE: u16 = 0x4000;
511    const CURRENT_INSERT_BEFORE: u16 = 0x0800;
512    const MARKED_INSERT_BEFORE: u16 = 0x0400;
513    const CURRENT_INSERT_COUNT: u16 = 0x03E0;
514    const MARKED_INSERT_COUNT: u16 = 0x001F;
515}
516
517impl driver_context_t<morx::InsertionEntryData> for InsertionCtx<'_> {
518    fn in_place(&self) -> bool {
519        false
520    }
521
522    fn can_advance(
523        &self,
524        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
525    ) -> bool {
526        entry.flags & Self::DONT_ADVANCE == 0
527    }
528
529    fn is_actionable(
530        &self,
531        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
532        _: &hb_buffer_t,
533    ) -> bool {
534        (entry.flags & (Self::CURRENT_INSERT_COUNT | Self::MARKED_INSERT_COUNT) != 0)
535            && (entry.extra.current_insert_index != 0xFFFF
536                || entry.extra.marked_insert_index != 0xFFFF)
537    }
538
539    fn transition(
540        &mut self,
541        entry: &apple_layout::GenericStateEntry<morx::InsertionEntryData>,
542        buffer: &mut hb_buffer_t,
543    ) -> Option<()> {
544        let flags = entry.flags;
545        let mark_loc = buffer.out_len;
546
547        if entry.extra.marked_insert_index != 0xFFFF {
548            let count = flags & Self::MARKED_INSERT_COUNT;
549            buffer.max_ops -= i32::from(count);
550            if buffer.max_ops <= 0 {
551                return Some(());
552            }
553
554            let start = entry.extra.marked_insert_index;
555            let before = flags & Self::MARKED_INSERT_BEFORE != 0;
556
557            let end = buffer.out_len;
558            buffer.move_to(self.mark as usize);
559
560            if buffer.idx < buffer.len && !before {
561                buffer.copy_glyph();
562            }
563
564            // TODO We ignore KashidaLike setting.
565            for i in 0..count {
566                let i = u32::from(start + i);
567                buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
568            }
569
570            if buffer.idx < buffer.len && !before {
571                buffer.skip_glyph();
572            }
573
574            buffer.move_to(end + usize::from(count));
575
576            buffer.unsafe_to_break_from_outbuffer(
577                Some(self.mark as usize),
578                Some((buffer.idx + 1).min(buffer.len)),
579            );
580        }
581
582        if flags & Self::SET_MARK != 0 {
583            self.mark = mark_loc as u32;
584        }
585
586        if entry.extra.current_insert_index != 0xFFFF {
587            let count = (flags & Self::CURRENT_INSERT_COUNT) >> 5;
588            buffer.max_ops -= i32::from(count);
589            if buffer.max_ops < 0 {
590                return Some(());
591            }
592
593            let start = entry.extra.current_insert_index;
594            let before = flags & Self::CURRENT_INSERT_BEFORE != 0;
595            let end = buffer.out_len;
596
597            if buffer.idx < buffer.len && !before {
598                buffer.copy_glyph();
599            }
600
601            // TODO We ignore KashidaLike setting.
602            for i in 0..count {
603                let i = u32::from(start + i);
604                buffer.output_glyph(u32::from(self.glyphs.get(i)?.0));
605            }
606
607            if buffer.idx < buffer.len && !before {
608                buffer.skip_glyph();
609            }
610
611            // Humm. Not sure where to move to. There's this wording under
612            // DontAdvance flag:
613            //
614            // "If set, don't update the glyph index before going to the new state.
615            // This does not mean that the glyph pointed to is the same one as
616            // before. If you've made insertions immediately downstream of the
617            // current glyph, the next glyph processed would in fact be the first
618            // one inserted."
619            //
620            // This suggests that if DontAdvance is NOT set, we should move to
621            // end+count. If it *was*, then move to end, such that newly inserted
622            // glyphs are now visible.
623            //
624            // https://github.com/harfbuzz/harfbuzz/issues/1224#issuecomment-427691417
625            buffer.move_to(if flags & Self::DONT_ADVANCE != 0 {
626                end
627            } else {
628                end + usize::from(count)
629            });
630        }
631
632        Some(())
633    }
634}
635
636const LIGATURE_MAX_MATCHES: usize = 64;
637
638struct LigatureCtx<'a> {
639    table: &'a morx::LigatureSubtable<'a>,
640    match_length: usize,
641    match_positions: [usize; LIGATURE_MAX_MATCHES],
642}
643
644impl LigatureCtx<'_> {
645    const SET_COMPONENT: u16 = 0x8000;
646    const DONT_ADVANCE: u16 = 0x4000;
647    const PERFORM_ACTION: u16 = 0x2000;
648
649    const LIG_ACTION_LAST: u32 = 0x80000000;
650    const LIG_ACTION_STORE: u32 = 0x40000000;
651    const LIG_ACTION_OFFSET: u32 = 0x3FFFFFFF;
652}
653
654impl driver_context_t<u16> for LigatureCtx<'_> {
655    fn in_place(&self) -> bool {
656        false
657    }
658
659    fn can_advance(&self, entry: &apple_layout::GenericStateEntry<u16>) -> bool {
660        entry.flags & Self::DONT_ADVANCE == 0
661    }
662
663    fn is_actionable(&self, entry: &apple_layout::GenericStateEntry<u16>, _: &hb_buffer_t) -> bool {
664        entry.flags & Self::PERFORM_ACTION != 0
665    }
666
667    fn transition(
668        &mut self,
669        entry: &apple_layout::GenericStateEntry<u16>,
670        buffer: &mut hb_buffer_t,
671    ) -> Option<()> {
672        if entry.flags & Self::SET_COMPONENT != 0 {
673            // Never mark same index twice, in case DONT_ADVANCE was used...
674            if self.match_length != 0
675                && self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES]
676                    == buffer.out_len
677            {
678                self.match_length -= 1;
679            }
680
681            self.match_positions[self.match_length % LIGATURE_MAX_MATCHES] = buffer.out_len;
682            self.match_length += 1;
683        }
684
685        if entry.flags & Self::PERFORM_ACTION != 0 {
686            let end = buffer.out_len;
687
688            if self.match_length == 0 {
689                return Some(());
690            }
691
692            if buffer.idx >= buffer.len {
693                return Some(()); // TODO: Work on previous instead?
694            }
695
696            let mut cursor = self.match_length;
697
698            let mut ligature_actions_index = entry.extra;
699            let mut ligature_idx = 0;
700            loop {
701                if cursor == 0 {
702                    // Stack underflow. Clear the stack.
703                    self.match_length = 0;
704                    break;
705                }
706
707                cursor -= 1;
708                buffer.move_to(self.match_positions[cursor % LIGATURE_MAX_MATCHES]);
709
710                // We cannot use ? in this loop, because we must call
711                // buffer.move_to(end) in the end.
712                let action = match self
713                    .table
714                    .ligature_actions
715                    .get(u32::from(ligature_actions_index))
716                {
717                    Some(v) => v,
718                    None => break,
719                };
720
721                let mut uoffset = action & Self::LIG_ACTION_OFFSET;
722                if uoffset & 0x20000000 != 0 {
723                    uoffset |= 0xC0000000; // Sign-extend.
724                }
725
726                let offset = uoffset as i32;
727                let component_idx = (buffer.cur(0).glyph_id as i32 + offset) as u32;
728                ligature_idx += match self.table.components.get(component_idx) {
729                    Some(v) => v,
730                    None => break,
731                };
732
733                if (action & (Self::LIG_ACTION_STORE | Self::LIG_ACTION_LAST)) != 0 {
734                    let lig = match self.table.ligatures.get(u32::from(ligature_idx)) {
735                        Some(v) => v,
736                        None => break,
737                    };
738
739                    buffer.replace_glyph(u32::from(lig.0));
740
741                    let lig_end =
742                        self.match_positions[(self.match_length - 1) % LIGATURE_MAX_MATCHES] + 1;
743                    // Now go and delete all subsequent components.
744                    while self.match_length - 1 > cursor {
745                        self.match_length -= 1;
746                        buffer.move_to(
747                            self.match_positions[self.match_length % LIGATURE_MAX_MATCHES],
748                        );
749                        buffer.replace_glyph(0xFFFF);
750                    }
751
752                    buffer.move_to(lig_end);
753                    buffer.merge_out_clusters(
754                        self.match_positions[cursor % LIGATURE_MAX_MATCHES],
755                        buffer.out_len,
756                    );
757                }
758
759                ligature_actions_index += 1;
760
761                if action & Self::LIG_ACTION_LAST != 0 {
762                    break;
763                }
764            }
765
766            buffer.move_to(end);
767        }
768
769        Some(())
770    }
771}