Skip to main content

harfrust/hb/
ot_shaper_thai.rs

1use super::buffer::*;
2use super::font_funcs::FontFuncsDispatch;
3use super::ot_layout::*;
4use super::ot_shape_normalize::HB_OT_SHAPE_NORMALIZATION_MODE_AUTO;
5use super::ot_shape_plan::hb_ot_shape_plan_t;
6use super::ot_shaper::*;
7use super::script;
8use super::unicode::GeneralCategory;
9
10pub const THAI_SHAPER: hb_ot_shaper_t = hb_ot_shaper_t {
11    collect_features: None,
12    override_features: None,
13    create_data: None,
14    preprocess_text: Some(preprocess_text),
15    postprocess_glyphs: None,
16    normalization_preference: HB_OT_SHAPE_NORMALIZATION_MODE_AUTO,
17    decompose: None,
18    compose: None,
19    setup_masks: None,
20    gpos_tag: None,
21    reorder_marks: None,
22    zero_width_marks: HB_OT_SHAPE_ZERO_WIDTH_MARKS_BY_GDEF_LATE,
23    fallback_position: false,
24};
25
26#[derive(Clone, Copy, PartialEq)]
27enum Consonant {
28    NC = 0,
29    AC,
30    RC,
31    DC,
32    NotConsonant,
33}
34
35fn get_consonant_type(u: u32) -> Consonant {
36    match u {
37        0x0E1B | 0x0E1D | 0x0E1F => Consonant::AC,
38        0x0E0D | 0x0E10 => Consonant::RC,
39        0x0E0E | 0x0E0F => Consonant::DC,
40        0x0E01..=0x0E2E => Consonant::NC,
41        _ => Consonant::NotConsonant,
42    }
43}
44
45#[derive(Clone, Copy, PartialEq)]
46enum Mark {
47    AV,
48    BV,
49    T,
50    NotMark,
51}
52
53fn get_mark_type(u: u32) -> Mark {
54    match u {
55        0x0E31 | 0x0E34..=0x0E37 | 0x0E47 | 0x0E4D..=0x0E4E => Mark::AV,
56        0x0E38..=0x0E3A => Mark::BV,
57        0x0E48..=0x0E4C => Mark::T,
58        _ => Mark::NotMark,
59    }
60}
61
62#[derive(Clone, Copy, PartialEq)]
63enum Action {
64    NOP,
65    /// Shift combining-mark down.
66    SD,
67    /// Shift combining-mark left.
68    SL,
69    /// Shift combining-mark down-left.
70    SDL,
71    /// Remove descender from base.
72    RD,
73}
74
75#[derive(Clone, Copy)]
76struct PuaMapping {
77    u: u16,
78    win_pua: u16,
79    mac_pua: u16,
80}
81
82impl PuaMapping {
83    const fn new(u: u16, win_pua: u16, mac_pua: u16) -> Self {
84        PuaMapping {
85            u,
86            win_pua,
87            mac_pua,
88        }
89    }
90}
91
92static SD_MAPPINGS: &[PuaMapping] = &[
93    PuaMapping::new(0x0E48, 0xF70A, 0xF88B), // MAI EK
94    PuaMapping::new(0x0E49, 0xF70B, 0xF88E), // MAI THO
95    PuaMapping::new(0x0E4A, 0xF70C, 0xF891), // MAI TRI
96    PuaMapping::new(0x0E4B, 0xF70D, 0xF894), // MAI CHATTAWA
97    PuaMapping::new(0x0E4C, 0xF70E, 0xF897), // THANTHAKHAT
98    PuaMapping::new(0x0E38, 0xF718, 0xF89B), // SARA U
99    PuaMapping::new(0x0E39, 0xF719, 0xF89C), // SARA UU
100    PuaMapping::new(0x0E3A, 0xF71A, 0xF89D), // PHINTHU
101    PuaMapping::new(0x0000, 0x0000, 0x0000),
102];
103
104static SDL_MAPPINGS: &[PuaMapping] = &[
105    PuaMapping::new(0x0E48, 0xF705, 0xF88C), // MAI EK
106    PuaMapping::new(0x0E49, 0xF706, 0xF88F), // MAI THO
107    PuaMapping::new(0x0E4A, 0xF707, 0xF892), // MAI TRI
108    PuaMapping::new(0x0E4B, 0xF708, 0xF895), // MAI CHATTAWA
109    PuaMapping::new(0x0E4C, 0xF709, 0xF898), // THANTHAKHAT
110    PuaMapping::new(0x0000, 0x0000, 0x0000),
111];
112
113static SL_MAPPINGS: &[PuaMapping] = &[
114    PuaMapping::new(0x0E48, 0xF713, 0xF88A), // MAI EK
115    PuaMapping::new(0x0E49, 0xF714, 0xF88D), // MAI THO
116    PuaMapping::new(0x0E4A, 0xF715, 0xF890), // MAI TRI
117    PuaMapping::new(0x0E4B, 0xF716, 0xF893), // MAI CHATTAWA
118    PuaMapping::new(0x0E4C, 0xF717, 0xF896), // THANTHAKHAT
119    PuaMapping::new(0x0E31, 0xF710, 0xF884), // MAI HAN-AKAT
120    PuaMapping::new(0x0E34, 0xF701, 0xF885), // SARA I
121    PuaMapping::new(0x0E35, 0xF702, 0xF886), // SARA II
122    PuaMapping::new(0x0E36, 0xF703, 0xF887), // SARA UE
123    PuaMapping::new(0x0E37, 0xF704, 0xF888), // SARA UEE
124    PuaMapping::new(0x0E47, 0xF712, 0xF889), // MAITAIKHU
125    PuaMapping::new(0x0E4D, 0xF711, 0xF899), // NIKHAHIT
126    PuaMapping::new(0x0000, 0x0000, 0x0000),
127];
128
129static RD_MAPPINGS: &[PuaMapping] = &[
130    PuaMapping::new(0x0E0D, 0xF70F, 0xF89A), // YO YING
131    PuaMapping::new(0x0E10, 0xF700, 0xF89E), // THO THAN
132    PuaMapping::new(0x0000, 0x0000, 0x0000),
133];
134
135fn pua_shape(u: u32, action: Action, face: &mut FontFuncsDispatch) -> u32 {
136    let mappings = match action {
137        Action::NOP => return u,
138        Action::SD => SD_MAPPINGS,
139        Action::SL => SL_MAPPINGS,
140        Action::SDL => SDL_MAPPINGS,
141        Action::RD => RD_MAPPINGS,
142    };
143
144    for m in mappings {
145        if m.u as u32 == u {
146            if face.nominal_glyph(m.win_pua as u32).is_some() {
147                return m.win_pua as u32;
148            }
149
150            if face.nominal_glyph(m.mac_pua as u32).is_some() {
151                return m.mac_pua as u32;
152            }
153
154            break;
155        }
156    }
157
158    u
159}
160
161#[derive(Clone, Copy)]
162enum AboveState {
163    // Cluster above looks like:
164    T0, //  ⣤
165    T1, //     ⣼
166    T2, //        ⣾
167    T3, //           ⣿
168}
169
170static ABOVE_START_STATE: &[AboveState] = &[
171    AboveState::T0, // NC
172    AboveState::T1, // AC
173    AboveState::T0, // RC
174    AboveState::T0, // DC
175    AboveState::T3, // NotConsonant
176];
177
178#[derive(Clone, Copy)]
179struct AboveStateMachineEdge {
180    action: Action,
181    next_state: AboveState,
182}
183
184impl AboveStateMachineEdge {
185    const fn new(action: Action, next_state: AboveState) -> Self {
186        AboveStateMachineEdge { action, next_state }
187    }
188}
189
190type ASME = AboveStateMachineEdge;
191
192static ABOVE_STATE_MACHINE: &[[ASME; 3]] = &[
193    //        AV                                      BV                                      T
194    /* T0 */
195    [
196        ASME::new(Action::NOP, AboveState::T3),
197        ASME::new(Action::NOP, AboveState::T0),
198        ASME::new(Action::SD, AboveState::T3),
199    ],
200    /* T1 */
201    [
202        ASME::new(Action::SL, AboveState::T2),
203        ASME::new(Action::NOP, AboveState::T1),
204        ASME::new(Action::SDL, AboveState::T2),
205    ],
206    /* T2 */
207    [
208        ASME::new(Action::NOP, AboveState::T3),
209        ASME::new(Action::NOP, AboveState::T2),
210        ASME::new(Action::SL, AboveState::T3),
211    ],
212    /* T3 */
213    [
214        ASME::new(Action::NOP, AboveState::T3),
215        ASME::new(Action::NOP, AboveState::T3),
216        ASME::new(Action::NOP, AboveState::T3),
217    ],
218];
219
220#[derive(Clone, Copy)]
221enum BelowState {
222    /// No descender.
223    B0,
224    /// Removable descender.
225    B1,
226    /// Strict descender.
227    B2,
228}
229
230static BELOW_START_STATE: &[BelowState] = &[
231    BelowState::B0, // NC
232    BelowState::B0, // AC
233    BelowState::B1, // RC
234    BelowState::B2, // DC
235    BelowState::B2, // NotConsonant
236];
237
238#[derive(Clone, Copy)]
239struct BelowStateMachineEdge {
240    action: Action,
241    next_state: BelowState,
242}
243
244impl BelowStateMachineEdge {
245    const fn new(action: Action, next_state: BelowState) -> Self {
246        BelowStateMachineEdge { action, next_state }
247    }
248}
249
250type BSME = BelowStateMachineEdge;
251
252static BELOW_STATE_MACHINE: &[[BSME; 3]] = &[
253    //        AV                                      BV                                      T
254    /* B0 */
255    [
256        BSME::new(Action::NOP, BelowState::B0),
257        BSME::new(Action::NOP, BelowState::B2),
258        BSME::new(Action::NOP, BelowState::B0),
259    ],
260    /* B1 */
261    [
262        BSME::new(Action::NOP, BelowState::B1),
263        BSME::new(Action::RD, BelowState::B2),
264        BSME::new(Action::NOP, BelowState::B1),
265    ],
266    /* B2 */
267    [
268        BSME::new(Action::NOP, BelowState::B2),
269        BSME::new(Action::SD, BelowState::B2),
270        BSME::new(Action::NOP, BelowState::B2),
271    ],
272];
273
274fn do_pua_shaping(face: &mut FontFuncsDispatch, buffer: &mut hb_buffer_t) {
275    let mut above_state = ABOVE_START_STATE[Consonant::NotConsonant as usize];
276    let mut below_state = BELOW_START_STATE[Consonant::NotConsonant as usize];
277    let mut base = 0;
278
279    for i in 0..buffer.len {
280        let mt = get_mark_type(buffer.info[i].glyph_id);
281
282        if mt == Mark::NotMark {
283            let ct = get_consonant_type(buffer.info[i].glyph_id);
284            above_state = ABOVE_START_STATE[ct as usize];
285            below_state = BELOW_START_STATE[ct as usize];
286            base = i;
287            continue;
288        }
289
290        let above_edge = ABOVE_STATE_MACHINE[above_state as usize][mt as usize];
291        let below_edge = BELOW_STATE_MACHINE[below_state as usize][mt as usize];
292        above_state = above_edge.next_state;
293        below_state = below_edge.next_state;
294
295        // At least one of the above/below actions is NOP.
296        let action = if above_edge.action != Action::NOP {
297            above_edge.action
298        } else {
299            below_edge.action
300        };
301
302        buffer.unsafe_to_break(Some(base), Some(i));
303        if action == Action::RD {
304            buffer.info[base].glyph_id = pua_shape(buffer.info[base].glyph_id, action, face);
305        } else {
306            buffer.info[i].glyph_id = pua_shape(buffer.info[i].glyph_id, action, face);
307        }
308    }
309}
310
311// TODO: more tests
312fn preprocess_text(
313    plan: &hb_ot_shape_plan_t,
314    face: &mut FontFuncsDispatch,
315    buffer: &mut hb_buffer_t,
316) {
317    // This function implements the shaping logic documented here:
318    //
319    //   https://linux.thai.net/~thep/th-otf/shaping.html
320    //
321    // The first shaping rule listed there is needed even if the font has Thai
322    // OpenType tables.  The rest do fallback positioning based on PUA codepoints.
323    // We implement that only if there exist no Thai GSUB in the font.
324
325    // The following is NOT specified in the MS OT Thai spec, however, it seems
326    // to be what Uniscribe and other engines implement.  According to Eric Muller:
327    //
328    // When you have a SARA AM, decompose it in NIKHAHIT + SARA AA, *and* move the
329    // NIKHAHIT backwards over any above-base marks (0E31, 0E34-0E37, 0E47-0E4E).
330    //
331    // <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32>
332    //
333    // This reordering is legit only when the NIKHAHIT comes from a SARA AM, not
334    // when it's there to start with. The string <0E14, 0E4B, 0E4D> is probably
335    // not what a user wanted, but the rendering is nevertheless nikhahit above
336    // chattawa.
337    //
338    // Same for Lao.
339    //
340    // Note:
341    //
342    // Uniscribe also does some below-marks reordering.  Namely, it positions U+0E3A
343    // after U+0E38 and U+0E39.  We do that by modifying the ccc for U+0E3A.
344    // See unicode->modified_combining_class ().  Lao does NOT have a U+0E3A
345    // equivalent.
346
347    // Here are the characters of significance:
348    //
349    //              Thai    Lao
350    // SARA AM:     U+0E33  U+0EB3
351    // SARA AA:     U+0E32  U+0EB2
352    // Nikhahit:    U+0E4D  U+0ECD
353    //
354    // Testing shows that Uniscribe reorder the following marks:
355    // Thai:	<0E31,0E34..0E37,0E47..0E4E>
356    // Lao:     <0EB1,0EB4..0EB7,0EBB,0EC8..0ECD>
357    //
358    // Note how the Lao versions are the same as Thai + 0x80.
359
360    // We only get one script at a time, so a script-agnostic implementation
361    // is adequate here.
362    #[inline]
363    fn is_sara_am(u: u32) -> bool {
364        (u & !0x0080) == 0x0E33
365    }
366    #[inline]
367    fn nikhahit_from_sara_am(u: u32) -> u32 {
368        u - 0x0E33 + 0x0E4D
369    }
370    #[inline]
371    fn sara_aa_from_sara_am(u: u32) -> u32 {
372        u - 1
373    }
374    #[inline]
375    fn is_above_base_mark(u: u32) -> bool {
376        let u = u & !0x0080;
377        matches!(u, 0x0E34..=0x0E37 | 0x0E47..=0x0E4E | 0x0E31..=0x0E31 | 0x0E3B..=0x0E3B)
378    }
379
380    buffer.clear_output();
381    buffer.idx = 0;
382    while buffer.idx < buffer.len {
383        let u = buffer.cur(0).glyph_id;
384        if !is_sara_am(u) {
385            buffer.next_glyph();
386            continue;
387        }
388
389        // Is SARA AM. Decompose and reorder.
390        buffer.output_glyph(nikhahit_from_sara_am(u));
391        {
392            let out_idx = buffer.out_len - 1;
393            let mut info = buffer.out_info_mut()[out_idx];
394            info.set_continuation(&mut buffer.scratch_flags);
395        }
396        buffer.replace_glyph(sara_aa_from_sara_am(u));
397
398        // Make Nikhahit be recognized as a ccc=0 mark when zeroing widths.
399        let end = buffer.out_len;
400
401        buffer.out_info_mut()[end - 2].set_general_category(GeneralCategory::NON_SPACING_MARK);
402
403        // Ok, let's see...
404        let mut start = end - 2;
405        while start > 0 && is_above_base_mark(buffer.out_info()[start - 1].glyph_id) {
406            start -= 1;
407        }
408
409        if start + 2 < end {
410            // Move Nikhahit (end-2) to the beginning
411            buffer.merge_out_clusters(start, end);
412            let t = buffer.out_info()[end - 2];
413            for i in 0..(end - start - 2) {
414                buffer.out_info_mut()[i + start + 1] = buffer.out_info()[i + start];
415            }
416            buffer.out_info_mut()[start] = t;
417        }
418
419        // Since we decomposed, and NIKHAHIT is combining, merge clusters with the
420        // previous cluster.
421        if start != 0 {
422            buffer.merge_out_grapheme_clusters(start - 1, end);
423        }
424    }
425
426    buffer.sync();
427
428    // If font has Thai GSUB, we are done.
429    if plan.script == Some(script::THAI) && !plan.ot_map.found_script(TableIndex::GSUB) {
430        do_pua_shaping(face, buffer);
431    }
432}