1 | use alloc::boxed::Box; |
2 | |
3 | use super::buffer::hb_buffer_t; |
4 | use super::ot_map::*; |
5 | use super::ot_shape::*; |
6 | use super::ot_shape_normalize::*; |
7 | use super::ot_shape_plan::hb_ot_shape_plan_t; |
8 | use super::ot_shaper::*; |
9 | use super::ot_shaper_indic::ot_category_t; |
10 | use super::unicode::{CharExt, GeneralCategoryExt}; |
11 | use super::{hb_font_t, hb_glyph_info_t, hb_mask_t, hb_tag_t}; |
12 | |
13 | pub const KHMER_SHAPER: hb_ot_shaper_t = hb_ot_shaper_t { |
14 | collect_features: Some(collect_features), |
15 | override_features: Some(override_features), |
16 | create_data: Some(|plan: &hb_ot_shape_plan_t| Box::new(KhmerShapePlan::new(plan))), |
17 | preprocess_text: None, |
18 | postprocess_glyphs: None, |
19 | normalization_preference: HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS_NO_SHORT_CIRCUIT, |
20 | decompose: Some(decompose), |
21 | compose: Some(compose), |
22 | setup_masks: Some(setup_masks), |
23 | gpos_tag: None, |
24 | reorder_marks: None, |
25 | zero_width_marks: HB_OT_SHAPE_ZERO_WIDTH_MARKS_NONE, |
26 | fallback_position: false, |
27 | }; |
28 | |
29 | const KHMER_FEATURES: &[(hb_tag_t, hb_ot_map_feature_flags_t)] = &[ |
30 | // Basic features. |
31 | // These features are applied all at once, before reordering, constrained |
32 | // to the syllable. |
33 | ( |
34 | hb_tag_t::from_bytes(b"pref" ), |
35 | F_MANUAL_JOINERS | F_PER_SYLLABLE, |
36 | ), |
37 | ( |
38 | hb_tag_t::from_bytes(b"blwf" ), |
39 | F_MANUAL_JOINERS | F_PER_SYLLABLE, |
40 | ), |
41 | ( |
42 | hb_tag_t::from_bytes(b"abvf" ), |
43 | F_MANUAL_JOINERS | F_PER_SYLLABLE, |
44 | ), |
45 | ( |
46 | hb_tag_t::from_bytes(b"pstf" ), |
47 | F_MANUAL_JOINERS | F_PER_SYLLABLE, |
48 | ), |
49 | ( |
50 | hb_tag_t::from_bytes(b"cfar" ), |
51 | F_MANUAL_JOINERS | F_PER_SYLLABLE, |
52 | ), |
53 | // Other features. |
54 | // These features are applied all at once after clearing syllables. |
55 | (hb_tag_t::from_bytes(b"pres" ), F_GLOBAL_MANUAL_JOINERS), |
56 | (hb_tag_t::from_bytes(b"abvs" ), F_GLOBAL_MANUAL_JOINERS), |
57 | (hb_tag_t::from_bytes(b"blws" ), F_GLOBAL_MANUAL_JOINERS), |
58 | (hb_tag_t::from_bytes(b"psts" ), F_GLOBAL_MANUAL_JOINERS), |
59 | ]; |
60 | |
61 | // Must be in the same order as the KHMER_FEATURES array. |
62 | mod khmer_feature { |
63 | pub const PREF: usize = 0; |
64 | pub const BLWF: usize = 1; |
65 | pub const ABVF: usize = 2; |
66 | pub const PSTF: usize = 3; |
67 | pub const CFAR: usize = 4; |
68 | } |
69 | |
70 | impl hb_glyph_info_t { |
71 | fn set_khmer_properties(&mut self) { |
72 | let u: u32 = self.glyph_id; |
73 | let (cat: u8, _) = crate::hb::ot_shaper_indic_table::get_categories(u); |
74 | |
75 | self.set_indic_category(cat); |
76 | } |
77 | } |
78 | |
79 | struct KhmerShapePlan { |
80 | mask_array: [hb_mask_t; KHMER_FEATURES.len()], |
81 | } |
82 | |
83 | impl KhmerShapePlan { |
84 | fn new(plan: &hb_ot_shape_plan_t) -> Self { |
85 | let mut mask_array: [u32; 9] = [0; KHMER_FEATURES.len()]; |
86 | for (i: usize, feature: &(Tag, u32)) in KHMER_FEATURES.iter().enumerate() { |
87 | mask_array[i] = if feature.1 & F_GLOBAL != 0 { |
88 | 0 |
89 | } else { |
90 | plan.ot_map.get_1_mask(feature_tag:feature.0) |
91 | } |
92 | } |
93 | |
94 | KhmerShapePlan { mask_array } |
95 | } |
96 | } |
97 | |
98 | fn collect_features(planner: &mut hb_ot_shape_planner_t) { |
99 | // Do this before any lookups have been applied. |
100 | planner.ot_map.add_gsub_pause(Some(setup_syllables)); |
101 | planner.ot_map.add_gsub_pause(Some(reorder_khmer)); |
102 | |
103 | // Testing suggests that Uniscribe does NOT pause between basic |
104 | // features. Test with KhmerUI.ttf and the following three |
105 | // sequences: |
106 | // |
107 | // U+1789,U+17BC |
108 | // U+1789,U+17D2,U+1789 |
109 | // U+1789,U+17D2,U+1789,U+17BC |
110 | // |
111 | // https://github.com/harfbuzz/harfbuzz/issues/974 |
112 | planner |
113 | .ot_map |
114 | .enable_feature(hb_tag_t::from_bytes(b"locl" ), F_PER_SYLLABLE, 1); |
115 | planner |
116 | .ot_map |
117 | .enable_feature(hb_tag_t::from_bytes(b"ccmp" ), F_PER_SYLLABLE, 1); |
118 | |
119 | for feature in KHMER_FEATURES.iter().take(5) { |
120 | planner.ot_map.add_feature(feature.0, feature.1, 1); |
121 | } |
122 | |
123 | /* https://github.com/harfbuzz/harfbuzz/issues/3531 */ |
124 | planner.ot_map.add_gsub_pause(Some(syllabic_clear_var)); // Don't need syllables anymore. |
125 | |
126 | for feature in KHMER_FEATURES.iter().skip(5) { |
127 | planner.ot_map.add_feature(feature.0, feature.1, 1); |
128 | } |
129 | } |
130 | |
131 | fn setup_syllables(_: &hb_ot_shape_plan_t, _: &hb_font_t, buffer: &mut hb_buffer_t) -> bool { |
132 | super::ot_shaper_khmer_machine::find_syllables_khmer(buffer); |
133 | |
134 | let mut start: usize = 0; |
135 | let mut end: usize = buffer.next_syllable(start:0); |
136 | while start < buffer.len { |
137 | buffer.unsafe_to_break(start:Some(start), end:Some(end)); |
138 | start = end; |
139 | end = buffer.next_syllable(start); |
140 | } |
141 | |
142 | false |
143 | } |
144 | |
145 | fn reorder_khmer(plan: &hb_ot_shape_plan_t, face: &hb_font_t, buffer: &mut hb_buffer_t) -> bool { |
146 | use super::ot_shaper_khmer_machine::SyllableType; |
147 | |
148 | let mut ret = false; |
149 | |
150 | if super::ot_shaper_syllabic::insert_dotted_circles( |
151 | face, |
152 | buffer, |
153 | SyllableType::BrokenCluster as u8, |
154 | ot_category_t::OT_DOTTEDCIRCLE, |
155 | Some(ot_category_t::OT_Repha), |
156 | None, |
157 | ) { |
158 | ret = true; |
159 | } |
160 | |
161 | let khmer_plan = plan.data::<KhmerShapePlan>(); |
162 | |
163 | let mut start = 0; |
164 | let mut end = buffer.next_syllable(0); |
165 | while start < buffer.len { |
166 | reorder_syllable_khmer(khmer_plan, start, end, buffer); |
167 | start = end; |
168 | end = buffer.next_syllable(start); |
169 | } |
170 | |
171 | ret |
172 | } |
173 | |
174 | fn reorder_syllable_khmer( |
175 | khmer_plan: &KhmerShapePlan, |
176 | start: usize, |
177 | end: usize, |
178 | buffer: &mut hb_buffer_t, |
179 | ) { |
180 | use super::ot_shaper_khmer_machine::SyllableType; |
181 | |
182 | let syllable_type: SyllableType = match buffer.info[start].syllable() & 0x0F { |
183 | 0 => SyllableType::ConsonantSyllable, |
184 | 1 => SyllableType::BrokenCluster, |
185 | 2 => SyllableType::NonKhmerCluster, |
186 | _ => unreachable!(), |
187 | }; |
188 | |
189 | match syllable_type { |
190 | SyllableType::ConsonantSyllable | SyllableType::BrokenCluster => { |
191 | reorder_consonant_syllable(khmer_plan, start, end, buffer); |
192 | } |
193 | SyllableType::NonKhmerCluster => {} |
194 | } |
195 | } |
196 | |
197 | // Rules from: |
198 | // https://docs.microsoft.com/en-us/typography/script-development/devanagari |
199 | fn reorder_consonant_syllable( |
200 | plan: &KhmerShapePlan, |
201 | start: usize, |
202 | end: usize, |
203 | buffer: &mut hb_buffer_t, |
204 | ) { |
205 | // Setup masks. |
206 | { |
207 | // Post-base |
208 | let mask = plan.mask_array[khmer_feature::BLWF] |
209 | | plan.mask_array[khmer_feature::ABVF] |
210 | | plan.mask_array[khmer_feature::PSTF]; |
211 | for info in &mut buffer.info[start + 1..end] { |
212 | info.mask |= mask; |
213 | } |
214 | } |
215 | |
216 | let mut num_coengs = 0; |
217 | for i in start + 1..end { |
218 | // When a COENG + (Cons | IndV) combination are found (and subscript count |
219 | // is less than two) the character combination is handled according to the |
220 | // subscript type of the character following the COENG. |
221 | // |
222 | // ... |
223 | // |
224 | // Subscript Type 2 - The COENG + RO characters are reordered to immediately |
225 | // before the base glyph. Then the COENG + RO characters are assigned to have |
226 | // the 'pref' OpenType feature applied to them. |
227 | if buffer.info[i].indic_category() == ot_category_t::OT_H && num_coengs <= 2 && i + 1 < end |
228 | { |
229 | num_coengs += 1; |
230 | |
231 | if buffer.info[i + 1].indic_category() == ot_category_t::OT_Ra { |
232 | for j in 0..2 { |
233 | buffer.info[i + j].mask |= plan.mask_array[khmer_feature::PREF]; |
234 | } |
235 | |
236 | // Move the Coeng,Ro sequence to the start. |
237 | buffer.merge_clusters(start, i + 2); |
238 | let t0 = buffer.info[i]; |
239 | let t1 = buffer.info[i + 1]; |
240 | for k in (0..i - start).rev() { |
241 | buffer.info[k + start + 2] = buffer.info[k + start]; |
242 | } |
243 | |
244 | buffer.info[start] = t0; |
245 | buffer.info[start + 1] = t1; |
246 | |
247 | // Mark the subsequent stuff with 'cfar'. Used in Khmer. |
248 | // Read the feature spec. |
249 | // This allows distinguishing the following cases with MS Khmer fonts: |
250 | // U+1784,U+17D2,U+179A,U+17D2,U+1782 |
251 | // U+1784,U+17D2,U+1782,U+17D2,U+179A |
252 | if plan.mask_array[khmer_feature::CFAR] != 0 { |
253 | for j in i + 2..end { |
254 | buffer.info[j].mask |= plan.mask_array[khmer_feature::CFAR]; |
255 | } |
256 | } |
257 | |
258 | num_coengs = 2; // Done. |
259 | } |
260 | } else if buffer.info[i].indic_category() == ot_category_t::OT_VPre { |
261 | // Reorder left matra piece. |
262 | |
263 | // Move to the start. |
264 | buffer.merge_clusters(start, i + 1); |
265 | let t = buffer.info[i]; |
266 | for k in (0..i - start).rev() { |
267 | buffer.info[k + start + 1] = buffer.info[k + start]; |
268 | } |
269 | buffer.info[start] = t; |
270 | } |
271 | } |
272 | } |
273 | |
274 | fn override_features(planner: &mut hb_ot_shape_planner_t) { |
275 | // Khmer spec has 'clig' as part of required shaping features: |
276 | // "Apply feature 'clig' to form ligatures that are desired for |
277 | // typographical correctness.", hence in overrides... |
278 | planner |
279 | .ot_map |
280 | .enable_feature(tag:hb_tag_t::from_bytes(b"clig" ), F_NONE, value:1); |
281 | |
282 | planner |
283 | .ot_map |
284 | .disable_feature(tag:hb_tag_t::from_bytes(b"liga" )); |
285 | } |
286 | |
287 | fn decompose(_: &hb_ot_shape_normalize_context_t, ab: char) -> Option<(char, char)> { |
288 | // Decompose split matras that don't have Unicode decompositions. |
289 | match ab { |
290 | ' \u{17BE}' | ' \u{17BF}' | ' \u{17C0}' | ' \u{17C4}' | ' \u{17C5}' => Some((' \u{17C1}' , ab)), |
291 | _ => crate::hb::unicode::decompose(ab), |
292 | } |
293 | } |
294 | |
295 | fn compose(_: &hb_ot_shape_normalize_context_t, a: char, b: char) -> Option<char> { |
296 | // Avoid recomposing split matras. |
297 | if a.general_category().is_mark() { |
298 | return None; |
299 | } |
300 | |
301 | crate::hb::unicode::compose(a, b) |
302 | } |
303 | |
304 | fn setup_masks(_: &hb_ot_shape_plan_t, _: &hb_font_t, buffer: &mut hb_buffer_t) { |
305 | // We cannot setup masks here. We save information about characters |
306 | // and setup masks later on in a pause-callback. |
307 | for info: &mut hb_glyph_info_t in buffer.info_slice_mut() { |
308 | info.set_khmer_properties(); |
309 | } |
310 | } |
311 | |