Skip to main content

harfrust/hb/aat/
layout_kerx_table.rs

1use super::layout::DELETED_GLYPH;
2use crate::hb::aat::layout_common::{
3    get_class, AatApplyContext, ClassCache, TypedCollectGlyphs, START_OF_TEXT,
4};
5use crate::hb::{
6    buffer::*,
7    ot_layout::TableIndex,
8    ot_layout_common::lookup_flags,
9    ot_layout_gpos_table::attach_type,
10    ot_layout_gsubgpos::{skipping_iterator_t, OT::hb_ot_apply_context_t},
11};
12use crate::U32Set;
13use alloc::boxed::Box;
14use core::convert::TryFrom;
15use read_fonts::{
16    tables::{
17        aat,
18        ankr::Ankr,
19        kerx::{
20            Subtable, Subtable0, Subtable1, Subtable2, Subtable4, Subtable4Actions, Subtable6,
21            SubtableKind,
22        },
23    },
24    types::{BigEndian, FixedSize, GlyphId},
25};
26
27pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> {
28    c.buffer.unsafe_to_concat(None, None);
29
30    c.setup_buffer_glyph_set();
31
32    let (kerx, subtable_caches) = c.face.aat_tables.kerx.as_ref()?;
33
34    let mut subtable_idx = 0;
35
36    let mut seen_cross_stream = false;
37    for subtable in kerx.subtables().iter() {
38        let Ok(subtable) = subtable else {
39            continue;
40        };
41
42        let subtable_cache = subtable_caches.get(subtable_idx);
43        let Some(subtable_cache) = subtable_cache.as_ref() else {
44            break;
45        };
46        subtable_idx += 1;
47
48        // We don't handle variations
49        if subtable.is_variable() {
50            continue;
51        }
52
53        if c.buffer.direction.is_horizontal() != subtable.is_horizontal() {
54            continue;
55        }
56
57        c.first_set = Some(&subtable_cache.first_set);
58        c.second_set = Some(&subtable_cache.second_set);
59        c.machine_class_cache = Some(&subtable_cache.class_cache);
60        c.start_end_safe_to_break = subtable_cache.start_end_safe_to_break;
61
62        if !c.buffer_intersects_machine() {
63            continue;
64        }
65
66        let reverse = c.buffer.direction.is_backward();
67
68        if !seen_cross_stream && subtable.is_cross_stream() {
69            seen_cross_stream = true;
70
71            // Attach all glyphs into a chain.
72            for pos in &mut c.buffer.pos {
73                pos.set_attach_type(attach_type::CURSIVE);
74                pos.set_attach_chain(if c.buffer.direction.is_forward() {
75                    -1
76                } else {
77                    1
78                });
79                // We intentionally don't set BufferScratchFlags::HAS_GPOS_ATTACHMENT,
80                // since there needs to be a non-zero attachment for post-positioning to
81                // be needed.
82            }
83        }
84
85        let Ok(kind) = subtable.kind() else {
86            continue;
87        };
88
89        if reverse != c.buffer_is_reversed {
90            c.reverse_buffer();
91        }
92
93        match &kind {
94            SubtableKind::Format0(format0) => {
95                if !c.plan.requested_kerning {
96                    continue;
97                }
98                apply_simple_kerning(c, &subtable, format0);
99            }
100            SubtableKind::Format1(format1) => {
101                let mut driver = Driver1 {
102                    stack: [0; 8],
103                    depth: 0,
104                };
105                apply_state_machine_kerning(
106                    c,
107                    &subtable,
108                    format1,
109                    &format1.state_table,
110                    &mut driver,
111                );
112            }
113            SubtableKind::Format2(format2) => {
114                if !c.plan.requested_kerning {
115                    continue;
116                }
117                apply_simple_kerning(c, &subtable, format2);
118            }
119            SubtableKind::Format4(format4) => {
120                let mut driver = Driver4 {
121                    mark_set: false,
122                    mark: 0,
123                    ankr_table: c.face.aat_tables.ankr.clone(),
124                };
125                apply_state_machine_kerning(
126                    c,
127                    &subtable,
128                    format4,
129                    &format4.state_table,
130                    &mut driver,
131                );
132            }
133            SubtableKind::Format6(format6) => {
134                if !c.plan.requested_kerning {
135                    continue;
136                }
137                apply_simple_kerning(c, &subtable, format6);
138            }
139        }
140    }
141    if c.buffer_is_reversed {
142        c.reverse_buffer();
143    }
144
145    Some(())
146}
147
148pub trait SimpleKerning {
149    fn simple_kerning(&self, left: GlyphId, right: GlyphId) -> Option<i32>;
150    fn collect_glyphs(&self, _first_set: &mut U32Set, _second_set: &mut U32Set, _num_glyphs: u32);
151}
152
153impl SimpleKerning for Subtable0<'_> {
154    fn simple_kerning(&self, left: GlyphId, right: GlyphId) -> Option<i32> {
155        self.kerning(left, right)
156    }
157    fn collect_glyphs(&self, first_set: &mut U32Set, second_set: &mut U32Set, _num_glyphs: u32) {
158        for &pair in self.pairs() {
159            first_set.insert(pair.left.get().to_u32());
160            second_set.insert(pair.right.get().to_u32());
161        }
162    }
163}
164
165impl SimpleKerning for Subtable2<'_> {
166    fn simple_kerning(&self, left: GlyphId, right: GlyphId) -> Option<i32> {
167        self.kerning(left, right)
168    }
169    fn collect_glyphs(&self, first_set: &mut U32Set, second_set: &mut U32Set, num_glyphs: u32) {
170        let left_classes = &self.left_offset_table;
171        let right_classes = &self.right_offset_table;
172
173        left_classes.collect_glyphs(first_set, num_glyphs);
174        right_classes.collect_glyphs(second_set, num_glyphs);
175    }
176}
177
178impl SimpleKerning for Subtable6<'_> {
179    fn simple_kerning(&self, left: GlyphId, right: GlyphId) -> Option<i32> {
180        self.kerning(left, right)
181    }
182    fn collect_glyphs(&self, first_set: &mut U32Set, second_set: &mut U32Set, num_glyphs: u32) {
183        match &self {
184            Self::ShortValues(rows, columns, ..) => {
185                rows.collect_glyphs(first_set, num_glyphs);
186                columns.collect_glyphs(second_set, num_glyphs);
187            }
188            Self::LongValues(rows, columns, ..) => {
189                rows.collect_glyphs(first_set, num_glyphs);
190                columns.collect_glyphs(second_set, num_glyphs);
191            }
192        }
193    }
194}
195
196fn apply_simple_kerning<T: SimpleKerning>(c: &mut AatApplyContext, subtable: &Subtable, kind: &T) {
197    let scale = c.scale;
198    let mut ctx = hb_ot_apply_context_t::new(TableIndex::GPOS, c.face, c.scale, c.buffer);
199    ctx.set_lookup_mask(c.plan.kern_mask);
200    ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS);
201    ctx.update_matchers();
202
203    let horizontal = ctx.buffer.direction.is_horizontal();
204    let cross_stream = subtable.is_cross_stream();
205    let use_x_scale = horizontal ^ cross_stream;
206
207    let first_set = c.first_set.as_ref().unwrap();
208    let second_set = c.second_set.as_ref().unwrap();
209
210    let mut i = 0;
211    let mut iter = skipping_iterator_t::new(&mut ctx, false);
212    while i < iter.buffer.len {
213        if (iter.buffer.info[i].mask & c.plan.kern_mask) == 0 {
214            i += 1;
215            continue;
216        }
217
218        iter.reset_fast(i);
219
220        let mut unsafe_to = 0;
221        if !iter.next(Some(&mut unsafe_to)) {
222            iter.buffer.unsafe_to_concat(Some(i), Some(unsafe_to));
223            i += 1;
224            continue;
225        }
226
227        let j = iter.index();
228
229        let info = &iter.buffer.info;
230        let a = info[i].as_glyph();
231        let b = info[j].as_glyph();
232        let kern = if !first_set.contains(a.to_u32()) || !second_set.contains(b.to_u32()) {
233            0
234        } else {
235            kind.simple_kerning(a, b).unwrap_or(0)
236        };
237        let kern = if use_x_scale {
238            scale.scale_x(kern)
239        } else {
240            scale.scale_y(kern)
241        };
242
243        let pos = &mut iter.buffer.pos;
244        if kern != 0 {
245            if horizontal {
246                if cross_stream {
247                    pos[j].y_offset = kern;
248                    iter.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
249                } else {
250                    let kern1 = kern >> 1;
251                    let kern2 = kern - kern1;
252                    pos[i].x_advance = pos[i].x_advance.saturating_add(kern1);
253                    pos[j].x_advance = pos[j].x_advance.saturating_add(kern2);
254                    pos[j].x_offset = pos[j].x_offset.saturating_add(kern2);
255                }
256            } else {
257                if cross_stream {
258                    pos[j].x_offset = kern;
259                    iter.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
260                } else {
261                    let kern1 = kern >> 1;
262                    let kern2 = kern - kern1;
263                    pos[i].y_advance = pos[i].y_advance.saturating_add(kern1);
264                    pos[j].y_advance = pos[j].y_advance.saturating_add(kern2);
265                    pos[j].y_offset = pos[j].y_offset.saturating_add(kern2);
266                }
267            }
268
269            iter.buffer.unsafe_to_break(Some(i), Some(j + 1));
270        }
271
272        i = j;
273    }
274}
275
276pub(crate) trait KerxStateEntryExt {
277    fn flags(&self) -> u16;
278    fn action_index(&self) -> u16;
279
280    fn is_action_initiable(&self) -> bool {
281        self.flags() & 0x8000 != 0
282    }
283
284    fn is_actionable(&self) -> bool {
285        self.action_index() != 0xFFFF
286    }
287
288    fn has_advance(&self) -> bool {
289        self.flags() & 0x4000 == 0
290    }
291
292    fn has_reset(&self) -> bool {
293        self.flags() & 0x2000 != 0
294    }
295
296    fn has_push(&self) -> bool {
297        self.flags() & 0x8000 != 0
298    }
299
300    fn has_mark(&self) -> bool {
301        self.flags() & 0x8000 != 0
302    }
303}
304
305impl KerxStateEntryExt for aat::StateEntry<BigEndian<u16>> {
306    fn flags(&self) -> u16 {
307        self.flags
308    }
309
310    fn action_index(&self) -> u16 {
311        self.payload.get()
312    }
313}
314
315fn collect_initial_glyphs<T>(
316    machine: &aat::ExtendedStateTable<T>,
317    glyphs: &mut U32Set,
318    num_glyphs: u32,
319) where
320    T: FixedSize + bytemuck::AnyBitPattern,
321    aat::StateEntry<T>: KerxStateEntryExt,
322{
323    let mut classes = U32Set::default();
324
325    let class_table = &machine.class_table;
326    for i in 0..machine.n_classes {
327        if let Ok(entry) = machine.entry(START_OF_TEXT, i as u16) {
328            if entry.new_state == START_OF_TEXT
329                && !entry.is_action_initiable()
330                && !entry.is_actionable()
331            {
332                continue;
333            }
334            classes.insert(i as u32);
335        }
336    }
337
338    // And glyphs in those classes.
339
340    let filter = |class: u16| classes.contains(class as u32);
341
342    if filter(aat::class::DELETED_GLYPH as u16) {
343        glyphs.insert(DELETED_GLYPH);
344    }
345
346    class_table.collect_glyphs_filtered(glyphs, num_glyphs, filter);
347}
348
349fn collect_start_end_safe_to_break<T>(machine: &aat::ExtendedStateTable<T>) -> u64
350where
351    T: FixedSize + bytemuck::AnyBitPattern,
352    aat::StateEntry<T>: KerxStateEntryExt,
353{
354    let mut result = 0u64;
355    for state in 0..64 {
356        let bit = if let Ok(entry) = machine.entry(state, aat::class::END_OF_TEXT as u16) {
357            !entry.is_actionable()
358        } else {
359            true
360        };
361        if bit {
362            result |= 1 << state;
363        }
364    }
365    result
366}
367
368fn apply_state_machine_kerning<T, E, Driver: StateTableDriver<T, E>>(
369    c: &mut AatApplyContext,
370    subtable: &Subtable,
371    kind: &T,
372    state_table: &aat::ExtendedStateTable<E>,
373    driver: &mut Driver,
374) where
375    E: FixedSize + bytemuck::AnyBitPattern,
376    aat::StateEntry<E>: KerxStateEntryExt,
377{
378    let mut state = START_OF_TEXT;
379    c.buffer.idx = 0;
380    loop {
381        let class = if c.buffer.idx < c.buffer.len {
382            get_class(
383                state_table,
384                c.buffer.cur(0).as_glyph(),
385                c.machine_class_cache.unwrap(),
386            )
387        } else {
388            u16::from(aat::class::END_OF_TEXT)
389        };
390
391        let Ok(entry) = state_table.entry(state, class) else {
392            break;
393        };
394
395        let next_state = entry.new_state;
396
397        // Conditions under which it's guaranteed safe-to-break before current glyph:
398        //
399        // 1. There was no action in this transition; and
400        //
401        // 2. If we break before current glyph, the results will be the same. That
402        //    is guaranteed if:
403        //
404        //    2a. We were already in start-of-text state; or
405        //
406        //    2b. We are epsilon-transitioning to start-of-text state; or
407        //
408        //    2c. Starting from start-of-text state seeing current glyph:
409        //
410        //        2c'. There won't be any actions; and
411        //
412        //        2c". We would end up in the same state that we were going to end up
413        //             in now, including whether epsilon-transitioning.
414        //
415        //    and
416        //
417        // 3. If we break before current glyph, there won't be any end-of-text action
418        //    after previous glyph.
419        //
420        // This triples the transitions we need to look up, but is worth returning
421        // granular unsafe-to-break results. See eg.:
422        //
423        //   https://github.com/harfbuzz/harfbuzz/issues/2860
424
425        let is_safe_to_break =
426            // 1
427            !entry.is_actionable() &&
428
429            // 2
430            (
431                state == START_OF_TEXT
432                || (!entry.has_advance() && next_state == START_OF_TEXT)
433                ||
434                {
435                    // 2c
436                    if let Ok(wouldbe_entry) = state_table.entry(START_OF_TEXT, class) {
437                        // 2c'
438                        !wouldbe_entry.is_actionable() &&
439
440                        // 2c"
441                        (
442                            next_state == wouldbe_entry.new_state &&
443                            entry.has_advance() == wouldbe_entry.has_advance()
444                        )
445                    } else {
446                        false
447                    }
448                }
449            ) &&
450
451            // 3
452            (
453                if state < 64 {
454                    (c.start_end_safe_to_break & (1 << state)) != 0
455                } else {
456                    if let Ok(end_entry) = state_table.entry(state, u16::from(aat::class::END_OF_TEXT)) {
457                        !end_entry.is_actionable()
458                    } else {
459                        false
460                    }
461                }
462            )
463        ;
464
465        if !is_safe_to_break && c.buffer.backtrack_len() > 0 && c.buffer.idx < c.buffer.len {
466            c.buffer.unsafe_to_break_from_outbuffer(
467                Some(c.buffer.backtrack_len() - 1),
468                Some(c.buffer.idx + 1),
469            );
470        }
471
472        let _ = driver.transition(
473            kind,
474            &entry,
475            subtable.is_cross_stream(),
476            subtable.tuple_count(),
477            c,
478        );
479
480        state = next_state;
481
482        if c.buffer.idx >= c.buffer.len {
483            break;
484        }
485
486        if entry.has_advance() || c.buffer.max_ops <= 0 {
487            c.buffer.next_glyph();
488        }
489        c.buffer.max_ops -= 1;
490    }
491}
492
493trait StateTableDriver<T, E> {
494    fn transition(
495        &mut self,
496        aat: &T,
497        entry: &aat::StateEntry<E>,
498        has_cross_stream: bool,
499        tuple_count: u32,
500        c: &mut AatApplyContext,
501    ) -> Option<()>;
502}
503
504struct Driver1 {
505    stack: [usize; 8],
506    depth: usize,
507}
508
509impl StateTableDriver<Subtable1<'_>, BigEndian<u16>> for Driver1 {
510    #[inline(always)]
511    fn transition(
512        &mut self,
513        aat: &Subtable1,
514        entry: &aat::StateEntry<BigEndian<u16>>,
515        has_cross_stream: bool,
516        tuple_count: u32,
517        c: &mut AatApplyContext,
518    ) -> Option<()> {
519        if entry.has_reset() {
520            self.depth = 0;
521        }
522
523        if entry.has_push() {
524            if self.depth < self.stack.len() {
525                self.stack[self.depth] = c.buffer.idx;
526                self.depth += 1;
527            } else {
528                self.depth = 0; // Probably not what CoreText does, but better?
529            }
530        }
531
532        if entry.is_actionable() && self.depth != 0 {
533            let tuple_count = u16::try_from(tuple_count.max(1)).ok()?;
534
535            let mut action_index = entry.action_index();
536
537            // From Apple 'kern' spec:
538            // "Each pops one glyph from the kerning stack and applies the kerning value to it.
539            // The end of the list is marked by an odd value...
540            let mut last = false;
541            let use_x_scale = c.buffer.direction.is_horizontal() ^ has_cross_stream;
542            while !last && self.depth != 0 {
543                self.depth -= 1;
544                let idx = self.stack[self.depth];
545                let mut v = aat.values.get(action_index as usize)?.get() as i32;
546                action_index = action_index.checked_add(tuple_count)?;
547                if idx >= c.buffer.len {
548                    continue;
549                }
550
551                // "The end of the list is marked by an odd value..."
552                last = v & 1 != 0;
553                v &= !1;
554                let scaled_v = if use_x_scale {
555                    c.scale_x(v)
556                } else {
557                    c.scale_y(v)
558                };
559                // Testing shows that CoreText only applies kern (cross-stream or not)
560                // if none has been applied by previous subtables. That is, it does
561                // NOT seem to accumulate as otherwise implied by specs.
562
563                let mut has_gpos_attachment = false;
564                let glyph_mask = c.buffer.info[idx].mask;
565                let pos = &mut c.buffer.pos[idx];
566
567                if c.buffer.direction.is_horizontal() {
568                    if has_cross_stream {
569                        // The following flag is undocumented in the spec, but described
570                        // in the 'kern' table example.
571                        if v == -0x8000 {
572                            pos.set_attach_type(0);
573                            pos.set_attach_chain(0);
574                            pos.y_offset = 0;
575                        } else if pos.attach_type() != 0 {
576                            pos.y_offset = pos.y_offset.saturating_add(scaled_v);
577                            has_gpos_attachment = true;
578                        }
579                    } else if glyph_mask & c.plan.kern_mask != 0 {
580                        pos.x_advance = pos.x_advance.saturating_add(scaled_v);
581                        pos.x_offset = pos.x_offset.saturating_add(scaled_v);
582                    }
583                } else {
584                    if has_cross_stream {
585                        // CoreText doesn't do crossStream kerning in vertical. We do.
586                        if v == -0x8000 {
587                            pos.set_attach_type(0);
588                            pos.set_attach_chain(0);
589                            pos.x_offset = 0;
590                        } else if pos.attach_type() != 0 {
591                            pos.x_offset = pos.x_offset.saturating_add(scaled_v);
592                            has_gpos_attachment = true;
593                        }
594                    } else if glyph_mask & c.plan.kern_mask != 0 {
595                        if pos.y_offset == 0 {
596                            pos.y_advance = pos.y_advance.saturating_add(scaled_v);
597                            pos.y_offset = pos.y_offset.saturating_add(scaled_v);
598                        }
599                    }
600                }
601
602                if has_gpos_attachment {
603                    c.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
604                }
605            }
606        }
607
608        Some(())
609    }
610}
611struct Driver4<'a> {
612    mark_set: bool,
613    mark: usize,
614    ankr_table: Option<Ankr<'a>>,
615}
616
617impl StateTableDriver<Subtable4<'_>, BigEndian<u16>> for Driver4<'_> {
618    #[inline(always)]
619    fn transition(
620        &mut self,
621        aat: &Subtable4,
622        entry: &aat::StateEntry<BigEndian<u16>>,
623        _has_cross_stream: bool,
624        _tuple_count: u32,
625        c: &mut AatApplyContext,
626    ) -> Option<()> {
627        if self.mark_set && entry.is_actionable() && c.buffer.idx < c.buffer.len {
628            match (self.ankr_table.as_ref(), &aat.actions) {
629                (Some(ankr_table), Subtable4Actions::AnchorPoints(ankr_data)) => {
630                    let action_idx = entry.action_index() as usize * 2;
631                    let mark_action_idx = ankr_data.get(action_idx)?.get() as usize;
632                    let curr_action_idx = ankr_data.get(action_idx + 1)?.get() as usize;
633                    let mark_idx = c.buffer.info[self.mark].as_glyph();
634                    let mark_anchor = ankr_table
635                        .anchor_points(mark_idx)
636                        .ok()
637                        .and_then(|list| list.get(mark_action_idx))
638                        .map(|point| (point.x(), point.y()))
639                        .unwrap_or_default();
640
641                    let curr_idx = c.buffer.cur(0).as_glyph();
642                    let curr_anchor = ankr_table
643                        .anchor_points(curr_idx)
644                        .ok()
645                        .and_then(|list| list.get(curr_action_idx))
646                        .map(|point| (point.x(), point.y()))
647                        .unwrap_or_default();
648
649                    let x_offset =
650                        c.scale_x(i32::from(mark_anchor.0)) - c.scale_x(i32::from(curr_anchor.0));
651                    let y_offset =
652                        c.scale_y(i32::from(mark_anchor.1)) - c.scale_y(i32::from(curr_anchor.1));
653                    let pos = c.buffer.cur_pos_mut();
654                    pos.x_offset = x_offset;
655                    pos.y_offset = y_offset;
656                }
657                (_, Subtable4Actions::ControlPointCoords(coords)) => {
658                    let action_idx = entry.action_index() as usize * 4;
659                    let mark_x = coords.get(action_idx)?.get() as i32;
660                    let mark_y = coords.get(action_idx + 1)?.get() as i32;
661                    let curr_x = coords.get(action_idx + 2)?.get() as i32;
662                    let curr_y = coords.get(action_idx + 3)?.get() as i32;
663                    let x_offset = c.scale_x(mark_x) - c.scale_x(curr_x);
664                    let y_offset = c.scale_y(mark_y) - c.scale_y(curr_y);
665                    let pos = c.buffer.cur_pos_mut();
666                    pos.x_offset = x_offset;
667                    pos.y_offset = y_offset;
668                }
669                _ => {}
670            }
671
672            c.buffer.cur_pos_mut().set_attach_type(attach_type::MARK);
673            let idx = c.buffer.idx;
674            let mut attach_chain = self.mark as i16 - idx as i16;
675            if c.buffer_is_reversed {
676                attach_chain = -attach_chain;
677            }
678            c.buffer.cur_pos_mut().set_attach_chain(attach_chain);
679            c.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
680        }
681
682        if entry.has_mark() {
683            self.mark_set = true;
684            self.mark = c.buffer.idx;
685        }
686
687        Some(())
688    }
689}
690
691pub(crate) struct KerxSubtableCache {
692    start_end_safe_to_break: u64,
693    first_set: U32Set,
694    second_set: U32Set,
695    class_cache: Box<ClassCache>,
696}
697
698impl KerxSubtableCache {
699    pub(crate) fn new(subtable: &Subtable, num_glyphs: u32) -> Self {
700        let mut start_end_safe_to_break = 0u64;
701        let mut first_set = U32Set::default();
702        let mut second_set = U32Set::default();
703        if let Ok(kind) = subtable.kind() {
704            match &kind {
705                SubtableKind::Format0(format0) => {
706                    format0.collect_glyphs(&mut first_set, &mut second_set, num_glyphs);
707                }
708                SubtableKind::Format1(format1) => {
709                    start_end_safe_to_break = collect_start_end_safe_to_break(&format1.state_table);
710                    collect_initial_glyphs(&format1.state_table, &mut first_set, num_glyphs);
711                }
712                SubtableKind::Format2(format2) => {
713                    format2.collect_glyphs(&mut first_set, &mut second_set, num_glyphs);
714                }
715                SubtableKind::Format4(format4) => {
716                    start_end_safe_to_break = collect_start_end_safe_to_break(&format4.state_table);
717                    collect_initial_glyphs(&format4.state_table, &mut first_set, num_glyphs);
718                }
719                SubtableKind::Format6(format6) => {
720                    format6.collect_glyphs(&mut first_set, &mut second_set, num_glyphs);
721                }
722            }
723        }
724        KerxSubtableCache {
725            start_end_safe_to_break,
726            first_set,
727            second_set,
728            class_cache: Box::new(ClassCache::new()),
729        }
730    }
731}