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 buffer.unsafe_to_concat(None, None);
89 let mut ctx = ApplyContext::new(TableIndex::GPOS, face, buffer);
90 ctx.lookup_mask = kern_mask;
91 ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS);
92
93 let horizontal = ctx.buffer.direction.is_horizontal();
94
95 let mut i = 0;
96 while i < ctx.buffer.len {
97 if (ctx.buffer.info[i].mask & kern_mask) == 0 {
98 i += 1;
99 continue;
100 }
101
102 let mut iter = SkippyIter::new(&ctx, i, 1, false);
103
104 let mut unsafe_to = 0;
105 if !iter.next(Some(&mut unsafe_to)) {
106 i += 1;
107 continue;
108 }
109
110 let j = iter.index();
111
112 let info = &ctx.buffer.info;
113 let kern = get_kerning(info[i].glyph_id, info[j].glyph_id);
114
115 let pos = &mut ctx.buffer.pos;
116 if kern != 0 {
117 if horizontal {
118 if cross_stream {
119 pos[j].y_offset = kern;
120 ctx.buffer.scratch_flags |= BufferScratchFlags::HAS_GPOS_ATTACHMENT;
121 } else {
122 let kern1 = kern >> 1;
123 let kern2 = kern - kern1;
124 pos[i].x_advance += kern1;
125 pos[j].x_advance += kern2;
126 pos[j].x_offset += kern2;
127 }
128 } else {
129 if cross_stream {
130 pos[j].x_offset = kern;
131 ctx.buffer.scratch_flags |= BufferScratchFlags::HAS_GPOS_ATTACHMENT;
132 } else {
133 let kern1 = kern >> 1;
134 let kern2 = kern - kern1;
135 pos[i].y_advance += kern1;
136 pos[j].y_advance += kern2;
137 pos[j].y_offset += kern2;
138 }
139 }
140
141 ctx.buffer.unsafe_to_break(Some(i), Some(j + 1))
142 }
143
144 i = j;
145 }
146}
147
148fn apply_simple_kerning(
149 subtable: &kern::Subtable,
150 face: &Face,
151 kern_mask: Mask,
152 buffer: &mut Buffer,
153) {
154 machine_kern(
155 face,
156 buffer,
157 kern_mask,
158 subtable.has_cross_stream,
159 |left: u32, right: u32| {
160 subtable
161 .glyphs_kerning(GlyphId(left as u16), GlyphId(right as u16))
162 .map(i32::from)
163 .unwrap_or(default:0)
164 },
165 );
166}
167
168struct StateMachineDriver {
169 stack: [usize; 8],
170 depth: usize,
171}
172
173fn apply_state_machine_kerning(subtable: &kern::Subtable, kern_mask: Mask, buffer: &mut Buffer) {
174 let state_table = match subtable.format {
175 kern::Format::Format1(ref state_table) => state_table,
176 _ => return,
177 };
178
179 let mut driver = StateMachineDriver {
180 stack: [0; 8],
181 depth: 0,
182 };
183
184 let mut state = apple_layout::state::START_OF_TEXT;
185 buffer.idx = 0;
186 loop {
187 let class = if buffer.idx < buffer.len {
188 state_table
189 .class(buffer.info[buffer.idx].as_glyph())
190 .unwrap_or(1)
191 } else {
192 apple_layout::class::END_OF_TEXT as u8
193 };
194
195 let entry = match state_table.entry(state, class) {
196 Some(v) => v,
197 None => break,
198 };
199
200 // Unsafe-to-break before this if not in state 0, as things might
201 // go differently if we start from state 0 here.
202 if state != apple_layout::state::START_OF_TEXT
203 && buffer.backtrack_len() != 0
204 && buffer.idx < buffer.len
205 {
206 // If there's no value and we're just epsilon-transitioning to state 0, safe to break.
207 if entry.has_offset()
208 || !(entry.new_state == apple_layout::state::START_OF_TEXT && !entry.has_advance())
209 {
210 buffer.unsafe_to_break_from_outbuffer(
211 Some(buffer.backtrack_len() - 1),
212 Some(buffer.idx + 1),
213 );
214 }
215 }
216
217 // Unsafe-to-break if end-of-text would kick in here.
218 if buffer.idx + 2 <= buffer.len {
219 let end_entry = match state_table.entry(state, apple_layout::class::END_OF_TEXT) {
220 Some(v) => v,
221 None => break,
222 };
223
224 if end_entry.has_offset() {
225 buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + 2));
226 }
227 }
228
229 state_machine_transition(
230 entry,
231 subtable.has_cross_stream,
232 kern_mask,
233 state_table,
234 &mut driver,
235 buffer,
236 );
237
238 state = state_table.new_state(entry.new_state);
239
240 if buffer.idx >= buffer.len {
241 break;
242 }
243
244 buffer.max_ops -= 1;
245 if entry.has_advance() || buffer.max_ops <= 0 {
246 buffer.next_glyph();
247 }
248 }
249}
250
251fn state_machine_transition(
252 entry: apple_layout::StateEntry,
253 has_cross_stream: bool,
254 kern_mask: Mask,
255 state_table: &apple_layout::StateTable,
256 driver: &mut StateMachineDriver,
257 buffer: &mut Buffer,
258) {
259 if entry.has_push() {
260 if driver.depth < driver.stack.len() {
261 driver.stack[driver.depth] = buffer.idx;
262 driver.depth += 1;
263 } else {
264 driver.depth = 0; // Probably not what CoreText does, but better?
265 }
266 }
267
268 if entry.has_offset() && driver.depth != 0 {
269 let mut value_offset = entry.value_offset();
270 let mut value = match state_table.kerning(value_offset) {
271 Some(v) => v,
272 None => {
273 driver.depth = 0;
274 return;
275 }
276 };
277
278 // From Apple 'kern' spec:
279 // "Each pops one glyph from the kerning stack and applies the kerning value to it.
280 // The end of the list is marked by an odd value...
281 let mut last = false;
282 while !last && driver.depth != 0 {
283 driver.depth -= 1;
284 let idx = driver.stack[driver.depth];
285 let mut v = value as i32;
286 value_offset = value_offset.next();
287 value = state_table.kerning(value_offset).unwrap_or(0);
288 if idx >= buffer.len {
289 continue;
290 }
291
292 // "The end of the list is marked by an odd value..."
293 last = v & 1 != 0;
294 v &= !1;
295
296 // Testing shows that CoreText only applies kern (cross-stream or not)
297 // if none has been applied by previous subtables. That is, it does
298 // NOT seem to accumulate as otherwise implied by specs.
299
300 let mut has_gpos_attachment = false;
301 let glyph_mask = buffer.info[idx].mask;
302 let pos = &mut buffer.pos[idx];
303
304 if buffer.direction.is_horizontal() {
305 if has_cross_stream {
306 // The following flag is undocumented in the spec, but described
307 // in the 'kern' table example.
308 if v == -0x8000 {
309 pos.set_attach_type(0);
310 pos.set_attach_chain(0);
311 pos.y_offset = 0;
312 } else if pos.attach_type() != 0 {
313 pos.y_offset += v;
314 has_gpos_attachment = true;
315 }
316 } else if glyph_mask & kern_mask != 0 {
317 pos.x_advance += v;
318 pos.x_offset += v;
319 }
320 } else {
321 if has_cross_stream {
322 // CoreText doesn't do crossStream kerning in vertical. We do.
323 if v == -0x8000 {
324 pos.set_attach_type(0);
325 pos.set_attach_chain(0);
326 pos.x_offset = 0;
327 } else if pos.attach_type() != 0 {
328 pos.x_offset += v;
329 has_gpos_attachment = true;
330 }
331 } else if glyph_mask & kern_mask != 0 {
332 if pos.y_offset == 0 {
333 pos.y_advance += v;
334 pos.y_offset += v;
335 }
336 }
337 }
338
339 if has_gpos_attachment {
340 buffer.scratch_flags |= BufferScratchFlags::HAS_GPOS_ATTACHMENT;
341 }
342 }
343 }
344}
345