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