Skip to main content

harfrust/hb/
ot_shaper_hangul.rs

1use alloc::boxed::Box;
2
3use super::buffer::*;
4use super::font_funcs::FontFuncsDispatch;
5use super::ot_map::*;
6use super::ot_shape::*;
7use super::ot_shape_normalize::HB_OT_SHAPE_NORMALIZATION_MODE_NONE;
8use super::ot_shape_plan::hb_ot_shape_plan_t;
9use super::ot_shaper::*;
10use super::*;
11use crate::hb::unicode::Codepoint;
12use crate::BufferFlags;
13
14const LJMO: u8 = 1;
15const VJMO: u8 = 2;
16const TJMO: u8 = 3;
17
18impl GlyphInfo {
19    declare_buffer_var_alias!(
20        OT_SHAPER_VAR_U8_AUXILIARY_VAR,
21        u8,
22        HANGUL_SHAPING_FEATURE_VAR,
23        hangul_shaping_feature,
24        set_hangul_shaping_feature
25    );
26}
27
28fn collect_features_hangul(planner: &mut hb_ot_shape_planner_t) {
29    planner
30        .ot_map
31        .add_feature(hb_tag_t::new(b"ljmo"), F_NONE, 1);
32    planner
33        .ot_map
34        .add_feature(hb_tag_t::new(b"vjmo"), F_NONE, 1);
35    planner
36        .ot_map
37        .add_feature(hb_tag_t::new(b"tjmo"), F_NONE, 1);
38}
39
40fn override_features_hangul(planner: &mut hb_ot_shape_planner_t) {
41    // Uniscribe does not apply 'calt' for Hangul, and certain fonts
42    // (Noto Sans CJK, Source Sans Han, etc) apply all of jamo lookups
43    // in calt, which is not desirable.
44    planner.ot_map.disable_feature(hb_tag_t::new(b"calt"));
45}
46
47struct hangul_shape_plan_t {
48    mask_array: [hb_mask_t; 4],
49}
50
51fn data_create_hangul(map: &hb_ot_map_t) -> hangul_shape_plan_t {
52    hangul_shape_plan_t {
53        mask_array: [
54            0,
55            map.get_1_mask(hb_tag_t::new(b"ljmo")),
56            map.get_1_mask(hb_tag_t::new(b"vjmo")),
57            map.get_1_mask(hb_tag_t::new(b"tjmo")),
58        ],
59    }
60}
61
62const L_BASE: u32 = 0x1100;
63const V_BASE: u32 = 0x1161;
64const T_BASE: u32 = 0x11A7;
65const L_COUNT: u32 = 19;
66const V_COUNT: u32 = 21;
67const T_COUNT: u32 = 28;
68const N_COUNT: u32 = V_COUNT * T_COUNT;
69const S_COUNT: u32 = L_COUNT * N_COUNT;
70const S_BASE: u32 = 0xAC00;
71
72fn is_combining_l(u: u32) -> bool {
73    (L_BASE..L_BASE + L_COUNT).contains(&u)
74}
75
76fn is_combining_v(u: u32) -> bool {
77    (V_BASE..V_BASE + V_COUNT).contains(&u)
78}
79
80fn is_combining_t(u: u32) -> bool {
81    (T_BASE + 1..T_BASE + T_COUNT).contains(&u)
82}
83
84fn is_combined_s(u: u32) -> bool {
85    (S_BASE..S_BASE + S_COUNT).contains(&u)
86}
87
88fn is_l(u: u32) -> bool {
89    (0x1100..=0x115F).contains(&u) || (0xA960..=0xA97C).contains(&u)
90}
91
92fn is_v(u: u32) -> bool {
93    (0x1160..=0x11A7).contains(&u) || (0xD7B0..=0xD7C6).contains(&u)
94}
95
96fn is_t(u: u32) -> bool {
97    (0x11A8..=0x11FF).contains(&u) || (0xD7CB..=0xD7FB).contains(&u)
98}
99
100fn is_hangul_tone(u: u32) -> bool {
101    (0x302E..=0x302F).contains(&u)
102}
103
104fn is_zero_width_char(face: &mut FontFuncsDispatch, c: Codepoint) -> bool {
105    if let Some(glyph) = face.nominal_glyph(c) {
106        face.advance_width(glyph) == 0
107    } else {
108        false
109    }
110}
111
112fn preprocess_text_hangul(
113    _: &hb_ot_shape_plan_t,
114    face: &mut FontFuncsDispatch,
115    buffer: &mut hb_buffer_t,
116) {
117    buffer.allocate_var(GlyphInfo::HANGUL_SHAPING_FEATURE_VAR);
118
119    // Hangul syllables come in two shapes: LV, and LVT.  Of those:
120    //
121    //   - LV can be precomposed, or decomposed.  Lets call those
122    //     <LV> and <L,V>,
123    //   - LVT can be fully precomposed, partially precomposed, or
124    //     fully decomposed.  Ie. <LVT>, <LV,T>, or <L,V,T>.
125    //
126    // The composition / decomposition is mechanical.  However, not
127    // all <L,V> sequences compose, and not all <LV,T> sequences
128    // compose.
129    //
130    // Here are the specifics:
131    //
132    //   - <L>: U+1100..115F, U+A960..A97F
133    //   - <V>: U+1160..11A7, U+D7B0..D7C7
134    //   - <T>: U+11A8..11FF, U+D7CB..D7FB
135    //
136    //   - Only the <L,V> sequences for some of the U+11xx ranges combine.
137    //   - Only <LV,T> sequences for some of the Ts in U+11xx range combine.
138    //
139    // Here is what we want to accomplish in this shaper:
140    //
141    //   - If the whole syllable can be precomposed, do that,
142    //   - Otherwise, fully decompose and apply ljmo/vjmo/tjmo features.
143    //   - If a valid syllable is followed by a Hangul tone mark, reorder the tone
144    //     mark to precede the whole syllable - unless it is a zero-width glyph, in
145    //     which case we leave it untouched, assuming it's designed to overstrike.
146    //
147    // That is, of the different possible syllables:
148    //
149    //   <L>
150    //   <L,V>
151    //   <L,V,T>
152    //   <LV>
153    //   <LVT>
154    //   <LV, T>
155    //
156    // - <L> needs no work.
157    //
158    // - <LV> and <LVT> can stay the way they are if the font supports them, otherwise we
159    //   should fully decompose them if font supports.
160    //
161    // - <L,V> and <L,V,T> we should compose if the whole thing can be composed.
162    //
163    // - <LV,T> we should compose if the whole thing can be composed, otherwise we should
164    //   decompose.
165
166    buffer.clear_output();
167    // Extent of most recently seen syllable; valid only if start < end
168    let mut start = 0;
169    let mut end = 0;
170    buffer.idx = 0;
171    while buffer.idx < buffer.len {
172        let u = buffer.cur(0).glyph_id;
173        let c = buffer.cur(0).as_codepoint();
174
175        if is_hangul_tone(u) {
176            // We could cache the width of the tone marks and the existence of dotted-circle,
177            // but the use of the Hangul tone mark characters seems to be rare enough that
178            // I didn't bother for now.
179            if start < end && end == buffer.out_len {
180                // Tone mark follows a valid syllable; move it in front, unless it's zero width.
181                buffer.unsafe_to_break_from_outbuffer(Some(start), Some(buffer.idx));
182                buffer.next_glyph();
183                if !is_zero_width_char(face, c) {
184                    buffer.merge_out_grapheme_clusters(start, end + 1);
185                    let out_info = buffer.out_info_mut();
186                    let tone = out_info[end];
187                    for i in (0..end - start).rev() {
188                        out_info[i + start + 1] = out_info[i + start];
189                    }
190                    out_info[start] = tone;
191                }
192            } else {
193                // No valid syllable as base for tone mark; try to insert dotted circle.
194                if !buffer
195                    .flags
196                    .contains(BufferFlags::DO_NOT_INSERT_DOTTED_CIRCLE)
197                    && face.has_glyph(0x25CC)
198                {
199                    let mut chars = [0; 2];
200                    if !is_zero_width_char(face, c) {
201                        chars[0] = u;
202                        chars[1] = 0x25CC;
203                    } else {
204                        chars[0] = 0x25CC;
205                        chars[1] = u;
206                    }
207
208                    buffer.replace_glyphs(1, 2, &chars);
209                } else {
210                    // No dotted circle available in the font; just leave tone mark untouched.
211                    buffer.next_glyph();
212                }
213            }
214
215            start = buffer.out_len;
216            end = buffer.out_len;
217            continue;
218        }
219
220        // Remember current position as a potential syllable start;
221        // will only be used if we set end to a later position.
222        start = buffer.out_len;
223
224        if is_l(u) && buffer.idx + 1 < buffer.len {
225            let l = u;
226            let v = buffer.cur(1).glyph_id;
227            if is_v(v) {
228                // Have <L,V> or <L,V,T>.
229                let mut t = 0;
230                let mut tindex = 0;
231                if buffer.idx + 2 < buffer.len {
232                    t = buffer.cur(2).glyph_id;
233                    if is_t(t) {
234                        // Only used if isCombiningT (t); otherwise invalid.
235                        tindex = t - T_BASE;
236                    } else {
237                        // The next character was not a trailing jamo.
238                        t = 0;
239                    }
240                }
241
242                let offset = if t != 0 { 3 } else { 2 };
243                buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + offset));
244
245                // We've got a syllable <L,V,T?>; see if it can potentially be composed.
246                if is_combining_l(l) && is_combining_v(v) && (t == 0 || is_combining_t(t)) {
247                    // Try to compose; if this succeeds, end is set to start+1.
248                    let s = S_BASE + (l - L_BASE) * N_COUNT + (v - V_BASE) * T_COUNT + tindex;
249                    if face.has_glyph(s) {
250                        let n = if t != 0 { 3 } else { 2 };
251                        buffer.replace_glyphs(n, 1, &[s]);
252                        end = start + 1;
253                        continue;
254                    }
255                }
256
257                // We didn't compose, either because it's an Old Hangul syllable without a
258                // precomposed character in Unicode, or because the font didn't support the
259                // necessary precomposed glyph.
260                // Set jamo features on the individual glyphs, and advance past them.
261                buffer.cur_mut(0).set_hangul_shaping_feature(LJMO);
262                buffer.next_glyph();
263                buffer.cur_mut(0).set_hangul_shaping_feature(VJMO);
264                buffer.next_glyph();
265                if t != 0 {
266                    buffer.cur_mut(0).set_hangul_shaping_feature(TJMO);
267                    buffer.next_glyph();
268                    end = start + 3;
269                } else {
270                    end = start + 2;
271                }
272
273                buffer.merge_out_grapheme_clusters(start, end);
274
275                continue;
276            }
277        } else if is_combined_s(u) {
278            // Have <LV>, <LVT>, or <LV,T>
279            let s = u;
280            let has_glyph = face.has_glyph(s);
281
282            let lindex = (s - S_BASE) / N_COUNT;
283            let nindex = (s - S_BASE) % N_COUNT;
284            let vindex = nindex / T_COUNT;
285            let tindex = nindex % T_COUNT;
286
287            if tindex == 0 && buffer.idx + 1 < buffer.len && is_combining_t(buffer.cur(1).glyph_id)
288            {
289                // <LV,T>, try to combine.
290                let new_tindex = buffer.cur(1).glyph_id - T_BASE;
291                let new_s = s + new_tindex;
292
293                if face.has_glyph(new_s) {
294                    buffer.replace_glyphs(2, 1, &[new_s]);
295                    end = start + 1;
296                    continue;
297                } else {
298                    // Mark unsafe between LV and T.
299                    buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + 2));
300                }
301            }
302
303            // Otherwise, decompose if font doesn't support <LV> or <LVT>,
304            // or if having non-combining <LV,T>.  Note that we already handled
305            // combining <LV,T> above.
306            if !has_glyph
307                || (tindex == 0 && buffer.idx + 1 < buffer.len && is_t(buffer.cur(1).glyph_id))
308            {
309                let decomposed = [L_BASE + lindex, V_BASE + vindex, T_BASE + tindex];
310                if face.has_glyph(decomposed[0])
311                    && face.has_glyph(decomposed[1])
312                    && (tindex == 0 || face.has_glyph(decomposed[2]))
313                {
314                    let mut s_len = if tindex != 0 { 3 } else { 2 };
315                    buffer.replace_glyphs(1, s_len, &decomposed);
316
317                    // If we decomposed an LV because of a non-combining T following,
318                    // we want to include this T in the syllable.
319                    if has_glyph && tindex == 0 {
320                        buffer.next_glyph();
321                        s_len += 1;
322                    }
323
324                    // We decomposed S: apply jamo features to the individual glyphs
325                    // that are now in `buffer.out_info`.
326                    end = start + s_len;
327
328                    buffer.out_info_mut()[start + 0].set_hangul_shaping_feature(LJMO);
329                    buffer.out_info_mut()[start + 1].set_hangul_shaping_feature(VJMO);
330                    if start + 2 < end {
331                        buffer.out_info_mut()[start + 2].set_hangul_shaping_feature(TJMO);
332                    }
333
334                    buffer.merge_out_grapheme_clusters(start, end);
335
336                    continue;
337                } else if tindex == 0 && buffer.idx + 1 > buffer.len && is_t(buffer.cur(1).glyph_id)
338                {
339                    // Mark unsafe between LV and T.
340                    buffer.unsafe_to_break(Some(buffer.idx), Some(buffer.idx + 2));
341                }
342            }
343
344            if has_glyph {
345                // We didn't decompose the S, so just advance past it.
346                end = start + 1;
347                buffer.next_glyph();
348                continue;
349            }
350        }
351
352        // Didn't find a recognizable syllable, so we leave end <= start;
353        // this will prevent tone-mark reordering happening.
354        buffer.next_glyph();
355    }
356
357    buffer.sync();
358}
359
360fn setup_masks_hangul(
361    plan: &hb_ot_shape_plan_t,
362    _: &mut FontFuncsDispatch,
363    buffer: &mut hb_buffer_t,
364) {
365    let hangul_plan = plan.data::<hangul_shape_plan_t>();
366    for info in buffer.info_slice_mut() {
367        info.mask |= hangul_plan.mask_array[info.hangul_shaping_feature() as usize];
368    }
369
370    buffer.deallocate_var(GlyphInfo::HANGUL_SHAPING_FEATURE_VAR);
371}
372
373pub const HANGUL_SHAPER: hb_ot_shaper_t = hb_ot_shaper_t {
374    collect_features: Some(collect_features_hangul),
375    override_features: Some(override_features_hangul),
376    create_data: Some(|plan| Box::new(data_create_hangul(&plan.ot_map))),
377    preprocess_text: Some(preprocess_text_hangul),
378    postprocess_glyphs: None,
379    normalization_preference: HB_OT_SHAPE_NORMALIZATION_MODE_NONE,
380    decompose: None,
381    compose: None,
382    setup_masks: Some(setup_masks_hangul),
383    gpos_tag: None,
384    reorder_marks: None,
385    zero_width_marks: HB_OT_SHAPE_ZERO_WIDTH_MARKS_NONE,
386    fallback_position: true,
387};