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