1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::any::Any;
4
5use crate::complex::{complex_categorize, ComplexShaper, DEFAULT_SHAPER, DUMBER_SHAPER};
6use crate::ot::{self, feature, FeatureFlags, TableIndex};
7use crate::{aat, Direction, Face, Feature, Language, Mask, Script, Tag};
8
9/// A reusable plan for shaping a text buffer.
10pub struct ShapePlan {
11 pub(crate) direction: Direction,
12 pub(crate) script: Option<Script>,
13 pub(crate) shaper: &'static ComplexShaper,
14 pub(crate) ot_map: ot::Map,
15 pub(crate) aat_map: aat::Map,
16 data: Option<Box<dyn Any + Send + Sync>>,
17
18 pub(crate) frac_mask: Mask,
19 pub(crate) numr_mask: Mask,
20 pub(crate) dnom_mask: Mask,
21 pub(crate) rtlm_mask: Mask,
22 pub(crate) kern_mask: Mask,
23 pub(crate) trak_mask: Mask,
24
25 pub(crate) requested_kerning: bool,
26 pub(crate) has_frac: bool,
27 pub(crate) has_vert: bool,
28 pub(crate) has_gpos_mark: bool,
29 pub(crate) zero_marks: bool,
30 pub(crate) fallback_glyph_classes: bool,
31 pub(crate) fallback_mark_positioning: bool,
32 pub(crate) adjust_mark_positioning_when_zeroing: bool,
33
34 pub(crate) apply_gpos: bool,
35 pub(crate) apply_fallback_kern: bool,
36 pub(crate) apply_kern: bool,
37 pub(crate) apply_kerx: bool,
38 pub(crate) apply_morx: bool,
39 pub(crate) apply_trak: bool,
40
41 pub(crate) user_features: Vec<Feature>,
42}
43
44impl ShapePlan {
45 /// Returns a plan that can be used for shaping any buffer with the
46 /// provided properties.
47 pub fn new(
48 face: &Face,
49 direction: Direction,
50 script: Option<Script>,
51 language: Option<&Language>,
52 user_features: &[Feature],
53 ) -> Self {
54 assert_ne!(direction, Direction::Invalid);
55 let mut planner: ShapePlanner<'_> = ShapePlanner::new(face, direction, script, language);
56 planner.collect_features(user_features);
57 planner.compile(user_features)
58 }
59
60 pub(crate) fn data<T: 'static>(&self) -> &T {
61 self.data.as_ref().unwrap().downcast_ref().unwrap()
62 }
63}
64
65pub struct ShapePlanner<'a> {
66 pub face: &'a Face<'a>,
67 pub direction: Direction,
68 pub script: Option<Script>,
69 pub ot_map: ot::MapBuilder<'a>,
70 pub aat_map: aat::MapBuilder,
71 pub apply_morx: bool,
72 pub script_zero_marks: bool,
73 pub script_fallback_mark_positioning: bool,
74 pub shaper: &'static ComplexShaper,
75}
76
77impl<'a> ShapePlanner<'a> {
78 fn new(
79 face: &'a Face<'a>,
80 direction: Direction,
81 script: Option<Script>,
82 language: Option<&Language>,
83 ) -> Self {
84 let ot_map = ot::MapBuilder::new(face, script, language);
85 let aat_map = aat::MapBuilder::default();
86
87 let mut shaper = match script {
88 Some(script) => {
89 complex_categorize(script, direction, ot_map.chosen_script(TableIndex::GSUB))
90 }
91 None => &DEFAULT_SHAPER,
92 };
93
94 let script_zero_marks = shaper.zero_width_marks.is_some();
95 let script_fallback_mark_positioning = shaper.fallback_position;
96
97 // https://github.com/harfbuzz/harfbuzz/issues/2124
98 let apply_morx =
99 face.tables().morx.is_some() && (direction.is_horizontal() || face.gsub.is_none());
100
101 // https://github.com/harfbuzz/harfbuzz/issues/1528
102 if apply_morx && shaper as *const _ != &DEFAULT_SHAPER as *const _ {
103 shaper = &DUMBER_SHAPER;
104 }
105
106 ShapePlanner {
107 face,
108 direction,
109 script,
110 ot_map,
111 aat_map,
112 apply_morx,
113 script_zero_marks,
114 script_fallback_mark_positioning,
115 shaper,
116 }
117 }
118
119 fn collect_features(&mut self, user_features: &[Feature]) {
120 const COMMON_FEATURES: &[(Tag, FeatureFlags)] = &[
121 (feature::ABOVE_BASE_MARK_POSITIONING, FeatureFlags::GLOBAL),
122 (feature::BELOW_BASE_MARK_POSITIONING, FeatureFlags::GLOBAL),
123 (
124 feature::GLYPH_COMPOSITION_DECOMPOSITION,
125 FeatureFlags::GLOBAL,
126 ),
127 (feature::LOCALIZED_FORMS, FeatureFlags::GLOBAL),
128 (
129 feature::MARK_POSITIONING,
130 FeatureFlags::GLOBAL_MANUAL_JOINERS,
131 ),
132 (
133 feature::MARK_TO_MARK_POSITIONING,
134 FeatureFlags::GLOBAL_MANUAL_JOINERS,
135 ),
136 (feature::REQUIRED_LIGATURES, FeatureFlags::GLOBAL),
137 ];
138
139 const HORIZONTAL_FEATURES: &[(Tag, FeatureFlags)] = &[
140 (feature::CONTEXTUAL_ALTERNATES, FeatureFlags::GLOBAL),
141 (feature::CONTEXTUAL_LIGATURES, FeatureFlags::GLOBAL),
142 (feature::CURSIVE_POSITIONING, FeatureFlags::GLOBAL),
143 (feature::DISTANCES, FeatureFlags::GLOBAL),
144 (feature::KERNING, FeatureFlags::GLOBAL_HAS_FALLBACK),
145 (feature::STANDARD_LIGATURES, FeatureFlags::GLOBAL),
146 (
147 feature::REQUIRED_CONTEXTUAL_ALTERNATES,
148 FeatureFlags::GLOBAL,
149 ),
150 ];
151
152 let empty = FeatureFlags::empty();
153
154 self.ot_map
155 .enable_feature(feature::REQUIRED_VARIATION_ALTERNATES, empty, 1);
156 self.ot_map.add_gsub_pause(None);
157
158 match self.direction {
159 Direction::LeftToRight => {
160 self.ot_map
161 .enable_feature(feature::LEFT_TO_RIGHT_ALTERNATES, empty, 1);
162 self.ot_map
163 .enable_feature(feature::LEFT_TO_RIGHT_MIRRORED_FORMS, empty, 1);
164 }
165 Direction::RightToLeft => {
166 self.ot_map
167 .enable_feature(feature::RIGHT_TO_LEFT_ALTERNATES, empty, 1);
168 self.ot_map
169 .add_feature(feature::RIGHT_TO_LEFT_MIRRORED_FORMS, empty, 1);
170 }
171 _ => {}
172 }
173
174 // Automatic fractions.
175 self.ot_map.add_feature(feature::FRACTIONS, empty, 1);
176 self.ot_map.add_feature(feature::NUMERATORS, empty, 1);
177 self.ot_map.add_feature(feature::DENOMINATORS, empty, 1);
178
179 // Random!
180 self.ot_map
181 .enable_feature(feature::RANDOMIZE, FeatureFlags::RANDOM, ot::Map::MAX_VALUE);
182
183 // Tracking. We enable dummy feature here just to allow disabling
184 // AAT 'trak' table using features.
185 // https://github.com/harfbuzz/harfbuzz/issues/1303
186 self.ot_map
187 .enable_feature(Tag::from_bytes(b"trak"), FeatureFlags::HAS_FALLBACK, 1);
188
189 self.ot_map
190 .enable_feature(Tag::from_bytes(b"HARF"), empty, 1);
191
192 if let Some(func) = self.shaper.collect_features {
193 func(self);
194 }
195
196 self.ot_map
197 .enable_feature(Tag::from_bytes(b"BUZZ"), empty, 1);
198
199 for &(tag, flags) in COMMON_FEATURES {
200 self.ot_map.add_feature(tag, flags, 1);
201 }
202
203 if self.direction.is_horizontal() {
204 for &(tag, flags) in HORIZONTAL_FEATURES {
205 self.ot_map.add_feature(tag, flags, 1);
206 }
207 } else {
208 // We only apply `vert` feature. See:
209 // https://github.com/harfbuzz/harfbuzz/commit/d71c0df2d17f4590d5611239577a6cb532c26528
210 // https://lists.freedesktop.org/archives/harfbuzz/2013-August/003490.html
211
212 // We really want to find a 'vert' feature if there's any in the font, no
213 // matter which script/langsys it is listed (or not) under.
214 // See various bugs referenced from:
215 // https://github.com/harfbuzz/harfbuzz/issues/63
216 self.ot_map
217 .enable_feature(feature::VERTICAL_WRITING, FeatureFlags::GLOBAL_SEARCH, 1);
218 }
219
220 for feature in user_features {
221 let flags = if feature.is_global() {
222 FeatureFlags::GLOBAL
223 } else {
224 empty
225 };
226 self.ot_map.add_feature(feature.tag, flags, feature.value);
227 }
228
229 if self.apply_morx {
230 for feature in user_features {
231 self.aat_map
232 .add_feature(self.face, feature.tag, feature.value);
233 }
234 }
235
236 if let Some(func) = self.shaper.override_features {
237 func(self);
238 }
239 }
240
241 fn compile(mut self, user_features: &[Feature]) -> ShapePlan {
242 let ot_map = self.ot_map.compile();
243
244 let aat_map = if self.apply_morx {
245 self.aat_map.compile(self.face)
246 } else {
247 aat::Map::default()
248 };
249
250 let frac_mask = ot_map.one_mask(feature::FRACTIONS);
251 let numr_mask = ot_map.one_mask(feature::NUMERATORS);
252 let dnom_mask = ot_map.one_mask(feature::DENOMINATORS);
253 let has_frac = frac_mask != 0 || (numr_mask != 0 && dnom_mask != 0);
254
255 let rtlm_mask = ot_map.one_mask(feature::RIGHT_TO_LEFT_MIRRORED_FORMS);
256 let has_vert = ot_map.one_mask(feature::VERTICAL_WRITING) != 0;
257
258 let horizontal = self.direction.is_horizontal();
259 let kern_tag = if horizontal {
260 feature::KERNING
261 } else {
262 feature::VERTICAL_KERNING
263 };
264 let kern_mask = ot_map.mask(kern_tag).0;
265 let requested_kerning = kern_mask != 0;
266 let trak_mask = ot_map.mask(Tag::from_bytes(b"trak")).0;
267 let requested_tracking = trak_mask != 0;
268
269 let has_gpos_kern = ot_map.feature_index(TableIndex::GPOS, kern_tag).is_some();
270 let disable_gpos = self.shaper.gpos_tag.is_some()
271 && self.shaper.gpos_tag != ot_map.chosen_script(TableIndex::GPOS);
272
273 // Decide who provides glyph classes. GDEF or Unicode.
274 let fallback_glyph_classes = !self
275 .face
276 .tables()
277 .gdef
278 .map_or(false, |table| table.has_glyph_classes());
279
280 // Decide who does substitutions. GSUB, morx, or fallback.
281 let apply_morx = self.apply_morx;
282
283 let mut apply_gpos = false;
284 let mut apply_kerx = false;
285 let mut apply_kern = false;
286
287 // Decide who does positioning. GPOS, kerx, kern, or fallback.
288 let has_gsub = self.face.tables().gsub.is_some();
289 let has_gpos = !disable_gpos && self.face.tables().gpos.is_some();
290
291 if self.face.tables().kerx.is_some() && !(has_gsub && has_gpos) {
292 apply_kerx = true;
293 } else if !apply_morx && has_gpos {
294 apply_gpos = true;
295 }
296
297 if !apply_kerx && (!has_gpos_kern || !apply_gpos) {
298 // Apparently Apple applies kerx if GPOS kern was not applied.
299 if self.face.tables().kerx.is_some() {
300 apply_kerx = true;
301 } else if ot::has_kerning(self.face) {
302 apply_kern = true;
303 }
304 }
305
306 let apply_fallback_kern = !(apply_gpos || apply_kerx || apply_kern);
307 let zero_marks = self.script_zero_marks
308 && !apply_kerx
309 && (!apply_kern || !ot::has_machine_kerning(self.face));
310
311 let has_gpos_mark = ot_map.one_mask(feature::MARK_POSITIONING) != 0;
312
313 let mut adjust_mark_positioning_when_zeroing =
314 !apply_gpos && !apply_kerx && (!apply_kern || !ot::has_cross_kerning(self.face));
315
316 let fallback_mark_positioning =
317 adjust_mark_positioning_when_zeroing && self.script_fallback_mark_positioning;
318
319 // If we're using morx shaping, we cancel mark position adjustment because
320 // Apple Color Emoji assumes this will NOT be done when forming emoji sequences;
321 // https://github.com/harfbuzz/harfbuzz/issues/2967.
322 if apply_morx {
323 adjust_mark_positioning_when_zeroing = false;
324 }
325
326 // Currently we always apply trak.
327 let apply_trak = requested_tracking && self.face.tables().trak.is_some();
328
329 let mut plan = ShapePlan {
330 direction: self.direction,
331 script: self.script,
332 shaper: self.shaper,
333 ot_map,
334 aat_map,
335 data: None,
336 frac_mask,
337 numr_mask,
338 dnom_mask,
339 rtlm_mask,
340 kern_mask,
341 trak_mask,
342 requested_kerning,
343 has_frac,
344 has_vert,
345 has_gpos_mark,
346 zero_marks,
347 fallback_glyph_classes,
348 fallback_mark_positioning,
349 adjust_mark_positioning_when_zeroing,
350 apply_gpos,
351 apply_kern,
352 apply_fallback_kern,
353 apply_kerx,
354 apply_morx,
355 apply_trak,
356 user_features: user_features.to_vec(),
357 };
358
359 if let Some(func) = self.shaper.create_data {
360 plan.data = Some(func(&plan));
361 }
362
363 plan
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::ShapePlan;
370
371 #[test]
372 fn test_shape_plan_is_send_and_sync() {
373 fn ensure_send_and_sync<T: Send + Sync>() {}
374 ensure_send_and_sync::<ShapePlan>();
375 }
376}
377