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); // Considered required.
191 self.ot_map
192 .enable_feature(Tag::from_bytes(b"HARF"), empty, 1); // Considered discretionary.
193
194 if let Some(func) = self.shaper.collect_features {
195 func(self);
196 }
197
198 self.ot_map
199 .enable_feature(Tag::from_bytes(b"Buzz"), empty, 1); // Considered required.
200 self.ot_map
201 .enable_feature(Tag::from_bytes(b"BUZZ"), empty, 1); // Considered discretionary.
202
203 for &(tag, flags) in COMMON_FEATURES {
204 self.ot_map.add_feature(tag, flags, 1);
205 }
206
207 if self.direction.is_horizontal() {
208 for &(tag, flags) in HORIZONTAL_FEATURES {
209 self.ot_map.add_feature(tag, flags, 1);
210 }
211 } else {
212 // We only apply `vert` feature. See:
213 // https://github.com/harfbuzz/harfbuzz/commit/d71c0df2d17f4590d5611239577a6cb532c26528
214 // https://lists.freedesktop.org/archives/harfbuzz/2013-August/003490.html
215
216 // We really want to find a 'vert' feature if there's any in the font, no
217 // matter which script/langsys it is listed (or not) under.
218 // See various bugs referenced from:
219 // https://github.com/harfbuzz/harfbuzz/issues/63
220 self.ot_map
221 .enable_feature(feature::VERTICAL_WRITING, FeatureFlags::GLOBAL_SEARCH, 1);
222 }
223
224 for feature in user_features {
225 let flags = if feature.is_global() {
226 FeatureFlags::GLOBAL
227 } else {
228 empty
229 };
230 self.ot_map.add_feature(feature.tag, flags, feature.value);
231 }
232
233 if self.apply_morx {
234 for feature in user_features {
235 self.aat_map
236 .add_feature(self.face, feature.tag, feature.value);
237 }
238 }
239
240 if let Some(func) = self.shaper.override_features {
241 func(self);
242 }
243 }
244
245 fn compile(mut self, user_features: &[Feature]) -> ShapePlan {
246 let ot_map = self.ot_map.compile();
247
248 let aat_map = if self.apply_morx {
249 self.aat_map.compile(self.face)
250 } else {
251 aat::Map::default()
252 };
253
254 let frac_mask = ot_map.one_mask(feature::FRACTIONS);
255 let numr_mask = ot_map.one_mask(feature::NUMERATORS);
256 let dnom_mask = ot_map.one_mask(feature::DENOMINATORS);
257 let has_frac = frac_mask != 0 || (numr_mask != 0 && dnom_mask != 0);
258
259 let rtlm_mask = ot_map.one_mask(feature::RIGHT_TO_LEFT_MIRRORED_FORMS);
260 let has_vert = ot_map.one_mask(feature::VERTICAL_WRITING) != 0;
261
262 let horizontal = self.direction.is_horizontal();
263 let kern_tag = if horizontal {
264 feature::KERNING
265 } else {
266 feature::VERTICAL_KERNING
267 };
268 let kern_mask = ot_map.mask(kern_tag).0;
269 let requested_kerning = kern_mask != 0;
270 let trak_mask = ot_map.mask(Tag::from_bytes(b"trak")).0;
271 let requested_tracking = trak_mask != 0;
272
273 let has_gpos_kern = ot_map.feature_index(TableIndex::GPOS, kern_tag).is_some();
274 let disable_gpos = self.shaper.gpos_tag.is_some()
275 && self.shaper.gpos_tag != ot_map.chosen_script(TableIndex::GPOS);
276
277 // Decide who provides glyph classes. GDEF or Unicode.
278 let fallback_glyph_classes = !self
279 .face
280 .tables()
281 .gdef
282 .map_or(false, |table| table.has_glyph_classes());
283
284 // Decide who does substitutions. GSUB, morx, or fallback.
285 let apply_morx = self.apply_morx;
286
287 let mut apply_gpos = false;
288 let mut apply_kerx = false;
289 let mut apply_kern = false;
290
291 // Decide who does positioning. GPOS, kerx, kern, or fallback.
292 let has_kerx = self.face.tables().kerx.is_some();
293 let has_gsub = self.face.tables().gsub.is_some();
294 let has_gpos = !disable_gpos && self.face.tables().gpos.is_some();
295
296 // Prefer GPOS over kerx if GSUB is present;
297 // https://github.com/harfbuzz/harfbuzz/issues/3008
298 if has_kerx && !(has_gsub && has_gpos) {
299 apply_kerx = true;
300 } else if has_gpos {
301 apply_gpos = true;
302 }
303
304 if !apply_kerx && (!has_gpos_kern || !apply_gpos) {
305 if has_kerx {
306 apply_kerx = true;
307 } else if ot::has_kerning(self.face) {
308 apply_kern = true;
309 }
310 }
311
312 let apply_fallback_kern = !(apply_gpos || apply_kerx || apply_kern);
313 let zero_marks = self.script_zero_marks
314 && !apply_kerx
315 && (!apply_kern || !ot::has_machine_kerning(self.face));
316
317 let has_gpos_mark = ot_map.one_mask(feature::MARK_POSITIONING) != 0;
318
319 let mut adjust_mark_positioning_when_zeroing =
320 !apply_gpos && !apply_kerx && (!apply_kern || !ot::has_cross_kerning(self.face));
321
322 let fallback_mark_positioning =
323 adjust_mark_positioning_when_zeroing && self.script_fallback_mark_positioning;
324
325 // If we're using morx shaping, we cancel mark position adjustment because
326 // Apple Color Emoji assumes this will NOT be done when forming emoji sequences;
327 // https://github.com/harfbuzz/harfbuzz/issues/2967.
328 if apply_morx {
329 adjust_mark_positioning_when_zeroing = false;
330 }
331
332 // Currently we always apply trak.
333 let apply_trak = requested_tracking && self.face.tables().trak.is_some();
334
335 let mut plan = ShapePlan {
336 direction: self.direction,
337 script: self.script,
338 shaper: self.shaper,
339 ot_map,
340 aat_map,
341 data: None,
342 frac_mask,
343 numr_mask,
344 dnom_mask,
345 rtlm_mask,
346 kern_mask,
347 trak_mask,
348 requested_kerning,
349 has_frac,
350 has_vert,
351 has_gpos_mark,
352 zero_marks,
353 fallback_glyph_classes,
354 fallback_mark_positioning,
355 adjust_mark_positioning_when_zeroing,
356 apply_gpos,
357 apply_kern,
358 apply_fallback_kern,
359 apply_kerx,
360 apply_morx,
361 apply_trak,
362 user_features: user_features.to_vec(),
363 };
364
365 if let Some(func) = self.shaper.create_data {
366 plan.data = Some(func(&plan));
367 }
368
369 plan
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::ShapePlan;
376
377 #[test]
378 fn test_shape_plan_is_send_and_sync() {
379 fn ensure_send_and_sync<T: Send + Sync>() {}
380 ensure_send_and_sync::<ShapePlan>();
381 }
382}
383