rustybuzz/hb/
kerning.rs

1use ttf_parser::{apple_layout, kern, GlyphId};
2
3use super::buffer::*;
4use super::ot_layout::TableIndex;
5use super::ot_layout_common::lookup_flags;
6use super::ot_layout_gpos_table::attach_type;
7use super::ot_layout_gsubgpos::{skipping_iterator_t, OT::hb_ot_apply_context_t};
8use super::ot_shape_plan::hb_ot_shape_plan_t;
9use super::{hb_font_t, hb_mask_t};
10
11pub fn kern(plan: &hb_ot_shape_plan_t, face: &hb_font_t, buffer: &mut hb_buffer_t) {
12    let subtables = match face.tables().kern {
13        Some(table) => table.subtables,
14        None => return,
15    };
16
17    let mut seen_cross_stream = false;
18    for subtable in subtables {
19        if subtable.variable {
20            continue;
21        }
22
23        if buffer.direction.is_horizontal() != subtable.horizontal {
24            continue;
25        }
26
27        let reverse = buffer.direction.is_backward();
28
29        if !seen_cross_stream && subtable.has_cross_stream {
30            seen_cross_stream = true;
31
32            // Attach all glyphs into a chain.
33            for pos in &mut buffer.pos {
34                pos.set_attach_type(attach_type::CURSIVE);
35                pos.set_attach_chain(if buffer.direction.is_forward() { -1 } else { 1 });
36                // We intentionally don't set BufferScratchFlags::HAS_GPOS_ATTACHMENT,
37                // since there needs to be a non-zero attachment for post-positioning to
38                // be needed.
39            }
40        }
41
42        if reverse {
43            buffer.reverse();
44        }
45
46        if subtable.has_state_machine {
47            apply_state_machine_kerning(&subtable, plan.kern_mask, buffer);
48        } else {
49            if !plan.requested_kerning {
50                continue;
51            }
52
53            apply_simple_kerning(&subtable, face, plan.kern_mask, buffer);
54        }
55
56        if reverse {
57            buffer.reverse();
58        }
59    }
60}
61
62// TODO: remove
63fn machine_kern(
64    face: &hb_font_t,
65    buffer: &mut hb_buffer_t,
66    kern_mask: hb_mask_t,
67    cross_stream: bool,
68    get_kerning: impl Fn(u32, u32) -> i32,
69) {
70    buffer.unsafe_to_concat(None, None);
71    let mut ctx = hb_ot_apply_context_t::new(TableIndex::GPOS, face, buffer);
72    ctx.lookup_mask = kern_mask;
73    ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS);
74
75    let horizontal = ctx.buffer.direction.is_horizontal();
76
77    let mut i = 0;
78    while i < ctx.buffer.len {
79        if (ctx.buffer.info[i].mask & kern_mask) == 0 {
80            i += 1;
81            continue;
82        }
83
84        let mut iter = skipping_iterator_t::new(&ctx, i, 1, false);
85
86        let mut unsafe_to = 0;
87        if !iter.next(Some(&mut unsafe_to)) {
88            i += 1;
89            continue;
90        }
91
92        let j = iter.index();
93
94        let info = &ctx.buffer.info;
95        let kern = get_kerning(info[i].glyph_id, info[j].glyph_id);
96
97        let pos = &mut ctx.buffer.pos;
98        if kern != 0 {
99            if horizontal {
100                if cross_stream {
101                    pos[j].y_offset = kern;
102                    ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
103                } else {
104                    let kern1 = kern >> 1;
105                    let kern2 = kern - kern1;
106                    pos[i].x_advance += kern1;
107                    pos[j].x_advance += kern2;
108                    pos[j].x_offset += kern2;
109                }
110            } else {
111                if cross_stream {
112                    pos[j].x_offset = kern;
113                    ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
114                } else {
115                    let kern1 = kern >> 1;
116                    let kern2 = kern - kern1;
117                    pos[i].y_advance += kern1;
118                    pos[j].y_advance += kern2;
119                    pos[j].y_offset += kern2;
120                }
121            }
122
123            ctx.buffer.unsafe_to_break(Some(i), Some(j + 1))
124        }
125
126        i = j;
127    }
128}
129
130fn apply_simple_kerning(
131    subtable: &kern::Subtable,
132    face: &hb_font_t,
133    kern_mask: hb_mask_t,
134    buffer: &mut hb_buffer_t,
135) {
136    machine_kern(
137        face,
138        buffer,
139        kern_mask,
140        subtable.has_cross_stream,
141        |left, right| {
142            subtable
143                .glyphs_kerning(GlyphId(left as u16), GlyphId(right as u16))
144                .map(i32::from)
145                .unwrap_or(0)
146        },
147    );
148}
149
150struct StateMachineDriver {
151    stack: [usize; 8],
152    depth: usize,
153}
154
155fn apply_state_machine_kerning(
156    subtable: &kern::Subtable,
157    kern_mask: hb_mask_t,
158    buffer: &mut hb_buffer_t,
159) {
160    let state_table = match subtable.format {
161        kern::Format::Format1(ref state_table) => state_table,
162        _ => return,
163    };
164
165    let mut driver = StateMachineDriver {
166        stack: [0; 8],
167        depth: 0,
168    };
169
170    let mut state = apple_layout::state::START_OF_TEXT;
171    buffer.idx = 0;
172    loop {
173        let class = if buffer.idx < buffer.len {
174            state_table
175                .class(buffer.info[buffer.idx].as_glyph())
176                .unwrap_or(1)
177        } else {
178            apple_layout::class::END_OF_TEXT as u8
179        };
180
181        let entry = match state_table.entry(state, class) {
182            Some(v) => v,
183            None => break,
184        };
185
186        // Unsafe-to-break before this if not in state 0, as things might
187        // go differently if we start from state 0 here.
188        if state != apple_layout::state::START_OF_TEXT
189            && buffer.backtrack_len() != 0
190            && buffer.idx < buffer.len
191        {
192            // If there's no value and we're just epsilon-transitioning to state 0, safe to break.
193            if entry.has_offset()
194                || !(entry.new_state == apple_layout::state::START_OF_TEXT && !entry.has_advance())
195            {
196                buffer.unsafe_to_break_from_outbuffer(
197                    Some(buffer.backtrack_len() - 1),
198                    Some(buffer.idx + 1),
199                );
200            }
201        }
202
203        // Unsafe-to-break if end-of-text would kick in here.
204        if buffer.idx + 2 <= buffer.len {
205            let end_entry = match state_table.entry(state, apple_layout::class::END_OF_TEXT) {
206                Some(v) => v,
207                None => break,
208            };
209
210            if end_entry.has_offset() {
211                buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + 2));
212            }
213        }
214
215        state_machine_transition(
216            entry,
217            subtable.has_cross_stream,
218            kern_mask,
219            state_table,
220            &mut driver,
221            buffer,
222        );
223
224        state = state_table.new_state(entry.new_state);
225
226        if buffer.idx >= buffer.len {
227            break;
228        }
229
230        buffer.max_ops -= 1;
231        if entry.has_advance() || buffer.max_ops <= 0 {
232            buffer.next_glyph();
233        }
234    }
235}
236
237fn state_machine_transition(
238    entry: apple_layout::StateEntry,
239    has_cross_stream: bool,
240    kern_mask: hb_mask_t,
241    state_table: &apple_layout::StateTable,
242    driver: &mut StateMachineDriver,
243    buffer: &mut hb_buffer_t,
244) {
245    if entry.has_push() {
246        if driver.depth < driver.stack.len() {
247            driver.stack[driver.depth] = buffer.idx;
248            driver.depth += 1;
249        } else {
250            driver.depth = 0; // Probably not what CoreText does, but better?
251        }
252    }
253
254    if entry.has_offset() && driver.depth != 0 {
255        let mut value_offset = entry.value_offset();
256        let mut value = match state_table.kerning(value_offset) {
257            Some(v) => v,
258            None => {
259                driver.depth = 0;
260                return;
261            }
262        };
263
264        // From Apple 'kern' spec:
265        // "Each pops one glyph from the kerning stack and applies the kerning value to it.
266        // The end of the list is marked by an odd value...
267        let mut last = false;
268        while !last && driver.depth != 0 {
269            driver.depth -= 1;
270            let idx = driver.stack[driver.depth];
271            let mut v = value as i32;
272            value_offset = value_offset.next();
273            value = state_table.kerning(value_offset).unwrap_or(0);
274            if idx >= buffer.len {
275                continue;
276            }
277
278            // "The end of the list is marked by an odd value..."
279            last = v & 1 != 0;
280            v &= !1;
281
282            // Testing shows that CoreText only applies kern (cross-stream or not)
283            // if none has been applied by previous subtables. That is, it does
284            // NOT seem to accumulate as otherwise implied by specs.
285
286            let mut has_gpos_attachment = false;
287            let glyph_mask = buffer.info[idx].mask;
288            let pos = &mut buffer.pos[idx];
289
290            if buffer.direction.is_horizontal() {
291                if has_cross_stream {
292                    // The following flag is undocumented in the spec, but described
293                    // in the 'kern' table example.
294                    if v == -0x8000 {
295                        pos.set_attach_type(0);
296                        pos.set_attach_chain(0);
297                        pos.y_offset = 0;
298                    } else if pos.attach_type() != 0 {
299                        pos.y_offset += v;
300                        has_gpos_attachment = true;
301                    }
302                } else if glyph_mask & kern_mask != 0 {
303                    pos.x_advance += v;
304                    pos.x_offset += v;
305                }
306            } else {
307                if has_cross_stream {
308                    // CoreText doesn't do crossStream kerning in vertical. We do.
309                    if v == -0x8000 {
310                        pos.set_attach_type(0);
311                        pos.set_attach_chain(0);
312                        pos.x_offset = 0;
313                    } else if pos.attach_type() != 0 {
314                        pos.x_offset += v;
315                        has_gpos_attachment = true;
316                    }
317                } else if glyph_mask & kern_mask != 0 {
318                    if pos.y_offset == 0 {
319                        pos.y_advance += v;
320                        pos.y_offset += v;
321                    }
322                }
323            }
324
325            if has_gpos_attachment {
326                buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT;
327            }
328        }
329    }
330}