skrifa/outline/cff/
hint.rs

1//! CFF hinting.
2
3use read_fonts::{
4    tables::postscript::{charstring::CommandSink, dict::Blues},
5    types::Fixed,
6};
7
8// "Default values for OS/2 typoAscender/Descender.."
9// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L98>
10const ICF_TOP: Fixed = Fixed::from_i32(880);
11const ICF_BOTTOM: Fixed = Fixed::from_i32(-120);
12
13// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L141>
14const MAX_BLUES: usize = 7;
15const MAX_OTHER_BLUES: usize = 5;
16const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES;
17
18// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L47>
19const MAX_HINTS: usize = 96;
20
21// One bit per stem hint
22// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L80>
23const HINT_MASK_SIZE: usize = MAX_HINTS.div_ceil(8);
24
25// Constant for hint adjustment and em box hint placement.
26// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L114>
27const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000);
28
29// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psfixed.h#L55>
30const EPSILON: Fixed = Fixed::from_bits(1);
31
32/// Parameters used to generate the stem and counter zones for the hinting
33/// algorithm.
34#[derive(Clone)]
35pub(crate) struct HintParams {
36    pub blues: Blues,
37    pub family_blues: Blues,
38    pub other_blues: Blues,
39    pub family_other_blues: Blues,
40    pub blue_scale: Fixed,
41    pub blue_shift: Fixed,
42    pub blue_fuzz: Fixed,
43    pub language_group: i32,
44}
45
46impl Default for HintParams {
47    fn default() -> Self {
48        Self {
49            blues: Blues::default(),
50            other_blues: Blues::default(),
51            family_blues: Blues::default(),
52            family_other_blues: Blues::default(),
53            // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-16-private-dict-operators>
54            blue_scale: Fixed::from_f64(0.039625),
55            blue_shift: Fixed::from_i32(7),
56            blue_fuzz: Fixed::ONE,
57            language_group: 0,
58        }
59    }
60}
61
62/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L129>
63#[derive(Copy, Clone, PartialEq, Default, Debug)]
64struct BlueZone {
65    is_bottom: bool,
66    cs_bottom_edge: Fixed,
67    cs_top_edge: Fixed,
68    cs_flat_edge: Fixed,
69    ds_flat_edge: Fixed,
70}
71
72/// Hinting state for a PostScript subfont.
73///
74/// Note that hinter states depend on the scale, subfont index and
75/// variation coordinates of a glyph. They can be retained and reused
76/// if those values remain the same.
77#[derive(Copy, Clone, PartialEq, Default)]
78pub(crate) struct HintState {
79    scale: Fixed,
80    blue_scale: Fixed,
81    blue_shift: Fixed,
82    blue_fuzz: Fixed,
83    language_group: i32,
84    suppress_overshoot: bool,
85    do_em_box_hints: bool,
86    boost: Fixed,
87    darken_y: Fixed,
88    zones: [BlueZone; MAX_BLUE_ZONES],
89    zone_count: usize,
90}
91
92impl HintState {
93    pub fn new(params: &HintParams, scale: Fixed) -> Self {
94        let mut state = Self {
95            scale,
96            blue_scale: params.blue_scale,
97            blue_shift: params.blue_shift,
98            blue_fuzz: params.blue_fuzz,
99            language_group: params.language_group,
100            suppress_overshoot: false,
101            do_em_box_hints: false,
102            boost: Fixed::ZERO,
103            darken_y: Fixed::ZERO,
104            zones: [BlueZone::default(); MAX_BLUE_ZONES],
105            zone_count: 0,
106        };
107        state.build_zones(params);
108        state
109    }
110
111    fn zones(&self) -> &[BlueZone] {
112        &self.zones[..self.zone_count]
113    }
114
115    /// Initialize zones from the set of blues values.
116    ///
117    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L66>
118    fn build_zones(&mut self, params: &HintParams) {
119        self.do_em_box_hints = false;
120        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L141>
121        match (self.language_group, params.blues.values().len()) {
122            (1, 2) => {
123                let blues = params.blues.values();
124                if blues[0].0 < ICF_BOTTOM
125                    && blues[0].1 < ICF_BOTTOM
126                    && blues[1].0 > ICF_TOP
127                    && blues[1].1 > ICF_TOP
128                {
129                    // FreeType generates synthetic hints here. We'll do it
130                    // later when building the hint map.
131                    self.do_em_box_hints = true;
132                    return;
133                }
134            }
135            (1, 0) => {
136                self.do_em_box_hints = true;
137                return;
138            }
139            _ => {}
140        }
141        let mut zones = [BlueZone::default(); MAX_BLUE_ZONES];
142        let mut max_zone_height = Fixed::ZERO;
143        let mut zone_ix = 0usize;
144        // Copy blues and other blues to a combined array of top and bottom zones.
145        for blue in params.blues.values().iter().take(MAX_BLUES) {
146            let (bottom, top) = *blue;
147            let zone_height = top - bottom;
148            if zone_height < Fixed::ZERO {
149                // Reject zones with negative height
150                continue;
151            }
152            max_zone_height = max_zone_height.max(zone_height);
153            let zone = &mut zones[zone_ix];
154            zone.cs_bottom_edge = bottom;
155            zone.cs_top_edge = top;
156            if zone_ix == 0 {
157                // First blue value is bottom zone
158                zone.is_bottom = true;
159                zone.cs_flat_edge = top;
160            } else {
161                // Remaining blue values are top zones
162                zone.is_bottom = false;
163                // Adjust both edges of top zone upward by twice darkening amount
164                zone.cs_top_edge += twice(self.darken_y);
165                zone.cs_bottom_edge += twice(self.darken_y);
166                zone.cs_flat_edge = zone.cs_bottom_edge;
167            }
168            zone_ix += 1;
169        }
170        for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) {
171            let (bottom, top) = *blue;
172            let zone_height = top - bottom;
173            if zone_height < Fixed::ZERO {
174                // Reject zones with negative height
175                continue;
176            }
177            max_zone_height = max_zone_height.max(zone_height);
178            let zone = &mut zones[zone_ix];
179            // All "other" blues are bottom zone
180            zone.is_bottom = true;
181            zone.cs_bottom_edge = bottom;
182            zone.cs_top_edge = top;
183            zone.cs_flat_edge = top;
184            zone_ix += 1;
185        }
186        // Adjust for family blues
187        let units_per_pixel = Fixed::ONE / self.scale;
188        for zone in &mut zones[..zone_ix] {
189            let flat = zone.cs_flat_edge;
190            let mut min_diff = Fixed::MAX;
191            if zone.is_bottom {
192                // In a bottom zone, the top edge is the flat edge.
193                // Search family other blues for bottom zones. Look for the
194                // closest edge that is within the one pixel threshold.
195                for blue in params.family_other_blues.values() {
196                    let family_flat = blue.1;
197                    let diff = (flat - family_flat).abs();
198                    if diff < min_diff && diff < units_per_pixel {
199                        zone.cs_flat_edge = family_flat;
200                        min_diff = diff;
201                        if diff == Fixed::ZERO {
202                            break;
203                        }
204                    }
205                }
206                // Check the first member of family blues, which is a bottom
207                // zone
208                if !params.family_blues.values().is_empty() {
209                    let family_flat = params.family_blues.values()[0].1;
210                    let diff = (flat - family_flat).abs();
211                    if diff < min_diff && diff < units_per_pixel {
212                        zone.cs_flat_edge = family_flat;
213                    }
214                }
215            } else {
216                // In a top zone, the bottom edge is the flat edge.
217                // Search family blues for top zones, skipping the first, which
218                // is a bottom zone. Look for closest family edge that is
219                // within the one pixel threshold.
220                for blue in params.family_blues.values().iter().skip(1) {
221                    let family_flat = blue.0 + twice(self.darken_y);
222                    let diff = (flat - family_flat).abs();
223                    if diff < min_diff && diff < units_per_pixel {
224                        zone.cs_flat_edge = family_flat;
225                        min_diff = diff;
226                        if diff == Fixed::ZERO {
227                            break;
228                        }
229                    }
230                }
231            }
232        }
233        if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) {
234            // Clamp at maximum scale
235            self.blue_scale = Fixed::ONE / max_zone_height;
236        }
237        // Suppress overshoot and boost blue zones at small sizes
238        if self.scale < self.blue_scale {
239            self.suppress_overshoot = true;
240            self.boost =
241                Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale);
242            // boost must remain less than 0.5, or baseline could go negative
243            self.boost = self.boost.min(Fixed::from_bits(0x7FFF));
244        }
245        if self.darken_y != Fixed::ZERO {
246            self.boost = Fixed::ZERO;
247        }
248        // Set device space alignment for each zone; apply boost amount before
249        // rounding flat edge
250        let scale = self.scale;
251        let boost = self.boost;
252        for zone in &mut zones[..zone_ix] {
253            let boost = if zone.is_bottom { -boost } else { boost };
254            zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round();
255        }
256        self.zones = zones;
257        self.zone_count = zone_ix;
258    }
259
260    /// Check whether a hint is captured by one of the blue zones.
261    ///
262    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L465>
263    fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool {
264        // We use some wrapping arithmetic on this value below to avoid panics
265        // on overflow and match FreeType's behavior
266        // See <https://github.com/googlefonts/fontations/issues/1193>
267        let fuzz = self.blue_fuzz;
268        let mut captured = false;
269        let mut adjustment = Fixed::ZERO;
270        for zone in self.zones() {
271            if zone.is_bottom
272                && bottom_edge.is_bottom()
273                && zone.cs_bottom_edge.wrapping_sub(fuzz) <= bottom_edge.cs_coord
274                && bottom_edge.cs_coord <= zone.cs_top_edge.wrapping_add(fuzz)
275            {
276                // Bottom edge captured by bottom zone.
277                adjustment = if self.suppress_overshoot {
278                    zone.ds_flat_edge
279                } else if zone.cs_top_edge.wrapping_sub(bottom_edge.cs_coord) >= self.blue_shift {
280                    // Guarantee minimum of 1 pixel overshoot
281                    bottom_edge
282                        .ds_coord
283                        .round()
284                        .min(zone.ds_flat_edge - Fixed::ONE)
285                } else {
286                    bottom_edge.ds_coord.round()
287                };
288                adjustment -= bottom_edge.ds_coord;
289                captured = true;
290                break;
291            }
292            if !zone.is_bottom
293                && top_edge.is_top()
294                && zone.cs_bottom_edge.wrapping_sub(fuzz) <= top_edge.cs_coord
295                && top_edge.cs_coord <= zone.cs_top_edge.wrapping_add(fuzz)
296            {
297                // Top edge captured by top zone.
298                adjustment = if self.suppress_overshoot {
299                    zone.ds_flat_edge
300                } else if top_edge.cs_coord.wrapping_sub(zone.cs_bottom_edge) >= self.blue_shift {
301                    // Guarantee minimum of 1 pixel overshoot
302                    top_edge
303                        .ds_coord
304                        .round()
305                        .max(zone.ds_flat_edge + Fixed::ONE)
306                } else {
307                    top_edge.ds_coord.round()
308                };
309                adjustment -= top_edge.ds_coord;
310                captured = true;
311                break;
312            }
313        }
314        if captured {
315            // Move both edges and mark them as "locked"
316            if bottom_edge.is_valid() {
317                bottom_edge.ds_coord += adjustment;
318                bottom_edge.lock();
319            }
320            if top_edge.is_valid() {
321                top_edge.ds_coord += adjustment;
322                top_edge.lock();
323            }
324        }
325        captured
326    }
327}
328
329/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L85>
330#[derive(Copy, Clone, Default)]
331struct StemHint {
332    /// If true, device space position is valid
333    is_used: bool,
334    // Character space position
335    min: Fixed,
336    max: Fixed,
337    // Device space position after first use
338    ds_min: Fixed,
339    ds_max: Fixed,
340}
341
342// Hint flags
343const GHOST_BOTTOM: u8 = 0x1;
344const GHOST_TOP: u8 = 0x2;
345const PAIR_BOTTOM: u8 = 0x4;
346const PAIR_TOP: u8 = 0x8;
347const LOCKED: u8 = 0x10;
348const SYNTHETIC: u8 = 0x20;
349
350/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L118>
351#[derive(Copy, Clone, PartialEq, Default, Debug)]
352struct Hint {
353    flags: u8,
354    /// Index in original stem hint array (if not synthetic)
355    index: u8,
356    cs_coord: Fixed,
357    ds_coord: Fixed,
358    scale: Fixed,
359}
360
361impl Hint {
362    fn is_valid(&self) -> bool {
363        self.flags != 0
364    }
365
366    fn is_bottom(&self) -> bool {
367        self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0
368    }
369
370    fn is_top(&self) -> bool {
371        self.flags & (GHOST_TOP | PAIR_TOP) != 0
372    }
373
374    fn is_pair(&self) -> bool {
375        self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0
376    }
377
378    fn is_pair_top(&self) -> bool {
379        self.flags & PAIR_TOP != 0
380    }
381
382    fn is_locked(&self) -> bool {
383        self.flags & LOCKED != 0
384    }
385
386    fn is_synthetic(&self) -> bool {
387        self.flags & SYNTHETIC != 0
388    }
389
390    fn lock(&mut self) {
391        self.flags |= LOCKED
392    }
393
394    /// Hint initialization from an incoming stem hint.
395    ///
396    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L89>
397    fn setup(
398        &mut self,
399        stem: &StemHint,
400        index: u8,
401        origin: Fixed,
402        scale: Fixed,
403        darken_y: Fixed,
404        is_bottom: bool,
405    ) {
406        // "Ghost hints" are used to align a single edge rather than a
407        // stem-- think the top and bottom edges of an uppercase
408        // sans-serif I.
409        // These are encoded internally with stem hints of width -21
410        // and -20 for bottom and top hints, respectively.
411        const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21);
412        const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20);
413        let width = stem.max - stem.min;
414        if width == GHOST_BOTTOM_WIDTH {
415            if is_bottom {
416                self.cs_coord = stem.max;
417                self.flags = GHOST_BOTTOM;
418            } else {
419                self.flags = 0;
420            }
421        } else if width == GHOST_TOP_WIDTH {
422            if !is_bottom {
423                self.cs_coord = stem.min;
424                self.flags = GHOST_TOP;
425            } else {
426                self.flags = 0;
427            }
428        } else if width < Fixed::ZERO {
429            // If width < 0, this is an inverted pair. We follow FreeType and
430            // swap the coordinates
431            if is_bottom {
432                self.cs_coord = stem.max;
433                self.flags = PAIR_BOTTOM;
434            } else {
435                self.cs_coord = stem.min;
436                self.flags = PAIR_TOP;
437            }
438        } else {
439            // This is a normal pair
440            if is_bottom {
441                self.cs_coord = stem.min;
442                self.flags = PAIR_BOTTOM;
443            } else {
444                self.cs_coord = stem.max;
445                self.flags = PAIR_TOP;
446            }
447        }
448        if self.is_top() {
449            // For top hints, adjust character space position up by twice the
450            // darkening amount
451            self.cs_coord += twice(darken_y);
452        }
453        self.cs_coord += origin;
454        self.scale = scale;
455        self.index = index;
456        // If original stem hint was used, copy the position
457        if self.flags != 0 && stem.is_used {
458            if self.is_top() {
459                self.ds_coord = stem.ds_max;
460            } else {
461                self.ds_coord = stem.ds_min;
462            }
463            self.lock();
464        } else {
465            self.ds_coord = self.cs_coord * scale;
466        }
467    }
468}
469
470/// Collection of adjusted hint edges.
471///
472/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L126>
473#[derive(Copy, Clone)]
474struct HintMap {
475    edges: [Hint; MAX_HINTS],
476    len: usize,
477    is_valid: bool,
478    scale: Fixed,
479}
480
481impl HintMap {
482    fn new(scale: Fixed) -> Self {
483        Self {
484            edges: [Hint::default(); MAX_HINTS],
485            len: 0,
486            is_valid: false,
487            scale,
488        }
489    }
490
491    fn clear(&mut self) {
492        self.len = 0;
493        self.is_valid = false;
494    }
495
496    /// Transform character space coordinate to device space.
497    ///
498    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L331>
499    fn transform(&self, coord: Fixed) -> Fixed {
500        if self.len == 0 {
501            return coord * self.scale;
502        }
503        let limit = self.len - 1;
504        let mut i = 0;
505        while i < limit && coord >= self.edges[i + 1].cs_coord {
506            i += 1;
507        }
508        while i > 0 && coord < self.edges[i].cs_coord {
509            i -= 1;
510        }
511        let first_edge = &self.edges[0];
512        if i == 0 && coord < first_edge.cs_coord {
513            // Special case for points below first edge: use uniform scale
514            ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord
515        } else {
516            // Use highest edge where cs_coord >= edge.cs_coord
517            let edge = &self.edges[i];
518            ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord
519        }
520    }
521
522    /// Insert hint edges into map, sorted by character space coordinate.
523    ///
524    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L606>
525    fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) {
526        let (is_pair, mut first_edge) = if !bottom.is_valid() {
527            // Bottom is invalid: insert only top edge
528            (false, *top)
529        } else if !top.is_valid() {
530            // Top is invalid: insert only bottom edge
531            (false, *bottom)
532        } else {
533            // We have a valid pair!
534            (true, *bottom)
535        };
536        let mut second_edge = *top;
537        if is_pair && top.cs_coord < bottom.cs_coord {
538            // Paired edges must be in proper order. FT just ignores the hint.
539            return;
540        }
541        let edge_count = if is_pair { 2 } else { 1 };
542        if self.len + edge_count > MAX_HINTS {
543            // Won't fit. Again, ignore.
544            return;
545        }
546        // Find insertion index that keeps the edge list sorted
547        let mut insert_ix = 0;
548        while insert_ix < self.len {
549            if self.edges[insert_ix].cs_coord >= first_edge.cs_coord {
550                break;
551            }
552            insert_ix += 1;
553        }
554        // Discard hints that overlap in character space
555        if insert_ix < self.len {
556            let current = &self.edges[insert_ix];
557            // Existing edge is the same
558            if (current.cs_coord == first_edge.cs_coord)
559                // Pair straddles the next edge
560                || (is_pair && current.cs_coord <= second_edge.cs_coord)
561                // Inserting between paired edges 
562                || current.is_pair_top()
563            {
564                return;
565            }
566        }
567        // Recompute device space locations using initial hint map
568        if !first_edge.is_locked() {
569            if let Some(initial) = initial {
570                if is_pair {
571                    // Preserve stem width: position center of stem with
572                    // initial hint map and two edges with nominal scale
573                    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/psaux/pshints.c#L693>
574                    let mid =
575                        initial.transform(midpoint(first_edge.cs_coord, second_edge.cs_coord));
576                    let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale;
577                    first_edge.ds_coord = mid - half_width;
578                    second_edge.ds_coord = mid + half_width;
579                } else {
580                    first_edge.ds_coord = initial.transform(first_edge.cs_coord);
581                }
582            }
583        }
584        // Now discard hints that overlap in device space:
585        if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord {
586            // Inserting after an existing edge
587            return;
588        }
589        if insert_ix < self.len
590            && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord)
591                || first_edge.ds_coord > self.edges[insert_ix].ds_coord)
592        {
593            // Inserting before an existing edge
594            return;
595        }
596        // If we're inserting in the middle, make room in the edge array
597        if insert_ix != self.len {
598            let mut src_index = self.len - 1;
599            let mut dst_index = self.len + edge_count - 1;
600            loop {
601                self.edges[dst_index] = self.edges[src_index];
602                if src_index == insert_ix {
603                    break;
604                }
605                src_index -= 1;
606                dst_index -= 1;
607            }
608        }
609        self.edges[insert_ix] = first_edge;
610        if is_pair {
611            self.edges[insert_ix + 1] = second_edge;
612        }
613        self.len += edge_count;
614    }
615
616    /// Adjust hint pairs so that one of the two edges is on a pixel boundary.
617    ///
618    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L396>
619    fn adjust(&mut self) {
620        let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS];
621        let mut saved_count = 0usize;
622        let mut i = 0;
623        // From FT with adjustments for variable names:
624        // "First pass is bottom-up (font hint order) without look-ahead.
625        // Locked edges are already adjusted.
626        // Unlocked edges begin with ds_coord from `initial_map'.
627        // Save edges that are not optimally adjusted in `saved' array,
628        // and process them in second pass."
629        let limit = self.len;
630        while i < limit {
631            let is_pair = self.edges[i].is_pair();
632            let j = if is_pair { i + 1 } else { i };
633            if !self.edges[i].is_locked() {
634                // We can adjust hint edges that are not locked
635                let frac_down = self.edges[i].ds_coord.fract();
636                let frac_up = self.edges[j].ds_coord.fract();
637                // There are four possibilities. We compute them all.
638                // (moves down are negative)
639                let down_move_down = Fixed::ZERO - frac_down;
640                let up_move_down = Fixed::ZERO - frac_up;
641                let down_move_up = if frac_down == Fixed::ZERO {
642                    Fixed::ZERO
643                } else {
644                    Fixed::ONE - frac_down
645                };
646                let up_move_up = if frac_up == Fixed::ZERO {
647                    Fixed::ZERO
648                } else {
649                    Fixed::ONE - frac_up
650                };
651                // Smallest move up
652                let move_up = down_move_up.min(up_move_up);
653                // Smallest move down
654                let move_down = down_move_down.max(up_move_down);
655                let mut save_edge = false;
656                let adjustment;
657                // Check for room to move up:
658                // 1. We're at the top of the array, or
659                // 2. The next edge is at or above the proposed move up
660                if j >= self.len - 1
661                    || self.edges[j + 1].ds_coord
662                        >= (self.edges[j].ds_coord + move_up + MIN_COUNTER)
663                {
664                    // Also check for room to move down...
665                    if i == 0
666                        || self.edges[i - 1].ds_coord
667                            <= (self.edges[i].ds_coord + move_down - MIN_COUNTER)
668                    {
669                        // .. and move the smallest distance
670                        adjustment = if -move_down < move_up {
671                            move_down
672                        } else {
673                            move_up
674                        };
675                    } else {
676                        adjustment = move_up;
677                    }
678                } else if i == 0
679                    || self.edges[i - 1].ds_coord
680                        <= (self.edges[i].ds_coord + move_down - MIN_COUNTER)
681                {
682                    // We can move down
683                    adjustment = move_down;
684                    // True if the move is not optimum
685                    save_edge = move_up < -move_down;
686                } else {
687                    // We can't move either way without overlapping
688                    adjustment = Fixed::ZERO;
689                    save_edge = true;
690                }
691                // Capture non-optimal adjustments and save them for a second
692                // pass. This is only possible if the edge above is unlocked
693                // and can be moved.
694                if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() {
695                    // (index, desired adjustment)
696                    saved[saved_count] = (j, move_up - adjustment);
697                    saved_count += 1;
698                }
699                // Apply the adjustment
700                self.edges[i].ds_coord += adjustment;
701                if is_pair {
702                    self.edges[j].ds_coord += adjustment;
703                }
704            }
705            // Compute the new edge scale
706            if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord {
707                let a = self.edges[i];
708                let b = self.edges[i - 1];
709                self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord);
710            }
711            if is_pair {
712                if self.edges[j].cs_coord != self.edges[j - 1].cs_coord {
713                    let a = self.edges[j];
714                    let b = self.edges[j - 1];
715                    self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord);
716                }
717                i += 1;
718            }
719            i += 1;
720        }
721        // Second pass tries to move non-optimal edges up if the first
722        // pass created room
723        for (j, adjustment) in saved[..saved_count].iter().copied().rev() {
724            if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) {
725                self.edges[j].ds_coord += adjustment;
726                if self.edges[j].is_pair() {
727                    self.edges[j - 1].ds_coord += adjustment;
728                }
729            }
730        }
731    }
732
733    /// Builds a hintmap from hints and mask.
734    ///
735    /// If `initial_map` is invalid, this recurses one level to initialize
736    /// it. If `is_initial` is true, simply build the initial map.
737    ///
738    /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L814>
739    fn build(
740        &mut self,
741        state: &HintState,
742        mask: Option<HintMask>,
743        mut initial_map: Option<&mut HintMap>,
744        stems: &mut [StemHint],
745        origin: Fixed,
746        is_initial: bool,
747    ) {
748        let scale = state.scale;
749        let darken_y = Fixed::ZERO;
750        if !is_initial {
751            if let Some(initial_map) = &mut initial_map {
752                if !initial_map.is_valid {
753                    // Note: recursive call here to build the initial map if it
754                    // is provided and invalid
755                    initial_map.build(state, Some(HintMask::all()), None, stems, origin, true);
756                }
757            }
758        }
759        let initial_map = initial_map.map(|x| x as &HintMap);
760        self.clear();
761        // If the mask is missing or invalid, assume all hints are active
762        let mut mask = mask.unwrap_or_else(HintMask::all);
763        if !mask.is_valid {
764            mask = HintMask::all();
765        }
766        if state.do_em_box_hints {
767            // FreeType generates these during blues initialization. Do
768            // it here just to avoid carrying the extra state in the
769            // already large HintState struct.
770            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L160>
771            let mut bottom = Hint::default();
772            bottom.cs_coord = ICF_BOTTOM - EPSILON;
773            bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER;
774            bottom.scale = scale;
775            bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC;
776            let mut top = Hint::default();
777            top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y);
778            top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER;
779            top.scale = scale;
780            top.flags = GHOST_TOP | LOCKED | SYNTHETIC;
781            let invalid = Hint::default();
782            self.insert(&bottom, &invalid, initial_map);
783            self.insert(&invalid, &top, initial_map);
784        }
785        let mut tmp_mask = mask;
786        // FreeType iterates over the hint mask with some fancy bit logic. We
787        // do the simpler thing and loop over the stems.
788        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L897>
789        for (i, stem) in stems.iter().enumerate() {
790            if !tmp_mask.get(i) {
791                continue;
792            }
793            let hint_ix = i as u8;
794            let mut bottom = Hint::default();
795            let mut top = Hint::default();
796            bottom.setup(stem, hint_ix, origin, scale, darken_y, true);
797            top.setup(stem, hint_ix, origin, scale, darken_y, false);
798            // Insert hints that are locked or captured by a blue zone
799            if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) {
800                if is_initial {
801                    self.insert(&bottom, &top, None);
802                } else {
803                    self.insert(&bottom, &top, initial_map);
804                }
805                // Avoid processing this hint in the second pass
806                tmp_mask.clear(i);
807            }
808        }
809        if is_initial {
810            // Heuristic: insert a point at (0, 0) if it's not covered by a
811            // mapping. Ensures a lock at baseline for glyphs missing a
812            // baseline hint.
813            if self.len == 0
814                || self.edges[0].cs_coord > Fixed::ZERO
815                || self.edges[self.len - 1].cs_coord < Fixed::ZERO
816            {
817                let edge = Hint {
818                    flags: GHOST_BOTTOM | LOCKED | SYNTHETIC,
819                    scale,
820                    ..Default::default()
821                };
822                let invalid = Hint::default();
823                self.insert(&edge, &invalid, None);
824            }
825        } else {
826            // Insert hints that were skipped in the first pass
827            for (i, stem) in stems.iter().enumerate() {
828                if !tmp_mask.get(i) {
829                    continue;
830                }
831                let hint_ix = i as u8;
832                let mut bottom = Hint::default();
833                let mut top = Hint::default();
834                bottom.setup(stem, hint_ix, origin, scale, darken_y, true);
835                top.setup(stem, hint_ix, origin, scale, darken_y, false);
836                self.insert(&bottom, &top, initial_map);
837            }
838        }
839        // Adjust edges that are not locked to blue zones
840        self.adjust();
841        if !is_initial {
842            // Save position of edges that were used by the hint map.
843            for edge in &self.edges[..self.len] {
844                if edge.is_synthetic() {
845                    continue;
846                }
847                let stem = &mut stems[edge.index as usize];
848                if edge.is_top() {
849                    stem.ds_max = edge.ds_coord;
850                } else {
851                    stem.ds_min = edge.ds_coord;
852                }
853                stem.is_used = true;
854            }
855        }
856        self.is_valid = true;
857    }
858}
859
860/// Bitmask that specifies which hints are currently active.
861///
862/// "Each bit of the mask, starting with the most-significant bit of
863/// the first byte, represents the corresponding hint zone in the
864/// order in which the hints were declared at the beginning of
865/// the charstring."
866///
867/// See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=24>
868/// Also <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L70>
869#[derive(Copy, Clone, PartialEq, Default)]
870struct HintMask {
871    mask: [u8; HINT_MASK_SIZE],
872    is_valid: bool,
873}
874
875impl HintMask {
876    fn new(bytes: &[u8]) -> Option<Self> {
877        let len = bytes.len();
878        if len > HINT_MASK_SIZE {
879            return None;
880        }
881        let mut mask = Self::default();
882        mask.mask[..len].copy_from_slice(&bytes[..len]);
883        mask.is_valid = true;
884        Some(mask)
885    }
886
887    fn all() -> Self {
888        Self {
889            mask: [0xFF; HINT_MASK_SIZE],
890            is_valid: true,
891        }
892    }
893
894    fn clear(&mut self, bit: usize) {
895        self.mask[bit >> 3] &= !msb_mask(bit);
896    }
897
898    fn get(&self, bit: usize) -> bool {
899        self.mask[bit >> 3] & msb_mask(bit) != 0
900    }
901}
902
903/// Returns a bit mask for the selected bit with the
904/// most significant bit at index 0.
905fn msb_mask(bit: usize) -> u8 {
906    1 << (7 - (bit & 0x7))
907}
908
909pub(super) struct HintingSink<'a, S> {
910    state: &'a HintState,
911    sink: &'a mut S,
912    stem_hints: [StemHint; MAX_HINTS],
913    stem_count: u8,
914    mask: HintMask,
915    initial_map: HintMap,
916    map: HintMap,
917    /// Most recent move_to in character space.
918    start_point: Option<[Fixed; 2]>,
919    /// Most recent line_to. First two elements are coords in character
920    /// space and the last two are in device space.
921    pending_line: Option<[Fixed; 4]>,
922}
923
924impl<'a, S: CommandSink> HintingSink<'a, S> {
925    pub fn new(state: &'a HintState, sink: &'a mut S) -> Self {
926        let scale = state.scale;
927        Self {
928            state,
929            sink,
930            stem_hints: [StemHint::default(); MAX_HINTS],
931            stem_count: 0,
932            mask: HintMask::all(),
933            initial_map: HintMap::new(scale),
934            map: HintMap::new(scale),
935            start_point: None,
936            pending_line: None,
937        }
938    }
939
940    pub fn finish(&mut self) {
941        self.maybe_close_subpath();
942    }
943
944    fn maybe_close_subpath(&mut self) {
945        // This requires some explanation. The hint mask can be modified
946        // during charstring evaluation which changes the set of hints that
947        // are applied. FreeType ensures that the closing line for any subpath
948        // is transformed with the same hint map as the starting point for the
949        // subpath. This is done by stashing a copy of the hint map that is
950        // active when a new subpath is started. Unlike FreeType, we make use
951        // of close elements, so we can cheat a bit here and avoid the
952        // extra hintmap. If we're closing an open subpath and have a pending
953        // line and the line is not equal to the start point in character
954        // space, then we emit the saved device space coordinates for the
955        // line. If the coordinates do match in character space, we omit
956        // that line. The unconditional close command ensures that the
957        // start and end points coincide.
958        // Note: this doesn't apply to subpaths that end in cubics.
959        match (self.start_point.take(), self.pending_line.take()) {
960            (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => {
961                if start != [cs_x, cs_y] {
962                    self.sink.line_to(ds_x, ds_y);
963                }
964                self.sink.close();
965            }
966            (Some(_), _) => self.sink.close(),
967            _ => {}
968        }
969    }
970
971    fn flush_pending_line(&mut self) {
972        if let Some([_, _, x, y]) = self.pending_line.take() {
973            self.sink.line_to(x, y);
974        }
975    }
976
977    fn hint(&mut self, coord: Fixed) -> Fixed {
978        if !self.map.is_valid {
979            self.build_hint_map(Some(self.mask), Fixed::ZERO);
980        }
981        trunc(self.map.transform(coord))
982    }
983
984    fn scale(&self, coord: Fixed) -> Fixed {
985        trunc(coord * self.state.scale)
986    }
987
988    fn add_stem(&mut self, min: Fixed, max: Fixed) {
989        let index = self.stem_count as usize;
990        if index >= MAX_HINTS || self.map.is_valid {
991            return;
992        }
993        let stem = &mut self.stem_hints[index];
994        stem.min = min;
995        stem.max = max;
996        stem.is_used = false;
997        stem.ds_min = Fixed::ZERO;
998        stem.ds_max = Fixed::ZERO;
999        self.stem_count = index as u8 + 1;
1000    }
1001
1002    fn build_hint_map(&mut self, mask: Option<HintMask>, origin: Fixed) {
1003        self.map.build(
1004            self.state,
1005            mask,
1006            Some(&mut self.initial_map),
1007            &mut self.stem_hints[..self.stem_count as usize],
1008            origin,
1009            false,
1010        );
1011    }
1012}
1013
1014impl<S: CommandSink> CommandSink for HintingSink<'_, S> {
1015    fn hstem(&mut self, min: Fixed, max: Fixed) {
1016        self.add_stem(min, max);
1017    }
1018
1019    fn hint_mask(&mut self, mask: &[u8]) {
1020        // For invalid hint masks, FreeType assumes all hints are active.
1021        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L844>
1022        let mask = HintMask::new(mask).unwrap_or_else(HintMask::all);
1023        if mask != self.mask {
1024            self.mask = mask;
1025            self.map.is_valid = false;
1026        }
1027    }
1028
1029    fn counter_mask(&mut self, mask: &[u8]) {
1030        // For counter masks, we build a temporary hint map "just to
1031        // place and lock those stems participating in the counter
1032        // mask." Building the map modifies the stem hint array as a
1033        // side effect.
1034        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2617>
1035        let mask = HintMask::new(mask).unwrap_or_else(HintMask::all);
1036        let mut map = HintMap::new(self.state.scale);
1037        map.build(
1038            self.state,
1039            Some(mask),
1040            Some(&mut self.initial_map),
1041            &mut self.stem_hints[..self.stem_count as usize],
1042            Fixed::ZERO,
1043            false,
1044        );
1045    }
1046
1047    fn move_to(&mut self, x: Fixed, y: Fixed) {
1048        self.maybe_close_subpath();
1049        self.start_point = Some([x, y]);
1050        let x = self.scale(x);
1051        let y = self.hint(y);
1052        self.sink.move_to(x, y);
1053    }
1054
1055    fn line_to(&mut self, x: Fixed, y: Fixed) {
1056        self.flush_pending_line();
1057        let ds_x = self.scale(x);
1058        let ds_y = self.hint(y);
1059        self.pending_line = Some([x, y, ds_x, ds_y]);
1060    }
1061
1062    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
1063        self.flush_pending_line();
1064        let cx1 = self.scale(cx1);
1065        let cy1 = self.hint(cy1);
1066        let cx2 = self.scale(cx2);
1067        let cy2 = self.hint(cy2);
1068        let x = self.scale(x);
1069        let y = self.hint(y);
1070        self.sink.curve_to(cx1, cy1, cx2, cy2, x, y);
1071    }
1072
1073    fn close(&mut self) {
1074        // We emit close commands based on the sequence of moves.
1075        // See `maybe_close_subpath`
1076    }
1077}
1078
1079/// FreeType converts from 16.16 to 26.6 by truncation. We keep our
1080/// values in 16.16 so simply zero the low 10 bits to match the
1081/// precision when converting to f32.
1082fn trunc(value: Fixed) -> Fixed {
1083    Fixed::from_bits(value.to_bits() & !0x3FF)
1084}
1085
1086fn half(value: Fixed) -> Fixed {
1087    Fixed::from_bits(value.to_bits() / 2)
1088}
1089
1090fn twice(value: Fixed) -> Fixed {
1091    Fixed::from_bits(value.to_bits().wrapping_mul(2))
1092}
1093
1094/// Computes midpoint between `a` and `b`, avoiding overflow if the sum
1095/// of the high 16 bits exceeds `i16::MAX`.
1096fn midpoint(a: Fixed, b: Fixed) -> Fixed {
1097    a + half(b - a)
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef};
1103
1104    use super::{
1105        BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink,
1106        StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP,
1107    };
1108
1109    fn make_hint_state() -> HintState {
1110        fn make_blues(values: &[f64]) -> Blues {
1111            Blues::new(values.iter().copied().map(Fixed::from_f64))
1112        }
1113        // <BlueValues value="-15 0 536 547 571 582 714 726 760 772"/>
1114        // <OtherBlues value="-255 -240"/>
1115        // <BlueScale value="0.05"/>
1116        // <BlueShift value="7"/>
1117        // <BlueFuzz value="0"/>
1118        let params = HintParams {
1119            blues: make_blues(&[
1120                -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0,
1121            ]),
1122            other_blues: make_blues(&[-255.0, -240.0]),
1123            blue_scale: Fixed::from_f64(0.05),
1124            blue_shift: Fixed::from_i32(7),
1125            blue_fuzz: Fixed::ZERO,
1126            ..Default::default()
1127        };
1128        HintState::new(&params, Fixed::ONE / Fixed::from_i32(64))
1129    }
1130
1131    #[test]
1132    fn scaled_blue_zones() {
1133        let state = make_hint_state();
1134        assert!(!state.do_em_box_hints);
1135        assert_eq!(state.zone_count, 6);
1136        assert_eq!(state.boost, Fixed::from_bits(27035));
1137        assert!(state.suppress_overshoot);
1138        // FreeType generates the following zones:
1139        let expected_zones = &[
1140            // csBottomEdge	-983040	int
1141            // csTopEdge	0	int
1142            // csFlatEdge	0	int
1143            // dsFlatEdge	0	int
1144            // bottomZone	1 '\x1'	unsigned char
1145            BlueZone {
1146                cs_bottom_edge: Fixed::from_bits(-983040),
1147                is_bottom: true,
1148                ..Default::default()
1149            },
1150            // csBottomEdge	35127296	int
1151            // csTopEdge	35848192	int
1152            // csFlatEdge	35127296	int
1153            // dsFlatEdge	589824	int
1154            // bottomZone	0 '\0'	unsigned char
1155            BlueZone {
1156                cs_bottom_edge: Fixed::from_bits(35127296),
1157                cs_top_edge: Fixed::from_bits(35848192),
1158                cs_flat_edge: Fixed::from_bits(35127296),
1159                ds_flat_edge: Fixed::from_bits(589824),
1160                is_bottom: false,
1161            },
1162            // csBottomEdge	37421056	int
1163            // csTopEdge	38141952	int
1164            // csFlatEdge	37421056	int
1165            // dsFlatEdge	589824	int
1166            // bottomZone	0 '\0'	unsigned char
1167            BlueZone {
1168                cs_bottom_edge: Fixed::from_bits(37421056),
1169                cs_top_edge: Fixed::from_bits(38141952),
1170                cs_flat_edge: Fixed::from_bits(37421056),
1171                ds_flat_edge: Fixed::from_bits(589824),
1172                is_bottom: false,
1173            },
1174            // csBottomEdge	46792704	int
1175            // csTopEdge	47579136	int
1176            // csFlatEdge	46792704	int
1177            // dsFlatEdge	786432	int
1178            // bottomZone	0 '\0'	unsigned char
1179            BlueZone {
1180                cs_bottom_edge: Fixed::from_bits(46792704),
1181                cs_top_edge: Fixed::from_bits(47579136),
1182                cs_flat_edge: Fixed::from_bits(46792704),
1183                ds_flat_edge: Fixed::from_bits(786432),
1184                is_bottom: false,
1185            },
1186            // csBottomEdge	49807360	int
1187            // csTopEdge	50593792	int
1188            // csFlatEdge	49807360	int
1189            // dsFlatEdge	786432	int
1190            // bottomZone	0 '\0'	unsigned char
1191            BlueZone {
1192                cs_bottom_edge: Fixed::from_bits(49807360),
1193                cs_top_edge: Fixed::from_bits(50593792),
1194                cs_flat_edge: Fixed::from_bits(49807360),
1195                ds_flat_edge: Fixed::from_bits(786432),
1196                is_bottom: false,
1197            },
1198            // csBottomEdge	-16711680	int
1199            // csTopEdge	-15728640	int
1200            // csFlatEdge	-15728640	int
1201            // dsFlatEdge	-262144	int
1202            // bottomZone	1 '\x1'	unsigned char
1203            BlueZone {
1204                cs_bottom_edge: Fixed::from_bits(-16711680),
1205                cs_top_edge: Fixed::from_bits(-15728640),
1206                cs_flat_edge: Fixed::from_bits(-15728640),
1207                ds_flat_edge: Fixed::from_bits(-262144),
1208                is_bottom: true,
1209            },
1210        ];
1211        assert_eq!(state.zones(), expected_zones);
1212    }
1213
1214    #[test]
1215    fn blue_zone_capture() {
1216        let state = make_hint_state();
1217        let bottom_edge = Hint {
1218            flags: PAIR_BOTTOM,
1219            ds_coord: Fixed::from_f64(2.3),
1220            ..Default::default()
1221        };
1222        let top_edge = Hint {
1223            flags: PAIR_TOP,
1224            // This value chosen to fit within the first "top" blue zone
1225            cs_coord: Fixed::from_bits(35127297),
1226            ds_coord: Fixed::from_f64(2.3),
1227            ..Default::default()
1228        };
1229        // Capture both
1230        {
1231            let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge);
1232            assert!(state.capture(&mut bottom_edge, &mut top_edge));
1233            assert!(bottom_edge.is_locked());
1234            assert!(top_edge.is_locked());
1235        }
1236        // Capture none
1237        {
1238            // Used to guarantee the edges are below all blue zones and will
1239            // not be captured
1240            let min_cs_coord = Fixed::MIN;
1241            let mut bottom_edge = Hint {
1242                cs_coord: min_cs_coord,
1243                ..bottom_edge
1244            };
1245            let mut top_edge = Hint {
1246                cs_coord: min_cs_coord,
1247                ..top_edge
1248            };
1249            assert!(!state.capture(&mut bottom_edge, &mut top_edge));
1250            assert!(!bottom_edge.is_locked());
1251            assert!(!top_edge.is_locked());
1252        }
1253        // Capture bottom, ignore invalid top
1254        {
1255            let mut bottom_edge = bottom_edge;
1256            let mut top_edge = Hint {
1257                // Empty flags == invalid hint
1258                flags: 0,
1259                ..top_edge
1260            };
1261            assert!(state.capture(&mut bottom_edge, &mut top_edge));
1262            assert!(bottom_edge.is_locked());
1263            assert!(!top_edge.is_locked());
1264        }
1265        // Capture top, ignore invalid bottom
1266        {
1267            let mut bottom_edge = Hint {
1268                // Empty flags == invalid hint
1269                flags: 0,
1270                ..bottom_edge
1271            };
1272            let mut top_edge = top_edge;
1273            assert!(state.capture(&mut bottom_edge, &mut top_edge));
1274            assert!(!bottom_edge.is_locked());
1275            assert!(top_edge.is_locked());
1276        }
1277    }
1278
1279    #[test]
1280    fn hint_mask_ops() {
1281        const MAX_BITS: usize = HINT_MASK_SIZE * 8;
1282        let all_bits = HintMask::all();
1283        for i in 0..MAX_BITS {
1284            assert!(all_bits.get(i));
1285        }
1286        let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap();
1287        for i in 0..MAX_BITS {
1288            assert_eq!(i & 1 != 0, odd_bits.get(i));
1289        }
1290        let mut cleared_bits = odd_bits;
1291        for i in 0..MAX_BITS {
1292            if i & 1 != 0 {
1293                cleared_bits.clear(i);
1294            }
1295        }
1296        assert_eq!(cleared_bits.mask, HintMask::default().mask);
1297    }
1298
1299    #[test]
1300    fn hint_mapping() {
1301        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1302        let cff_font = super::super::Outlines::new(&font).unwrap();
1303        let state = cff_font
1304            .subfont(0, Some(8.0), &[F2Dot14::from_f32(-1.0); 2])
1305            .unwrap()
1306            .hint_state;
1307        let mut initial_map = HintMap::new(state.scale);
1308        let mut map = HintMap::new(state.scale);
1309        // Stem hints from Cantarell-VF.otf glyph id 2
1310        let mut stems = [
1311            StemHint {
1312                min: Fixed::from_bits(1376256),
1313                max: Fixed::ZERO,
1314                ..Default::default()
1315            },
1316            StemHint {
1317                min: Fixed::from_bits(16318464),
1318                max: Fixed::from_bits(17563648),
1319                ..Default::default()
1320            },
1321            StemHint {
1322                min: Fixed::from_bits(45481984),
1323                max: Fixed::from_bits(44171264),
1324                ..Default::default()
1325            },
1326        ];
1327        map.build(
1328            &state,
1329            Some(HintMask::all()),
1330            Some(&mut initial_map),
1331            &mut stems,
1332            Fixed::ZERO,
1333            false,
1334        );
1335        // FT generates the following hint map:
1336        //
1337        // index  csCoord  dsCoord  scale  flags
1338        //   0       0.00     0.00    526  gbL
1339        //   1     249.00   250.14    524  pb
1340        //   1     268.00   238.22    592  pt
1341        //   2     694.00   750.41    524  gtL
1342        let expected_edges = [
1343            Hint {
1344                index: 0,
1345                cs_coord: Fixed::from_f64(0.0),
1346                ds_coord: Fixed::from_f64(0.0),
1347                scale: Fixed::from_bits(526),
1348                flags: GHOST_BOTTOM | LOCKED,
1349            },
1350            Hint {
1351                index: 1,
1352                cs_coord: Fixed::from_bits(16318464),
1353                ds_coord: Fixed::from_bits(131072),
1354                scale: Fixed::from_bits(524),
1355                flags: PAIR_BOTTOM,
1356            },
1357            Hint {
1358                index: 1,
1359                cs_coord: Fixed::from_bits(17563648),
1360                ds_coord: Fixed::from_bits(141028),
1361                scale: Fixed::from_bits(592),
1362                flags: PAIR_TOP,
1363            },
1364            Hint {
1365                index: 2,
1366                cs_coord: Fixed::from_bits(45481984),
1367                ds_coord: Fixed::from_bits(393216),
1368                scale: Fixed::from_bits(524),
1369                flags: GHOST_TOP | LOCKED,
1370            },
1371        ];
1372        assert_eq!(expected_edges, &map.edges[..map.len]);
1373        // And FT generates the following mappings
1374        let mappings = [
1375            // (coord in font units, expected hinted coord in device space) in 16.16
1376            (0, 0),             // 0 -> 0
1377            (44302336, 382564), // 676 -> 5.828125
1378            (45481984, 393216), // 694 -> 6
1379            (16318464, 131072), // 249 -> 2
1380            (17563648, 141028), // 268 -> 2.140625
1381            (49676288, 426752), // 758 -> 6.5
1382            (56754176, 483344), // 866 -> 7.375
1383            (57868288, 492252), // 883 -> 7.5
1384            (50069504, 429896), // 764 -> 6.546875
1385        ];
1386        for (coord, expected) in mappings {
1387            assert_eq!(
1388                map.transform(Fixed::from_bits(coord)),
1389                Fixed::from_bits(expected)
1390            );
1391        }
1392    }
1393
1394    #[test]
1395    fn midpoint_avoids_overflow() {
1396        // We encountered an overflow in the HintMap::insert midpoint
1397        // calculation for glyph id 950 at size 74 in
1398        // KawkabMono-Bold v0.501 <https://github.com/aiaf/kawkab-mono/tree/v0.501>.
1399        // Test that our midpoint function doesn't overflow when the sum of
1400        // the high 16 bits of the two values exceeds i16::MAX.
1401        let a = i16::MAX as i32;
1402        let b = a - 1;
1403        assert!(a + b > i16::MAX as i32);
1404        let mid = super::midpoint(Fixed::from_i32(a), Fixed::from_i32(b));
1405        assert_eq!((a + b) / 2, mid.to_bits() >> 16);
1406    }
1407
1408    /// HintingSink is mostly pass-through. This test captures the logic
1409    /// around omission of pending lines that match subpath start.
1410    /// See HintingSink::maybe_close_subpath for details.
1411    #[test]
1412    fn hinting_sink_omits_closing_line_that_matches_start() {
1413        let state = HintState {
1414            scale: Fixed::ONE,
1415            ..Default::default()
1416        };
1417        let mut path = Path::default();
1418        let mut sink = HintingSink::new(&state, &mut path);
1419        let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)];
1420        let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)];
1421        let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)];
1422        let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)];
1423        let curve = [
1424            Fixed::from_f64(3.0),
1425            Fixed::from_f64(4.0),
1426            Fixed::from_f64(5.0),
1427            Fixed::from_f64(6.0),
1428            Fixed::from_f64(1.0),
1429            Fixed::from_f64(2.0),
1430        ];
1431        // First subpath, closing line matches start
1432        sink.move_to(move1_2[0], move1_2[1]);
1433        sink.line_to(line2_3[0], line2_3[1]);
1434        sink.line_to(line1_2[0], line1_2[1]);
1435        // Second subpath, closing line does not match start
1436        sink.move_to(move1_2[0], move1_2[1]);
1437        sink.line_to(line2_3[0], line2_3[1]);
1438        sink.line_to(line3_4[0], line3_4[1]);
1439        // Third subpath, ends with cubic. Still emits a close command
1440        // even though end point matches start.
1441        sink.move_to(move1_2[0], move1_2[1]);
1442        sink.line_to(line2_3[0], line2_3[1]);
1443        sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]);
1444        sink.finish();
1445        // Subpaths always end with a close command. If a final line coincides
1446        // with the start of a subpath, it is omitted.
1447        assert_eq!(
1448            &path.0,
1449            &[
1450                // First subpath
1451                MoveTo(move1_2),
1452                LineTo(line2_3),
1453                // line1_2 is omitted
1454                Close,
1455                // Second subpath
1456                MoveTo(move1_2),
1457                LineTo(line2_3),
1458                LineTo(line3_4),
1459                Close,
1460                // Third subpath
1461                MoveTo(move1_2),
1462                LineTo(line2_3),
1463                CurveTo(curve),
1464                Close,
1465            ]
1466        );
1467    }
1468
1469    #[derive(Copy, Clone, PartialEq, Debug)]
1470    enum Command {
1471        MoveTo([Fixed; 2]),
1472        LineTo([Fixed; 2]),
1473        CurveTo([Fixed; 6]),
1474        Close,
1475    }
1476
1477    use Command::*;
1478
1479    #[derive(Default)]
1480    struct Path(Vec<Command>);
1481
1482    impl CommandSink for Path {
1483        fn move_to(&mut self, x: Fixed, y: Fixed) {
1484            self.0.push(MoveTo([x, y]));
1485        }
1486        fn line_to(&mut self, x: Fixed, y: Fixed) {
1487            self.0.push(LineTo([x, y]));
1488        }
1489        fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
1490            self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y]));
1491        }
1492        fn close(&mut self) {
1493            self.0.push(Close);
1494        }
1495    }
1496}