Skip to main content

harfrust/hb/
ot_shaper_khmer.rs

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