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