oxideav_ttf/shape.rs
1//! OpenType GSUB/GPOS shaping pipeline.
2//!
3//! This module wires the per-lookup-type GSUB substitution and GPOS
4//! positioning primitives implemented in [`crate::tables::gsub`] and
5//! [`crate::tables::gpos`] into a single coherent
6//! [`Font::shape`](crate::Font::shape) entry point that turns a run of
7//! Unicode text into a sequence of positioned glyphs.
8//!
9//! ## Pipeline (ISO/IEC 14496-22:2019 §6 "OFF Layout Common Table
10//! Formats" + the GSUB/GPOS chapters)
11//!
12//! 1. **Character-to-glyph mapping.** Each input `char` is mapped to a
13//! nominal glyph id through the `cmap` table
14//! ([`Font::glyph_index`](crate::Font::glyph_index)). Characters with
15//! no mapping resolve to glyph 0 (`.notdef`).
16//!
17//! 2. **GSUB substitution stage.** The features the caller requested are
18//! resolved against the active script/language through the GSUB
19//! ScriptList → FeatureList → LangSys walk. Per the common-table-format
20//! rules, the *union* of the lookup indices referenced by the active
21//! features is gathered and processed **in LookupList order** (not
22//! feature order): "the client … processes the lookups referenced by
23//! these features in the order the lookup definitions occur in the
24//! LookupList … lookups from several different features may be
25//! interleaved during text processing." Each lookup is applied across
26//! the whole glyph buffer left-to-right (reverse-chaining LookupType 8
27//! is walked right-to-left).
28//!
29//! 3. **GPOS positioning stage.** Advances are seeded from `hmtx`. The
30//! active GPOS features' lookups are likewise gathered and applied in
31//! LookupList order, accumulating x/y placement and advance
32//! adjustments plus mark-attachment, cursive-attachment, and
33//! pair-kerning offsets onto each glyph.
34//!
35//! The result is a `Vec<`[`ShapedGlyph`]`>`: one entry per output glyph,
36//! carrying the glyph id, the originating cluster (byte index into the
37//! input text), and the placement/advance in font units (TT Y-up
38//! convention, scale by `units_per_em` for a target ppem).
39//!
40//! ## Scope
41//!
42//! This is a *general* OpenType shaper: it applies whatever lookups the
43//! requested features reference, for any script, without script-specific
44//! reordering logic (the spec explicitly places complex-script glyph
45//! reordering — e.g. Indic syllable reordering — outside its scope, in
46//! the text-processing client). For scripts whose joining/positional
47//! behaviour is fully expressed through GSUB/GPOS lookups keyed off
48//! contextual rules (Latin ligatures and kerning, Arabic joining forms
49//! driven by `init`/`medi`/`fina` + `mark`/`mkmk`/`curs`), the requested
50//! feature set drives correct output directly.
51
52use crate::tables::gpos::PosRecord;
53use crate::Font;
54
55/// Maximum number of GSUB lookup passes over the buffer, as a guard
56/// against a pathological self-growing lookup graph (a multiple- or
57/// contextual-substitution chain that keeps expanding the buffer).
58/// Real fonts converge in a handful of passes; this only bounds
59/// adversarial inputs.
60const MAX_GSUB_BUFFER_GROWTH: usize = 64;
61
62/// One positioned glyph emitted by [`Font::shape`].
63///
64/// All four positioning fields are in font design units (the same units
65/// as `head.unitsPerEm`), in the TrueType Y-up convention. To render at
66/// a target pixel-per-em `ppem`, scale by `ppem / units_per_em`.
67///
68/// * `glyph_id` — the final glyph id after all GSUB substitutions.
69/// * `cluster` — the byte offset into the original `&str` of the
70/// character (or first character of the ligated group) this glyph
71/// originated from. Stable across substitutions: a ligature inherits
72/// the cluster of its first component; a multiple-substitution
73/// expansion shares the source glyph's cluster across every output.
74/// * `x_offset` / `y_offset` — placement adjustment applied to the pen
75/// position *for drawing this glyph only* (does not move the pen).
76/// Marks attach to bases through this field.
77/// * `x_advance` / `y_advance` — how far the pen moves after drawing
78/// this glyph. Seeded from the horizontal `hmtx` advance, then
79/// adjusted by GPOS.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ShapedGlyph {
82 pub glyph_id: u16,
83 pub cluster: u32,
84 pub x_offset: i32,
85 pub y_offset: i32,
86 pub x_advance: i32,
87 pub y_advance: i32,
88}
89
90/// Internal working item during the GSUB stage. The position fields are
91/// not populated until the GPOS stage; we carry the glyph id + cluster
92/// here and materialise [`ShapedGlyph`] at the boundary.
93#[derive(Debug, Clone, Copy)]
94struct WorkGlyph {
95 gid: u16,
96 cluster: u32,
97}
98
99impl<'a> Font<'a> {
100 /// Shape a run of text into positioned glyphs under `script` /
101 /// `lang`, applying the listed `features`.
102 ///
103 /// `script` and `lang` are OpenType tags (`*b"latn"`, `*b"arab"`,
104 /// `*b"DFLT"`; `lang = None` selects the script's default language
105 /// system). `features` is the ordered list of feature tags the
106 /// caller wants enabled (e.g. `[*b"ccmp", *b"liga", *b"kern"]`); a
107 /// feature tag the font does not list under the active script is
108 /// silently ignored. The relative order of `features` does not by
109 /// itself dictate application order — the GSUB/GPOS lookups behind
110 /// the *union* of requested features run in LookupList order, per the
111 /// OpenType common-table-format rules — but it determines which
112 /// features are active.
113 ///
114 /// Returns one [`ShapedGlyph`] per output glyph. For a font with no
115 /// GSUB/GPOS, this degenerates to nominal cmap mapping with `hmtx`
116 /// advances (i.e. unshaped glyph runs still come back correctly
117 /// positioned for simple scripts).
118 ///
119 /// The variation-instance-aware feature resolution
120 /// ([`Font::gsub_features_for_script_at_instance`]) is used, so a
121 /// variable font shaped after [`Font::set_variation_coords`] honours
122 /// its FeatureVariations substitutions.
123 pub fn shape(
124 &self,
125 text: &str,
126 script: [u8; 4],
127 lang: Option<[u8; 4]>,
128 features: &[[u8; 4]],
129 ) -> Vec<ShapedGlyph> {
130 // --- 1. character-to-glyph mapping --------------------------------
131 let mut buf: Vec<WorkGlyph> = Vec::with_capacity(text.len());
132 for (byte_idx, ch) in text.char_indices() {
133 let gid = self.glyph_index(ch).unwrap_or(0);
134 buf.push(WorkGlyph {
135 gid,
136 cluster: byte_idx as u32,
137 });
138 }
139
140 // --- 2. GSUB substitution stage -----------------------------------
141 self.run_gsub(&mut buf, script, lang, features);
142
143 // --- 3. GPOS positioning stage ------------------------------------
144 self.run_gpos(buf, script, lang, features)
145 }
146
147 /// Resolve the active GSUB lookup indices for the requested features
148 /// and apply them, in LookupList order, across `buf`.
149 fn run_gsub(
150 &self,
151 buf: &mut Vec<WorkGlyph>,
152 script: [u8; 4],
153 lang: Option<[u8; 4]>,
154 features: &[[u8; 4]],
155 ) {
156 if self.gsub.is_none() {
157 return;
158 }
159 let resolved = self.gsub_features_for_script_at_instance(script, lang);
160 // Gather the union of lookup indices referenced by every active
161 // requested feature.
162 let mut active: Vec<u16> = Vec::new();
163 for feat in &resolved {
164 if !features.contains(&feat.tag) {
165 continue;
166 }
167 for &li in &feat.lookup_indices {
168 if !active.contains(&li) {
169 active.push(li);
170 }
171 }
172 }
173 if active.is_empty() {
174 return;
175 }
176 // Process in LookupList order, not feature order.
177 active.sort_unstable();
178
179 // Map each active lookup index to its (effective) type so we can
180 // pick the right per-type apply path.
181 let types = self.gsub_lookup_list();
182 for &li in &active {
183 let kind = types
184 .iter()
185 .find(|(idx, _, _)| *idx == li)
186 .map(|(_, k, _)| *k)
187 .unwrap_or(0);
188 let flags = self.gsub.as_ref().map(|g| g.lookup_flags(li)).unwrap_or(0);
189 self.apply_gsub_lookup(buf, li, kind, flags);
190 }
191 }
192
193 /// Apply one GSUB lookup of the given effective `kind` across the
194 /// whole buffer. `flags` is the lookup's `lookupFlag`; the
195 /// IGNORE_MARKS / IGNORE_BASE_GLYPHS / IGNORE_LIGATURES skip bits are
196 /// honoured where they affect substitution (most consequentially
197 /// IGNORE_MARKS on ligature lookups, so a combining mark sitting
198 /// between two ligature components doesn't block the ligature).
199 fn apply_gsub_lookup(&self, buf: &mut Vec<WorkGlyph>, li: u16, kind: u16, flags: u16) {
200 match kind {
201 1 => {
202 // Single substitution: 1:1, no length change.
203 for w in buf.iter_mut() {
204 if let Some(g) = self.gsub_apply_lookup_type_1(li, w.gid) {
205 w.gid = g;
206 }
207 }
208 }
209 2 => {
210 // Multiple substitution: 1 → N (or 0 = deletion). All
211 // outputs inherit the source cluster.
212 let mut out: Vec<WorkGlyph> = Vec::with_capacity(buf.len());
213 let mut growth = 0usize;
214 for w in buf.iter() {
215 match self.gsub_apply_lookup_type_2(li, w.gid) {
216 Some(seq) => {
217 growth += seq.len();
218 for g in seq {
219 out.push(WorkGlyph {
220 gid: g,
221 cluster: w.cluster,
222 });
223 }
224 }
225 None => out.push(*w),
226 }
227 if growth > buf.len() + MAX_GSUB_BUFFER_GROWTH {
228 // Pathological expansion guard: keep the rest
229 // unsubstituted.
230 break;
231 }
232 }
233 if growth <= buf.len() + MAX_GSUB_BUFFER_GROWTH {
234 *buf = out;
235 }
236 }
237 3 => {
238 // Alternate substitution: default to alternate 0.
239 for w in buf.iter_mut() {
240 if let Some(g) = self.gsub_apply_lookup_type_3(li, w.gid, 0) {
241 w.gid = g;
242 }
243 }
244 }
245 4 => {
246 // Ligature substitution: N → 1, consuming a prefix from
247 // each position. The ligature inherits the cluster of its
248 // first component. The lookup's skip filter (§2) decides
249 // which glyphs are invisible to the match: a lookup with
250 // IGNORE_MARKS matches over the *non-mark* glyphs and
251 // removes only the consumed visible components, leaving
252 // interspersed marks in place (they re-anchor to the
253 // ligature during GPOS); IGNORE_LIGATURES /
254 // MARK_ATTACHMENT_CLASS_FILTER / USE_MARK_FILTERING_SET
255 // narrow the match the same way.
256 let mfs = self.gsub_lookup_mark_filtering_set(li);
257 let mut i = 0usize;
258 while i < buf.len() {
259 if self.lookup_skips_glyph(flags, mfs, buf[i].gid) {
260 i += 1;
261 continue;
262 }
263 // Build the candidate run from position i, recording
264 // which absolute indices the non-skipped gids came from.
265 let mut cand_gids: Vec<u16> = Vec::new();
266 let mut cand_idx: Vec<usize> = Vec::new();
267 for (off, w) in buf[i..].iter().enumerate() {
268 if self.lookup_skips_glyph(flags, mfs, w.gid) {
269 continue;
270 }
271 cand_gids.push(w.gid);
272 cand_idx.push(i + off);
273 }
274 if let Some((lig, consumed)) = self.gsub_apply_lookup_type_4(li, &cand_gids) {
275 if consumed >= 1 {
276 let cluster = buf[i].cluster;
277 buf[i] = WorkGlyph { gid: lig, cluster };
278 // Remove the consumed components 1..consumed
279 // (their absolute indices), highest first so
280 // earlier removals don't shift later indices.
281 let to_remove: Vec<usize> =
282 cand_idx[1..consumed.min(cand_idx.len())].to_vec();
283 for &idx in to_remove.iter().rev() {
284 if idx < buf.len() {
285 buf.remove(idx);
286 }
287 }
288 i += 1;
289 continue;
290 }
291 }
292 i += 1;
293 }
294 }
295 5 => {
296 // Contextual substitution. apply_lookup_type_5 returns the
297 // rewritten run (full buffer) on a hit at `pos`.
298 let mut pos = 0usize;
299 while pos < buf.len() {
300 let gids: Vec<u16> = buf.iter().map(|w| w.gid).collect();
301 if let Some(rewritten) = self.gsub_apply_lookup_type_5(li, &gids, pos) {
302 self.reconcile_context_rewrite(buf, &gids, rewritten, pos);
303 }
304 pos += 1;
305 }
306 }
307 6 => {
308 // Chained-context substitution.
309 let mut pos = 0usize;
310 while pos < buf.len() {
311 let gids: Vec<u16> = buf.iter().map(|w| w.gid).collect();
312 if let Some(rewritten) = self.gsub_apply_lookup_type_6(li, &gids, pos) {
313 self.reconcile_context_rewrite(buf, &gids, rewritten, pos);
314 }
315 pos += 1;
316 }
317 }
318 8 => {
319 // Reverse chained-context single substitution: 1:1, walked
320 // right-to-left so a later substitution's lookahead sees
321 // the original (not yet substituted) glyphs.
322 let gids: Vec<u16> = buf.iter().map(|w| w.gid).collect();
323 for pos in (0..buf.len()).rev() {
324 if let Some(g) = self.gsub_apply_lookup_type_8(li, &gids, pos) {
325 buf[pos].gid = g;
326 }
327 }
328 }
329 _ => {}
330 }
331 }
332
333 /// Reconcile a contextual/chained GSUB rewrite (which returns a full
334 /// rewritten gid run) back into the `WorkGlyph` buffer, preserving
335 /// clusters as best we can. The rewrite may change the buffer length
336 /// (a nested multiple- or ligature-substitution record). We align the
337 /// unchanged prefix/suffix and assign the source cluster of `pos` to
338 /// any glyphs in the changed middle.
339 fn reconcile_context_rewrite(
340 &self,
341 buf: &mut Vec<WorkGlyph>,
342 old: &[u16],
343 new: Vec<u16>,
344 pos: usize,
345 ) {
346 if new == old {
347 return;
348 }
349 // Common unchanged prefix.
350 let mut pre = 0usize;
351 while pre < old.len() && pre < new.len() && old[pre] == new[pre] {
352 pre += 1;
353 }
354 // Common unchanged suffix.
355 let mut suf = 0usize;
356 while suf < (old.len() - pre)
357 && suf < (new.len() - pre)
358 && old[old.len() - 1 - suf] == new[new.len() - 1 - suf]
359 {
360 suf += 1;
361 }
362 let cluster = buf.get(pos).map(|w| w.cluster).unwrap_or(0);
363 let mut rebuilt: Vec<WorkGlyph> = Vec::with_capacity(new.len());
364 for &g in &new[..pre] {
365 let c = buf.get(rebuilt.len()).map(|w| w.cluster).unwrap_or(cluster);
366 rebuilt.push(WorkGlyph { gid: g, cluster: c });
367 }
368 for &g in &new[pre..new.len() - suf] {
369 rebuilt.push(WorkGlyph { gid: g, cluster });
370 }
371 let suffix_start_old = old.len() - suf;
372 for (k, &g) in new[new.len() - suf..].iter().enumerate() {
373 let c = buf
374 .get(suffix_start_old + k)
375 .map(|w| w.cluster)
376 .unwrap_or(cluster);
377 rebuilt.push(WorkGlyph { gid: g, cluster: c });
378 }
379 *buf = rebuilt;
380 }
381
382 /// GPOS positioning stage. Seeds advances from `hmtx`, then applies
383 /// the active GPOS lookups in LookupList order.
384 fn run_gpos(
385 &self,
386 buf: Vec<WorkGlyph>,
387 script: [u8; 4],
388 lang: Option<[u8; 4]>,
389 features: &[[u8; 4]],
390 ) -> Vec<ShapedGlyph> {
391 // Seed every glyph with its nominal horizontal advance.
392 let mut out: Vec<ShapedGlyph> = buf
393 .iter()
394 .map(|w| ShapedGlyph {
395 glyph_id: w.gid,
396 cluster: w.cluster,
397 x_offset: 0,
398 y_offset: 0,
399 x_advance: self.glyph_advance(w.gid) as i32,
400 y_advance: 0,
401 })
402 .collect();
403
404 if self.gpos.is_none() {
405 return out;
406 }
407 let resolved = self.gpos_features_for_script_at_instance(script, lang);
408 let mut active: Vec<u16> = Vec::new();
409 for feat in &resolved {
410 if !features.contains(&feat.tag) {
411 continue;
412 }
413 for &li in &feat.lookup_indices {
414 if !active.contains(&li) {
415 active.push(li);
416 }
417 }
418 }
419 if active.is_empty() {
420 return out;
421 }
422 active.sort_unstable();
423
424 let types = self.gpos_lookup_list();
425 for &li in &active {
426 let kind = types
427 .iter()
428 .find(|(idx, _, _)| *idx == li)
429 .map(|(_, k, _)| *k)
430 .unwrap_or(0);
431 self.apply_gpos_lookup(&mut out, li, kind);
432 }
433 out
434 }
435
436 /// Apply one GPOS lookup of the given effective `kind` across the
437 /// positioned buffer.
438 fn apply_gpos_lookup(&self, out: &mut [ShapedGlyph], li: u16, kind: u16) {
439 match kind {
440 1 => {
441 // Single adjustment.
442 for g in out.iter_mut() {
443 if let Some(v) = self.gpos_apply_lookup_type_1(li, g.glyph_id) {
444 g.x_offset += v.x_placement as i32;
445 g.y_offset += v.y_placement as i32;
446 g.x_advance += v.x_advance as i32;
447 g.y_advance += v.y_advance as i32;
448 }
449 }
450 }
451 2 => {
452 // Pair adjustment (kerning). The legacy single-value
453 // `lookup_kerning` path extracts the x-advance applied to
454 // the left glyph of each pair. The pair members are the
455 // current glyph and the *next non-skipped* glyph per the
456 // lookup's §2 skip filter — so a kern pair separated by an
457 // (ignored) combining mark still kerns, the canonical
458 // IGNORE_MARKS-on-kern case.
459 let gdef = self.gdef.as_ref();
460 let flags = self.gpos_lookup_flags(li);
461 let mfs = self.gpos_lookup_mark_filtering_set(li);
462 for i in 0..out.len() {
463 if self.lookup_skips_glyph(flags, mfs, out[i].glyph_id) {
464 continue;
465 }
466 // Find the next glyph the lookup does not skip.
467 let right_idx = ((i + 1)..out.len())
468 .find(|&k| !self.lookup_skips_glyph(flags, mfs, out[k].glyph_id));
469 let right_idx = match right_idx {
470 Some(k) => k,
471 None => break,
472 };
473 let left = out[i].glyph_id;
474 let right = out[right_idx].glyph_id;
475 let adj = self
476 .gpos
477 .as_ref()
478 .map(|g| g.lookup_kerning_at(li, left, right, gdef))
479 .unwrap_or(0);
480 out[i].x_advance += adj as i32;
481 }
482 }
483 3 => {
484 // Cursive attachment: glyph N+1's entry anchor lands on
485 // glyph N's exit anchor. The per-glyph delta moves N+1 so
486 // its entry aligns with N's exit (x via offset, the
487 // baseline shift via y_offset). Glyphs the lookup skips
488 // (§2) are invisible to the chain, so the exit of N is
489 // matched against the entry of the next *non-skipped*
490 // glyph.
491 let flags = self.gpos_lookup_flags(li);
492 let mfs = self.gpos_lookup_mark_filtering_set(li);
493 let mut prev_exit: Option<(i16, i16)> = None;
494 for g in out.iter_mut() {
495 if self.lookup_skips_glyph(flags, mfs, g.glyph_id) {
496 continue;
497 }
498 if let Some(att) = self.gpos_apply_lookup_type_3(li, g.glyph_id) {
499 if let (Some((px, py)), Some((ex, ey))) = (prev_exit, att.entry) {
500 g.x_offset += (px - ex) as i32;
501 g.y_offset += (py - ey) as i32;
502 }
503 prev_exit = att.exit;
504 } else {
505 prev_exit = None;
506 }
507 }
508 }
509 4 => {
510 // Mark-to-base: a mark glyph attaches to the nearest
511 // preceding base glyph.
512 self.apply_mark_attach(out, li, false);
513 }
514 5 => {
515 // Mark-to-ligature: a mark attaches to a component of a
516 // preceding ligature. We attach to the last preceding
517 // ligature, component 0 (a reasonable default without
518 // per-component cluster tracking from the substitution
519 // stage); the per-lookup apply path handles component
520 // resolution when given an explicit component.
521 self.apply_mark_to_ligature(out, li);
522 }
523 6 => {
524 // Mark-to-mark: a mark attaches to the immediately
525 // preceding mark.
526 self.apply_mark_attach(out, li, true);
527 }
528 7 => {
529 // Contextual positioning.
530 let gids: Vec<u16> = out.iter().map(|g| g.glyph_id).collect();
531 for pos in 0..out.len() {
532 if let Some(records) = self.gpos_apply_lookup_type_7(li, &gids, pos) {
533 apply_pos_records(out, &records);
534 }
535 }
536 }
537 8 => {
538 // Chained-context positioning.
539 let gids: Vec<u16> = out.iter().map(|g| g.glyph_id).collect();
540 for pos in 0..out.len() {
541 if let Some(records) = self.gpos_apply_lookup_type_8(li, &gids, pos) {
542 apply_pos_records(out, &records);
543 }
544 }
545 }
546 _ => {}
547 }
548 }
549
550 /// Shared mark-to-base (`to_mark = false`) / mark-to-mark
551 /// (`to_mark = true`) attachment. For each mark glyph, find the
552 /// nearest preceding attachment glyph (a base for mark-to-base, a
553 /// mark for mark-to-mark) that the lookup binds it to, and offset the
554 /// mark so its anchor lands on the base's anchor.
555 ///
556 /// The candidate attachment glyph is the nearest preceding glyph the
557 /// lookup's §2 skip filter does *not* ignore: a mark-to-base lookup
558 /// almost always sets IGNORE_MARKS so the scan steps over interspersed
559 /// marks and lands on the base, while a mark-to-mark (`mkmk`) lookup
560 /// leaves marks visible so it pairs with the immediately preceding
561 /// mark. The current mark itself is left unattached when the lookup
562 /// skips it.
563 fn apply_mark_attach(&self, out: &mut [ShapedGlyph], li: u16, to_mark: bool) {
564 let flags = self.gpos_lookup_flags(li);
565 let mfs = self.gpos_lookup_mark_filtering_set(li);
566 for i in 0..out.len() {
567 let mark = out[i].glyph_id;
568 if self.lookup_skips_glyph(flags, mfs, mark) {
569 continue;
570 }
571 // Scan backwards for the nearest non-skipped attachment glyph.
572 for j in (0..i).rev() {
573 let base = out[j].glyph_id;
574 if self.lookup_skips_glyph(flags, mfs, base) {
575 continue;
576 }
577 let hit = if to_mark {
578 self.gpos
579 .as_ref()
580 .and_then(|g| g.apply_mark_to_mark_at(li, base, mark))
581 } else {
582 self.gpos
583 .as_ref()
584 .and_then(|g| g.apply_mark_to_base_at(li, base, mark))
585 };
586 if let Some((dx, dy)) = hit {
587 // Place the mark relative to the base's pen origin.
588 // The base sits at the accumulated advance from j to i;
589 // a mark has (typically) zero advance, so its drawing
590 // origin is the current pen. We express attachment as a
591 // placement offset that pulls the mark back over the
592 // base by the base's advance run plus the anchor delta.
593 let between: i32 = out[j..i].iter().map(|g| g.x_advance).sum();
594 out[i].x_offset += dx as i32 - between;
595 out[i].y_offset += dy as i32;
596 }
597 // The first non-skipped predecessor is the only attachment
598 // candidate, whether or not it produced a hit.
599 break;
600 }
601 }
602 }
603
604 /// Mark-to-ligature attachment (LookupType 5). Attaches each mark to
605 /// the nearest preceding ligature glyph at component 0. The candidate
606 /// ligature is the nearest preceding glyph the lookup's §2 skip filter
607 /// does not ignore (a `mark` / mark-to-ligature lookup typically sets
608 /// IGNORE_MARKS so the scan steps over interspersed marks onto the
609 /// ligature).
610 fn apply_mark_to_ligature(&self, out: &mut [ShapedGlyph], li: u16) {
611 let flags = self.gpos_lookup_flags(li);
612 let mfs = self.gpos_lookup_mark_filtering_set(li);
613 for i in 0..out.len() {
614 let mark = out[i].glyph_id;
615 if self.lookup_skips_glyph(flags, mfs, mark) {
616 continue;
617 }
618 for j in (0..i).rev() {
619 let lig = out[j].glyph_id;
620 if self.lookup_skips_glyph(flags, mfs, lig) {
621 continue;
622 }
623 if let Some((dx, dy)) = self
624 .gpos
625 .as_ref()
626 .and_then(|g| g.apply_lookup_type_5(li, lig, 0, mark))
627 {
628 let between: i32 = out[j..i].iter().map(|g| g.x_advance).sum();
629 out[i].x_offset += dx as i32 - between;
630 out[i].y_offset += dy as i32;
631 }
632 // The first non-skipped predecessor is the only candidate.
633 break;
634 }
635 }
636 }
637}
638
639/// Apply a set of [`PosRecord`]s (absolute-indexed) from a contextual /
640/// chained positioning match onto the output buffer.
641fn apply_pos_records(out: &mut [ShapedGlyph], records: &[PosRecord]) {
642 for r in records {
643 if let Some(g) = out.get_mut(r.glyph_index) {
644 g.x_offset += r.value.x_placement as i32;
645 g.y_offset += r.value.y_placement as i32;
646 g.x_advance += r.value.x_advance as i32;
647 g.y_advance += r.value.y_advance as i32;
648 }
649 }
650}