Skip to main content

harfrust/hb/
ot_shape_normalize.rs

1use crate::hb::unicode::Codepoint;
2
3use super::buffer::*;
4use super::font_funcs::FontFuncsDispatch;
5use super::hb_font_t;
6use super::ot_shape_plan::hb_ot_shape_plan_t;
7use super::ot_shaper::{ComposeFn, DecomposeFn, MAX_COMBINING_MARKS};
8use super::unicode::{hb_unicode_funcs_t, CharExt};
9use read_fonts::types::GlyphId;
10
11impl GlyphInfo {
12    declare_buffer_var!(
13        u32,
14        1,
15        0,
16        NORMALIZER_GLYPH_INDEX_VAR,
17        normalizer_glyph_index,
18        set_normalizer_glyph_index
19    );
20}
21
22pub struct hb_ot_shape_normalize_context_t<'a, 'x, 'u> {
23    pub plan: &'a hb_ot_shape_plan_t,
24    pub buffer: &'x mut hb_buffer_t,
25    pub font_funcs: &'x mut FontFuncsDispatch<'a, 'u>,
26    pub decompose: DecomposeFn,
27    pub compose: ComposeFn,
28}
29
30impl hb_ot_shape_normalize_context_t<'_, '_, '_> {
31    fn nominal_glyph(&mut self, codepoint: u32) -> Option<GlyphId> {
32        self.font_funcs.nominal_glyph(codepoint)
33    }
34
35    fn variant_glyph(&mut self, codepoint: u32, selector: u32) -> Option<GlyphId> {
36        self.font_funcs.variant_glyph(codepoint, selector)
37    }
38
39    fn set_current_glyph(&mut self) {
40        let info = self.buffer.cur_mut(0);
41        if let Some(glyph_id) = self.font_funcs.nominal_glyph(info.glyph_id) {
42            info.set_normalizer_glyph_index(u32::from(glyph_id));
43        }
44    }
45}
46
47pub type hb_ot_shape_normalization_mode_t = i32;
48pub const HB_OT_SHAPE_NORMALIZATION_MODE_NONE: i32 = 0;
49pub const HB_OT_SHAPE_NORMALIZATION_MODE_DECOMPOSED: i32 = 1;
50pub const HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS: i32 = 2; /* Never composes base-to-base */
51pub const HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS_NO_SHORT_CIRCUIT: i32 = 3; /* Always fully decomposes and then recompose back */
52pub const HB_OT_SHAPE_NORMALIZATION_MODE_AUTO: i32 = 4; /* See hb-ot-shape-normalize.cc for logic. */
53#[allow(dead_code)]
54pub const HB_OT_SHAPE_NORMALIZATION_MODE_DEFAULT: i32 = HB_OT_SHAPE_NORMALIZATION_MODE_AUTO;
55
56// HIGHLEVEL DESIGN:
57//
58// This file exports one main function: normalize().
59//
60// This function closely reflects the Unicode Normalization Algorithm,
61// yet it's different.
62//
63// Each shaper specifies whether it prefers decomposed (NFD) or composed (NFC).
64// The logic however tries to use whatever the font can support.
65//
66// In general what happens is that: each grapheme is decomposed in a chain
67// of 1:2 decompositions, marks reordered, and then recomposed if desired,
68// so far it's like Unicode Normalization.  However, the decomposition and
69// recomposition only happens if the font supports the resulting characters.
70//
71// The goals are:
72//
73//   - Try to render all canonically equivalent strings similarly.  To really
74//     achieve this we have to always do the full decomposition and then
75//     selectively recompose from there.  It's kinda too expensive though, so
76//     we skip some cases.  For example, if composed is desired, we simply
77//     don't touch 1-character clusters that are supported by the font, even
78//     though their NFC may be different.
79//
80//   - When a font has a precomposed character for a sequence but the 'ccmp'
81//     feature in the font is not adequate, use the precomposed character
82//     which typically has better mark positioning.
83//
84//   - When a font does not support a combining mark, but supports it precomposed
85//     with previous base, use that.  This needs the itemizer to have this
86//     knowledge too.  We need to provide assistance to the itemizer.
87//
88//   - When a font does not support a character but supports its canonical
89//     decomposition, well, use the decomposition.
90//
91//   - The shapers can customize the compose and decompose functions to
92//     offload some of their requirements to the normalizer.  For example, the
93//     Indic shaper may want to disallow recomposing of two matras.
94
95fn decompose_unicode(
96    _: &hb_ot_shape_normalize_context_t,
97    ab: Codepoint,
98) -> Option<(Codepoint, Codepoint)> {
99    super::unicode::decompose(ab)
100}
101
102fn compose_unicode(
103    _: &hb_ot_shape_normalize_context_t,
104    a: Codepoint,
105    b: Codepoint,
106) -> Option<Codepoint> {
107    super::unicode::compose(a, b)
108}
109
110fn output_char(buffer: &mut hb_buffer_t, unichar: u32, glyph: u32) {
111    // This is very confusing indeed.
112    buffer.cur_mut(0).set_normalizer_glyph_index(glyph);
113    buffer.output_glyph(unichar);
114    // TODO: should be _hb_glyph_info_set_unicode_props (&buffer->prev(), buffer);
115    let mut flags = buffer.scratch_flags;
116    buffer.prev_mut().init_unicode_props(&mut flags);
117    buffer.scratch_flags = flags;
118}
119
120fn next_char(buffer: &mut hb_buffer_t, glyph: u32) {
121    buffer.cur_mut(0).set_normalizer_glyph_index(glyph);
122    buffer.next_glyph();
123}
124
125fn skip_char(buffer: &mut hb_buffer_t) {
126    buffer.skip_glyph();
127}
128
129/// Returns 0 if didn't decompose, number of resulting characters otherwise.
130fn decompose(ctx: &mut hb_ot_shape_normalize_context_t, shortest: bool, ab: Codepoint) -> u32 {
131    let Some((a, b)) = (ctx.decompose)(ctx, ab) else {
132        return 0;
133    };
134
135    let a_glyph = ctx.nominal_glyph(a);
136    let b_glyph = if b != 0 {
137        match ctx.nominal_glyph(b) {
138            Some(glyph_id) => Some(glyph_id),
139            None => return 0,
140        }
141    } else {
142        None
143    };
144
145    if let Some(a_glyph) = a_glyph {
146        if shortest {
147            // Output a and b
148            output_char(ctx.buffer, a, u32::from(a_glyph));
149            if let Some(b_glyph) = b_glyph {
150                output_char(ctx.buffer, b, u32::from(b_glyph));
151                return 2;
152            }
153            return 1;
154        }
155    }
156
157    let ret = decompose(ctx, shortest, a);
158    if ret != 0 {
159        if let Some(b_glyph) = b_glyph {
160            output_char(ctx.buffer, b, u32::from(b_glyph));
161            return ret + 1;
162        }
163        return ret;
164    }
165
166    if let Some(a_glyph) = a_glyph {
167        output_char(ctx.buffer, a, u32::from(a_glyph));
168        if let Some(b_glyph) = b_glyph {
169            output_char(ctx.buffer, b, u32::from(b_glyph));
170            return 2;
171        }
172        return 1;
173    }
174
175    0
176}
177
178fn decompose_current_character(ctx: &mut hb_ot_shape_normalize_context_t, shortest: bool) {
179    let u = ctx.buffer.cur(0).as_codepoint();
180    let glyph = ctx.nominal_glyph(u);
181
182    if let Some(glyph) = glyph {
183        if shortest {
184            next_char(ctx.buffer, u32::from(glyph));
185            return;
186        }
187    }
188
189    if decompose(ctx, shortest, u) > 0 {
190        skip_char(ctx.buffer);
191        return;
192    }
193
194    if let Some(glyph) = glyph {
195        next_char(ctx.buffer, u32::from(glyph));
196        return;
197    }
198
199    if ctx.buffer.cur(0).is_unicode_space() {
200        let space_type = u.space_fallback();
201        if space_type != hb_unicode_funcs_t::NOT_SPACE {
202            let space_glyph = ctx.nominal_glyph(0x0020).or(ctx.buffer.invisible);
203
204            if let Some(space_glyph) = space_glyph {
205                ctx.buffer
206                    .cur_mut(0)
207                    .set_unicode_space_fallback_type(space_type);
208                next_char(ctx.buffer, u32::from(space_glyph));
209                ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_SPACE_FALLBACK;
210                return;
211            }
212        }
213    }
214
215    // U+2011 is the only sensible character that is a no-break version of another character
216    // and not a space.  The space ones are handled already.  Handle this lone one.
217    if u == 0x2011 {
218        if let Some(other_glyph) = ctx.nominal_glyph(0x2010) {
219            next_char(ctx.buffer, u32::from(other_glyph));
220            return;
221        }
222    }
223
224    // Insert a .notdef glyph if decomposition failed.
225    next_char(ctx.buffer, 0);
226}
227
228fn handle_variation_selector_cluster(
229    ctx: &mut hb_ot_shape_normalize_context_t,
230    end: usize,
231    _: bool,
232) {
233    // Currently if there's a variation-selector we give-up on normalization, it's just too hard.
234    while ctx.buffer.idx < end - 1 && ctx.buffer.successful {
235        if ctx.buffer.cur(1).as_codepoint().is_variation_selector() {
236            let base = ctx.buffer.cur(0).as_codepoint();
237            let selector = ctx.buffer.cur(1).as_codepoint();
238            if let Some(glyph_id) = ctx.variant_glyph(base, selector) {
239                ctx.buffer
240                    .cur_mut(0)
241                    .set_normalizer_glyph_index(u32::from(glyph_id));
242                let unicode = ctx.buffer.cur(0).glyph_id;
243                ctx.buffer.replace_glyphs(2, 1, &[unicode]);
244            } else {
245                // Just pass on the two characters separately, let GSUB do its magic.
246                ctx.set_current_glyph();
247                ctx.buffer.next_glyph();
248
249                ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_VARIATION_SELECTOR_FALLBACK;
250
251                ctx.buffer.cur_mut(0).set_variation_selector(true);
252
253                if ctx.buffer.not_found_variation_selector.is_some() {
254                    ctx.buffer.cur_mut(0).clear_default_ignorable();
255                }
256
257                ctx.set_current_glyph();
258                ctx.buffer.next_glyph();
259            }
260
261            // Skip any further variation selectors.
262            while ctx.buffer.idx < end && ctx.buffer.cur(0).as_codepoint().is_variation_selector() {
263                ctx.set_current_glyph();
264                ctx.buffer.next_glyph();
265            }
266        } else {
267            ctx.set_current_glyph();
268            ctx.buffer.next_glyph();
269        }
270    }
271
272    if ctx.buffer.idx < end {
273        ctx.set_current_glyph();
274        ctx.buffer.next_glyph();
275    }
276}
277
278fn decompose_multi_char_cluster(
279    ctx: &mut hb_ot_shape_normalize_context_t,
280    end: usize,
281    short_circuit: bool,
282) {
283    let mut i = ctx.buffer.idx;
284    while i < end && ctx.buffer.successful {
285        if ctx.buffer.info[i].as_codepoint().is_variation_selector() {
286            handle_variation_selector_cluster(ctx, end, short_circuit);
287            return;
288        }
289        i += 1;
290    }
291
292    while ctx.buffer.idx < end && ctx.buffer.successful {
293        decompose_current_character(ctx, short_circuit);
294    }
295}
296
297fn compare_combining_class(pa: &GlyphInfo, pb: &GlyphInfo) -> bool {
298    let a = pa.modified_combining_class();
299    let b = pb.modified_combining_class();
300    a > b
301}
302
303pub fn _hb_ot_shape_normalize<'a, 'x>(
304    plan: &'a hb_ot_shape_plan_t,
305    buffer: &'x mut hb_buffer_t,
306    _face: &'a hb_font_t<'a>,
307    font_funcs: &'x mut FontFuncsDispatch<'a, '_>,
308) {
309    if buffer.is_empty() {
310        return;
311    }
312
313    buffer.assert_unicode_vars();
314
315    let mut mode = plan.shaper.normalization_preference;
316    if mode == HB_OT_SHAPE_NORMALIZATION_MODE_AUTO {
317        if plan.has_gpos_mark {
318            // https://github.com/harfbuzz/harfbuzz/issues/653#issuecomment-423905920
319            // mode = Some(HB_OT_SHAPE_NORMALIZATION_MODE_DECOMPOSED);
320            mode = HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS;
321        } else {
322            mode = HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS;
323        }
324    }
325
326    let mut ctx = hb_ot_shape_normalize_context_t {
327        plan,
328        buffer,
329        font_funcs,
330        decompose: plan.shaper.decompose.unwrap_or(decompose_unicode),
331        compose: plan.shaper.compose.unwrap_or(compose_unicode),
332    };
333
334    let always_short_circuit = mode == HB_OT_SHAPE_NORMALIZATION_MODE_NONE;
335    let might_short_circuit = always_short_circuit
336        || (mode != HB_OT_SHAPE_NORMALIZATION_MODE_DECOMPOSED
337            && mode != HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS_NO_SHORT_CIRCUIT);
338
339    // We do a fairly straightforward yet custom normalization process in three
340    // separate rounds: decompose, reorder, recompose (if desired).  Currently
341    // this makes two buffer swaps.  We can make it faster by moving the last
342    // two rounds into the inner loop for the first round, but it's more readable
343    // this way.
344
345    // First round, decompose
346    let mut all_simple = true;
347    {
348        ctx.buffer.clear_output();
349        let count = ctx.buffer.len;
350        ctx.buffer.idx = 0;
351        loop {
352            let mut end = ctx.buffer.idx + 1;
353            while end < count && !ctx.buffer.info[end].is_unicode_mark() {
354                end += 1;
355            }
356
357            if end < count {
358                // Leave one base for the marks to cluster with.
359                end -= 1;
360            }
361
362            // From idx to end are simple clusters.
363            if might_short_circuit {
364                let len = end - ctx.buffer.idx;
365                let mut done = 0;
366                while done < len {
367                    let codepoint = ctx.buffer.cur(done).glyph_id;
368                    let glyph_id = match ctx.nominal_glyph(codepoint) {
369                        Some(glyph_id) => u32::from(glyph_id),
370                        None => break,
371                    };
372                    let cur = ctx.buffer.cur_mut(done);
373                    cur.set_normalizer_glyph_index(glyph_id);
374                    done += 1;
375                }
376                ctx.buffer.next_glyphs(done);
377            }
378
379            while ctx.buffer.idx < end && ctx.buffer.successful {
380                decompose_current_character(&mut ctx, might_short_circuit);
381            }
382
383            if ctx.buffer.idx == count || !ctx.buffer.successful {
384                break;
385            }
386
387            all_simple = false;
388
389            // Find all the marks now.
390            end = ctx.buffer.idx + 1;
391            while end < count && ctx.buffer.info[end].is_unicode_mark() {
392                end += 1;
393            }
394
395            // idx to end is one non-simple cluster.
396            decompose_multi_char_cluster(&mut ctx, end, always_short_circuit);
397
398            if ctx.buffer.idx >= count || !ctx.buffer.successful {
399                break;
400            }
401        }
402
403        ctx.buffer.sync();
404    }
405
406    // Second round, reorder (inplace)
407    if !all_simple {
408        let count = ctx.buffer.len;
409        let mut i = 0;
410        while i < count {
411            if ctx.buffer.info[i].modified_combining_class() == 0 {
412                i += 1;
413                continue;
414            }
415
416            let mut end = i + 1;
417            while end < count && ctx.buffer.info[end].modified_combining_class() != 0 {
418                end += 1;
419            }
420
421            // We are going to do a O(n^2).  Only do this if the sequence is short.
422            if end - i <= MAX_COMBINING_MARKS {
423                ctx.buffer.sort(i, end, compare_combining_class);
424
425                if let Some(reorder_marks) = ctx.plan.shaper.reorder_marks {
426                    reorder_marks(ctx.plan, ctx.buffer, i, end);
427                }
428            }
429
430            i = end + 1;
431        }
432    }
433    if ctx.buffer.scratch_flags & HB_BUFFER_SCRATCH_FLAG_HAS_CGJ != 0 {
434        // For all CGJ, check if it prevented any reordering at all.
435        // If it did NOT, then make it skippable.
436        // https://github.com/harfbuzz/harfbuzz/issues/554
437        for i in 1..ctx.buffer.len.saturating_sub(1) {
438            if ctx.buffer.info[i].glyph_id == 0x034F
439            /* CGJ */
440            {
441                let last = ctx.buffer.info[i - 1].modified_combining_class();
442                let next = ctx.buffer.info[i + 1].modified_combining_class();
443                if next == 0 || last <= next {
444                    ctx.buffer.info[i].unhide();
445                }
446            }
447        }
448    }
449
450    // Third round, recompose
451    if !all_simple
452        && ctx.buffer.successful
453        && (mode == HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS
454            || mode == HB_OT_SHAPE_NORMALIZATION_MODE_COMPOSED_DIACRITICS_NO_SHORT_CIRCUIT)
455    {
456        // As noted in the comment earlier, we don't try to combine
457        // ccc=0 chars with their previous Starter.
458
459        let count = ctx.buffer.len;
460        let mut starter = 0;
461        ctx.buffer.clear_output();
462        ctx.buffer.next_glyph();
463        while ctx.buffer.idx < count && ctx.buffer.successful {
464            // We don't try to compose a non-mark character with it's preceding starter.
465            // This is both an optimization to avoid trying to compose every two neighboring
466            // glyphs in most scripts AND a desired feature for Hangul.  Apparently Hangul
467            // fonts are not designed to mix-and-match pre-composed syllables and Jamo.
468            let cur = ctx.buffer.cur(0);
469            if cur.is_unicode_mark() &&
470                // If there's anything between the starter and this char, they should have CCC
471                // smaller than this character's.
472                (starter == ctx.buffer.out_len - 1
473                    || ctx.buffer.prev().modified_combining_class() < cur.modified_combining_class())
474            {
475                let a = ctx.buffer.out_info()[starter].as_codepoint();
476                let b = cur.as_codepoint();
477                if let Some(composed) = (ctx.compose)(&ctx, a, b) {
478                    if let Some(glyph_id) = ctx.nominal_glyph(composed) {
479                        // Copy to out-ctx.buffer.
480                        ctx.buffer.next_glyph();
481                        if !ctx.buffer.successful {
482                            return;
483                        }
484
485                        // Merge and remove the second composable.
486                        ctx.buffer.merge_out_clusters(starter, ctx.buffer.out_len);
487                        ctx.buffer.out_len -= 1;
488
489                        // Modify starter and carry on.
490                        let mut flags = ctx.buffer.scratch_flags;
491                        let info = &mut ctx.buffer.out_info_mut()[starter];
492                        info.glyph_id = composed;
493                        info.set_normalizer_glyph_index(u32::from(glyph_id));
494                        info.init_unicode_props(&mut flags);
495                        ctx.buffer.scratch_flags = flags;
496
497                        continue;
498                    }
499                }
500            }
501
502            // Blocked, or doesn't compose.
503            ctx.buffer.next_glyph();
504
505            if ctx.buffer.prev().modified_combining_class() == 0 {
506                starter = ctx.buffer.out_len - 1;
507            }
508        }
509
510        ctx.buffer.sync();
511    }
512}