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