rustybuzz/hb/
aat_layout_kerx_table.rs

1use core::convert::TryFrom;
2
3use ttf_parser::{ankr, apple_layout, kerx, FromData, GlyphId};
4
5use super::buffer::*;
6use super::hb_font_t;
7use super::ot_layout::TableIndex;
8use super::ot_layout_common::lookup_flags;
9use super::ot_layout_gpos_table::attach_type;
10use super::ot_layout_gsubgpos::{skipping_iterator_t, OT::hb_ot_apply_context_t};
11use super::ot_shape_plan::hb_ot_shape_plan_t;
12
13// TODO: Use set_digest, similarly to how it's used in harfbuzz.
14
15trait ExtendedStateTableExt<T: FromData + Copy> {
16    fn class(&self, glyph_id: GlyphId) -> Option<u16>;
17    fn entry(&self, state: u16, class: u16) -> Option<apple_layout::GenericStateEntry<T>>;
18}
19
20impl ExtendedStateTableExt<kerx::EntryData> for kerx::Subtable1<'_> {
21    fn class(&self, glyph_id: GlyphId) -> Option<u16> {
22        self.state_table.class(glyph_id)
23    }
24
25    fn entry(
26        &self,
27        state: u16,
28        class: u16,
29    ) -> Option<apple_layout::GenericStateEntry<kerx::EntryData>> {
30        self.state_table.entry(state, class)
31    }
32}
33
34impl ExtendedStateTableExt<kerx::EntryData> for kerx::Subtable4<'_> {
35    fn class(&self, glyph_id: GlyphId) -> Option<u16> {
36        self.state_table.class(glyph_id)
37    }
38
39    fn entry(
40        &self,
41        state: u16,
42        class: u16,
43    ) -> Option<apple_layout::GenericStateEntry<kerx::EntryData>> {
44        self.state_table.entry(state, class)
45    }
46}
47
48pub(crate) fn apply(
49    plan: &hb_ot_shape_plan_t,
50    face: &hb_font_t,
51    buffer: &mut hb_buffer_t,
52) -> Option<()> {
53    buffer.unsafe_to_concat(None, None);
54
55    let mut seen_cross_stream = false;
56    for subtable in face.tables().kerx?.subtables {
57        if subtable.variable {
58            continue;
59        }
60
61        if buffer.direction.is_horizontal() != subtable.horizontal {
62            continue;
63        }
64
65        let reverse = buffer.direction.is_backward();
66
67        if !seen_cross_stream && subtable.has_cross_stream {
68            seen_cross_stream = true;
69
70            // Attach all glyphs into a chain.
71            for pos in &mut buffer.pos {
72                pos.set_attach_type(attach_type::CURSIVE);
73                pos.set_attach_chain(if buffer.direction.is_forward() { -1 } else { 1 });
74                // We intentionally don't set BufferScratchFlags::HAS_GPOS_ATTACHMENT,
75                // since there needs to be a non-zero attachment for post-positioning to
76                // be needed.
77            }
78        }
79
80        if reverse {
81            buffer.reverse();
82        }
83
84        match subtable.format {
85            kerx::Format::Format0(_) => {
86                if !plan.requested_kerning {
87                    continue;
88                }
89
90                apply_simple_kerning(&subtable, plan, face, buffer);
91            }
92            kerx::Format::Format1(ref sub) => {
93                let mut driver = Driver1 {
94                    stack: [0; 8],
95                    depth: 0,
96                };
97
98                apply_state_machine_kerning(&subtable, sub, &mut driver, plan, buffer);
99            }
100            kerx::Format::Format2(_) => {
101                if !plan.requested_kerning {
102                    continue;
103                }
104
105                buffer.unsafe_to_concat(None, None);
106
107                apply_simple_kerning(&subtable, plan, face, buffer);
108            }
109            kerx::Format::Format4(ref sub) => {
110                let mut driver = Driver4 {
111                    mark_set: false,
112                    mark: 0,
113                    ankr_table: face.tables().ankr.clone(),
114                };
115
116                apply_state_machine_kerning(&subtable, sub, &mut driver, plan, buffer);
117            }
118            kerx::Format::Format6(_) => {
119                if !plan.requested_kerning {
120                    continue;
121                }
122
123                apply_simple_kerning(&subtable, plan, face, buffer);
124            }
125        }
126
127        if reverse {
128            buffer.reverse();
129        }
130    }
131
132    Some(())
133}
134
135fn apply_simple_kerning(
136    subtable: &kerx::Subtable,
137    plan: &hb_ot_shape_plan_t,
138    face: &hb_font_t,
139    buffer: &mut hb_buffer_t,
140) {
141    let mut ctx = hb_ot_apply_context_t::new(TableIndex::GPOS, face, buffer);
142    ctx.set_lookup_mask(plan.kern_mask);
143    ctx.lookup_props = u32::from(lookup_flags::IGNORE_FLAGS);
144
145    let horizontal = ctx.buffer.direction.is_horizontal();
146
147    let mut i = 0;
148    while i < ctx.buffer.len {
149        if (ctx.buffer.info[i].mask & plan.kern_mask) == 0 {
150            i += 1;
151            continue;
152        }
153
154        let mut iter = skipping_iterator_t::new(&ctx, i, false);
155
156        let mut unsafe_to = 0;
157        if !iter.next(Some(&mut unsafe_to)) {
158            ctx.buffer.unsafe_to_concat(Some(i), Some(unsafe_to));
159            i += 1;
160            continue;
161        }
162
163        let j = iter.index();
164
165        let info = &ctx.buffer.info;
166        let kern = subtable
167            .glyphs_kerning(info[i].as_glyph(), info[j].as_glyph())
168            .unwrap_or(0);
169        let kern = i32::from(kern);
170
171        let pos = &mut ctx.buffer.pos;
172        if kern != 0 {
173            if horizontal {
174                if subtable.has_cross_stream {
175                    pos[j].y_offset = kern;
176                    ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
177                } else {
178                    let kern1 = kern >> 1;
179                    let kern2 = kern - kern1;
180                    pos[i].x_advance += kern1;
181                    pos[j].x_advance += kern2;
182                    pos[j].x_offset += kern2;
183                }
184            } else {
185                if subtable.has_cross_stream {
186                    pos[j].x_offset = kern;
187                    ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
188                } else {
189                    let kern1 = kern >> 1;
190                    let kern2 = kern - kern1;
191                    pos[i].y_advance += kern1;
192                    pos[j].y_advance += kern2;
193                    pos[j].y_offset += kern2;
194                }
195            }
196
197            ctx.buffer.unsafe_to_break(Some(i), Some(j + 1))
198        }
199
200        i = j;
201    }
202}
203
204const START_OF_TEXT: u16 = 0;
205
206trait KerxEntryDataExt {
207    fn action_index(self) -> u16;
208    fn is_actionable(&self) -> bool;
209}
210
211impl KerxEntryDataExt for apple_layout::GenericStateEntry<kerx::EntryData> {
212    fn action_index(self) -> u16 {
213        self.extra.action_index
214    }
215    fn is_actionable(&self) -> bool {
216        self.extra.action_index != 0xFFFF
217    }
218}
219
220fn apply_state_machine_kerning<T, E>(
221    subtable: &kerx::Subtable,
222    state_table: &T,
223    driver: &mut dyn StateTableDriver<T, E>,
224    plan: &hb_ot_shape_plan_t,
225    buffer: &mut hb_buffer_t,
226) where
227    T: ExtendedStateTableExt<E>,
228    E: FromData + Copy,
229    apple_layout::GenericStateEntry<E>: KerxEntryDataExt,
230{
231    let mut state = START_OF_TEXT;
232    buffer.idx = 0;
233    loop {
234        let class = if buffer.idx < buffer.len {
235            state_table
236                .class(buffer.info[buffer.idx].as_glyph())
237                .unwrap_or(1)
238        } else {
239            u16::from(apple_layout::class::END_OF_TEXT)
240        };
241
242        let entry: apple_layout::GenericStateEntry<E> = match state_table.entry(state, class) {
243            Some(v) => v,
244            None => break,
245        };
246
247        // Unsafe-to-break before this if not in state 0, as things might
248        // go differently if we start from state 0 here.
249        if state != START_OF_TEXT && buffer.backtrack_len() != 0 && buffer.idx < buffer.len {
250            // If there's no value and we're just epsilon-transitioning to state 0, safe to break.
251            if entry.is_actionable() || entry.new_state != START_OF_TEXT || entry.has_advance()
252            {
253                buffer.unsafe_to_break_from_outbuffer(
254                    Some(buffer.backtrack_len() - 1),
255                    Some(buffer.idx + 1),
256                );
257            }
258        }
259
260        // Unsafe-to-break if end-of-text would kick in here.
261        if buffer.idx + 2 <= buffer.len {
262            let end_entry: Option<apple_layout::GenericStateEntry<E>> =
263                state_table.entry(state, u16::from(apple_layout::class::END_OF_TEXT));
264            let end_entry = match end_entry {
265                Some(v) => v,
266                None => break,
267            };
268
269            if end_entry.is_actionable() {
270                buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + 2));
271            }
272        }
273
274        let _ = driver.transition(
275            state_table,
276            entry,
277            subtable.has_cross_stream,
278            subtable.tuple_count,
279            plan,
280            buffer,
281        );
282
283        state = entry.new_state;
284
285        if buffer.idx >= buffer.len {
286            break;
287        }
288
289        if entry.has_advance() || buffer.max_ops <= 0 {
290            buffer.next_glyph();
291        }
292        buffer.max_ops -= 1;
293    }
294}
295
296trait StateTableDriver<Table, E: FromData> {
297    fn transition(
298        &mut self,
299        aat: &Table,
300        entry: apple_layout::GenericStateEntry<E>,
301        has_cross_stream: bool,
302        tuple_count: u32,
303        plan: &hb_ot_shape_plan_t,
304        buffer: &mut hb_buffer_t,
305    ) -> Option<()>;
306}
307
308struct Driver1 {
309    stack: [usize; 8],
310    depth: usize,
311}
312
313impl StateTableDriver<kerx::Subtable1<'_>, kerx::EntryData> for Driver1 {
314    fn transition(
315        &mut self,
316        aat: &kerx::Subtable1,
317        entry: apple_layout::GenericStateEntry<kerx::EntryData>,
318        has_cross_stream: bool,
319        tuple_count: u32,
320        plan: &hb_ot_shape_plan_t,
321        buffer: &mut hb_buffer_t,
322    ) -> Option<()> {
323        if entry.has_reset() {
324            self.depth = 0;
325        }
326
327        if entry.has_push() {
328            if self.depth < self.stack.len() {
329                self.stack[self.depth] = buffer.idx;
330                self.depth += 1;
331            } else {
332                self.depth = 0; // Probably not what CoreText does, but better?
333            }
334        }
335
336        if entry.is_actionable() && self.depth != 0 {
337            let tuple_count = u16::try_from(tuple_count.max(1)).ok()?;
338
339            let mut action_index = entry.action_index();
340
341            // From Apple 'kern' spec:
342            // "Each pops one glyph from the kerning stack and applies the kerning value to it.
343            // The end of the list is marked by an odd value...
344            let mut last = false;
345            while !last && self.depth != 0 {
346                self.depth -= 1;
347                let idx = self.stack[self.depth];
348                let mut v = aat.glyphs_kerning(action_index)? as i32;
349                action_index = action_index.checked_add(tuple_count)?;
350                if idx >= buffer.len {
351                    continue;
352                }
353
354                // "The end of the list is marked by an odd value..."
355                last = v & 1 != 0;
356                v &= !1;
357
358                // Testing shows that CoreText only applies kern (cross-stream or not)
359                // if none has been applied by previous subtables. That is, it does
360                // NOT seem to accumulate as otherwise implied by specs.
361
362                let mut has_gpos_attachment = false;
363                let glyph_mask = buffer.info[idx].mask;
364                let pos = &mut buffer.pos[idx];
365
366                if buffer.direction.is_horizontal() {
367                    if has_cross_stream {
368                        // The following flag is undocumented in the spec, but described
369                        // in the 'kern' table example.
370                        if v == -0x8000 {
371                            pos.set_attach_type(0);
372                            pos.set_attach_chain(0);
373                            pos.y_offset = 0;
374                        } else if pos.attach_type() != 0 {
375                            pos.y_offset += v;
376                            has_gpos_attachment = true;
377                        }
378                    } else if glyph_mask & plan.kern_mask != 0 {
379                        pos.x_advance += v;
380                        pos.x_offset += v;
381                    }
382                } else {
383                    if has_cross_stream {
384                        // CoreText doesn't do crossStream kerning in vertical. We do.
385                        if v == -0x8000 {
386                            pos.set_attach_type(0);
387                            pos.set_attach_chain(0);
388                            pos.x_offset = 0;
389                        } else if pos.attach_type() != 0 {
390                            pos.x_offset += v;
391                            has_gpos_attachment = true;
392                        }
393                    } else if glyph_mask & plan.kern_mask != 0 {
394                        if pos.y_offset == 0 {
395                            pos.y_advance += v;
396                            pos.y_offset += v;
397                        }
398                    }
399                }
400
401                if has_gpos_attachment {
402                    buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
403                }
404            }
405        }
406
407        Some(())
408    }
409}
410
411struct Driver4<'a> {
412    mark_set: bool,
413    mark: usize,
414    ankr_table: Option<ankr::Table<'a>>,
415}
416
417impl StateTableDriver<kerx::Subtable4<'_>, kerx::EntryData> for Driver4<'_> {
418    fn transition(
419        &mut self,
420        aat: &kerx::Subtable4,
421        entry: apple_layout::GenericStateEntry<kerx::EntryData>,
422        _has_cross_stream: bool,
423        _tuple_count: u32,
424        _opt: &hb_ot_shape_plan_t,
425        buffer: &mut hb_buffer_t,
426    ) -> Option<()> {
427        if self.mark_set && entry.is_actionable() && buffer.idx < buffer.len {
428            if let Some(ref ankr_table) = self.ankr_table {
429                let point = aat.anchor_points.get(entry.action_index())?;
430
431                let mark_idx = buffer.info[self.mark].as_glyph();
432                let mark_anchor = ankr_table
433                    .points(mark_idx)
434                    .and_then(|list| list.get(u32::from(point.0)))
435                    .unwrap_or_default();
436
437                let curr_idx = buffer.cur(0).as_glyph();
438                let curr_anchor = ankr_table
439                    .points(curr_idx)
440                    .and_then(|list| list.get(u32::from(point.1)))
441                    .unwrap_or_default();
442
443                let pos = buffer.cur_pos_mut();
444                pos.x_offset = i32::from(mark_anchor.x - curr_anchor.x);
445                pos.y_offset = i32::from(mark_anchor.y - curr_anchor.y);
446            }
447
448            buffer.cur_pos_mut().set_attach_type(attach_type::MARK);
449            let idx = buffer.idx;
450            buffer
451                .cur_pos_mut()
452                .set_attach_chain(self.mark as i16 - idx as i16);
453            buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
454        }
455
456        if entry.has_mark() {
457            self.mark_set = true;
458            self.mark = buffer.idx;
459        }
460
461        Some(())
462    }
463}