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