1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! Round 15 — general-script (Latin / Cyrillic / Greek / DFLT) GSUB
//! feature pass. Wires `ccmp` (Glyph Composition / Decomposition) and
//! `calt` (Contextual Alternates) into the post-cmap path of
//! [`crate::shaper::shape_run_with_font`] for fonts whose `latn` /
//! `cyrl` / `grek` / `DFLT` script tables publish those features.
//!
//! ## Why this exists
//!
//! The OpenType spec (chapter 5 — "OpenType Layout common table
//! formats", "Required Features") lists `ccmp` as a feature that
//! shapers MUST apply at the start of the GSUB pass for every script.
//! `ccmp` is the canonical site for fonts to decompose a precomposed
//! codepoint into base + combining sequence (or compose the other way)
//! so the rest of the pipeline can attach marks correctly. The
//! round-1..14 pipeline ignored `ccmp` entirely, which meant any font
//! relying on its `ccmp` decomposition for diacritic placement (Inter
//! has 7 such lookups; DejaVu has 2; Noto Sans Arabic has 1) was
//! shaping incorrectly. Same for `calt`: Latin "ct" / "st" historical
//! ligatures, fraction collapsing, swash variants in display faces —
//! all live in `calt` and need contextual-substitution dispatch
//! (GSUB LookupType 5 / 6) which the existing `lookup_ligature` walker
//! cannot reach.
//!
//! ## Application order
//!
//! The OpenType spec doesn't enumerate a per-script feature application
//! order beyond "required features first" — most production shapers
//! apply features in the order they're declared by the font, but
//! `ccmp` is always first (required-feature semantics) and `calt`
//! always after `liga` / `clig` (contextual alternates often refine
//! the output of the ligature pass). We follow the same convention:
//!
//! 1. `ccmp` — pre-substitution decomposition / composition.
//! 2. `liga` / `clig` — handled by the existing `lookup_ligature`
//! walker in [`crate::shaper::shape_run_with_font`].
//! 3. `calt` — post-ligature contextual refinement.
//!
//! Lookups are dispatched via the appropriate type entry point —
//! [`oxideav_ttf::Font::gsub_apply_lookup_type_1`] for single
//! substitution, type 2 for multiple (decomposition), type 4 for
//! ligature, type 5 / 6 for contextual / chained context. A lookup
//! whose declared type isn't one of those (e.g. GPOS-only) is silently
//! skipped — `oxideav-ttf` returns `None` for the wrong type and the
//! caller keeps the input glyph unchanged.
//!
//! ## Script auto-detection
//!
//! `shape_run_with_font` doesn't have access to the original codepoints
//! (the caller already cmap'd them), so script detection at the GSUB-
//! application stage isn't possible without a side channel. We instead
//! probe a fixed-priority list of script tags — `latn`, `cyrl`, `grek`,
//! `DFLT` — and apply every `ccmp` / `calt` feature the font publishes
//! under the FIRST tag that has any matching features. The lookup's own
//! coverage table decides per-glyph whether a substitution fires, so
//! probing a script tag the run doesn't actually use is benign: the
//! coverage table won't match the input glyph and the lookup returns
//! `None`. Indic and Arabic shaping uses different script tags
//! (`dev2` / `deva` / `arab`) and is dispatched from the per-script
//! pipelines in [`crate::face_chain`] before this pass runs.
//!
//! Source: docs/document/pdf/PDF32000-2008.html (text rendering chapter
//! for font substitution semantics) — there is no project-vendored
//! OpenType spec, so this module's feature ordering follows the
//! Microsoft Typography "Registered features" published rules
//! transcribed from public OpenType registry documentation. No HarfBuzz
//! / FreeType / ICU source was read.
use Font;
/// Apply `ccmp` (Glyph Composition / Decomposition) substitutions to
/// `gids` for whichever of the probed general scripts (`latn`, `cyrl`,
/// `grek`, `DFLT`) the font has features under. Returns the rewritten
/// glyph run. Length may change — `ccmp` is the canonical
/// decomposition-and-composition site, so a `ç` decomposing into `c` +
/// combining-cedilla via a LookupType 2 multiple-substitution is exactly
/// what this pass enables.
///
/// **Idempotent.** Re-running this on already-shaped output is a no-op
/// because the lookups are designed to fire at most once per glyph
/// (coverage table excludes the post-substitution glyph).
/// Apply `calt` (Contextual Alternates) substitutions to `gids`.
/// Typically dispatched AFTER the round-1 ligature pass — `calt` rules
/// often refine the output of `liga` / `clig` (e.g. picking a wider
/// "ct" variant only when the input was the historical "ct" digraph).
///
/// See [`apply_ccmp`] for the script-tag probing strategy and the
/// no-op-when-absent contract.
/// Generic feature-tag dispatcher used by [`apply_ccmp`] / [`apply_calt`].
/// Walks the probed script tag list and applies every lookup the chosen
/// script publishes for `feature_tag`. Each lookup is dispatched
/// according to its declared GSUB LookupType — types 1, 2, 4, 5, 6
/// are all covered.
/// Apply a single GSUB lookup of declared `ty` across every position in
/// `gids`. Returns the rewritten run.
///
/// Per-type semantics:
/// - **Type 1 (single)**: replace one glyph with one; length-preserving.
/// - **Type 2 (multiple)**: replace one glyph with N; length-changing.
/// - **Type 3 (alternate)**: pick alternate 0 (the default per spec —
/// user-driven indices live above this layer).
/// - **Type 4 (ligature)**: replace M glyphs with one; length-changing.
/// - **Type 5 (contextual)**: replace a window; length may change.
/// - **Type 6 (chained context)**: replace a window with
/// backtrack/lookahead; length may change.
///
/// The walker advances by one position when no substitution fires; when
/// a substitution fires it adopts the rewrite and re-examines the same
/// position so that a follow-on rule can match the new glyph. Iteration
/// is bounded by `4 * gids.len() + 8` to prevent pathological loops in
/// fonts whose chained-context rules might otherwise be self-feeding.