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 | 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 | |
123 | fn 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 | |
189 | const START_OF_TEXT: u16 = 0; |
190 | |
191 | trait KerxEntryDataExt { |
192 | fn action_index(self) -> u16; |
193 | fn is_actionable(&self) -> bool; |
194 | } |
195 | |
196 | impl 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 | |
205 | fn 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 | |
278 | trait 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 | |
291 | struct Driver1 { |
292 | stack: [usize; 8], |
293 | depth: usize, |
294 | } |
295 | |
296 | impl 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 | |
398 | struct Driver4<'a> { |
399 | mark_set: bool, |
400 | mark: usize, |
401 | ankr_table: Option<ankr::Table<'a>>, |
402 | } |
403 | |
404 | impl 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 | |