| 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 | |