1 | use core::convert::TryFrom; |
2 | |
3 | use ttf_parser::{ankr, apple_layout, kerx, FromData, GlyphId}; |
4 | |
5 | use crate::buffer::{Buffer, BufferScratchFlags}; |
6 | use crate::ot::matching::SkippyIter; |
7 | use crate::ot::{attach_type, lookup_flags, ApplyContext, TableIndex}; |
8 | use crate::plan::ShapePlan; |
9 | use crate::Face; |
10 | |
11 | trait 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 | |
16 | impl 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 | |
30 | impl 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 | |
44 | pub(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 | |
125 | fn 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 | |
194 | const START_OF_TEXT: u16 = 0; |
195 | |
196 | trait KerxEntryDataExt { |
197 | fn action_index(self) -> u16; |
198 | fn is_actionable(&self) -> bool; |
199 | } |
200 | |
201 | impl 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 | |
210 | fn 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 | |
286 | trait 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 | |
299 | struct Driver1 { |
300 | stack: [usize; 8], |
301 | depth: usize, |
302 | } |
303 | |
304 | impl 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 | |
406 | struct Driver4<'a> { |
407 | mark_set: bool, |
408 | mark: usize, |
409 | ankr_table: Option<ankr::Table<'a>>, |
410 | } |
411 | |
412 | impl 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 | |