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