Skip to main content

skrifa/outline/autohint/metrics/
blues.rs

1//! Latin blue values.
2
3use super::{
4    super::{
5        super::{unscaled::UnscaledOutlineBuf, OutlineGlyphCollection},
6        shape::{ShapedCluster, Shaper},
7        style::{ScriptGroup, StyleClass},
8    },
9    ScaledWidth,
10};
11use crate::{collections::SmallVec, FontRef, MetadataProvider};
12use raw::types::F2Dot14;
13use raw::TableProvider;
14
15/// Maximum number of blue values.
16///
17/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L328>
18const MAX_BLUES: usize = 8;
19
20// Chosen to maximize opportunity to avoid heap allocation while keeping stack
21// size < 2k.
22const MAX_INLINE_POINTS: usize = 256;
23
24// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L73>
25const BLUE_STRING_MAX_LEN: usize = 51;
26
27/// Defines the zone(s) that are associated with a blue value.
28#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
29#[repr(transparent)]
30pub struct BlueZones(u16);
31
32impl BlueZones {
33    // These properties ostensibly come from
34    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L317>
35    // but are modified to match those at
36    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L68>
37    // so that when don't need to keep two sets and adjust during blue
38    // computation.
39    pub const NONE: Self = Self(0);
40    pub const TOP: Self = Self(1 << 1);
41    pub const SUB_TOP: Self = Self(1 << 2);
42    pub const NEUTRAL: Self = Self(1 << 3);
43    pub const ADJUSTMENT: Self = Self(1 << 4);
44    pub const X_HEIGHT: Self = Self(1 << 5);
45    pub const LONG: Self = Self(1 << 6);
46    pub const HORIZONTAL: Self = Self(1 << 2);
47    pub const RIGHT: Self = Self::TOP;
48
49    pub const fn contains(self, other: Self) -> bool {
50        self.0 & other.0 == other.0
51    }
52
53    // Used for generated data structures because the bit-or operator
54    // cannot be const.
55    #[must_use]
56    pub(crate) const fn union(self, other: Self) -> Self {
57        Self(self.0 | other.0)
58    }
59
60    pub(crate) fn is_top_like(self) -> bool {
61        self & (Self::TOP | Self::SUB_TOP) != Self::NONE
62    }
63
64    pub fn is_top(self) -> bool {
65        self.contains(Self::TOP)
66    }
67
68    pub fn is_sub_top(self) -> bool {
69        self.contains(Self::SUB_TOP)
70    }
71
72    pub fn is_neutral(self) -> bool {
73        self.contains(Self::NEUTRAL)
74    }
75
76    pub fn is_x_height(self) -> bool {
77        self.contains(Self::X_HEIGHT)
78    }
79
80    pub(crate) fn is_long(self) -> bool {
81        self.contains(Self::LONG)
82    }
83
84    pub(crate) fn is_horizontal(self) -> bool {
85        self.contains(Self::HORIZONTAL)
86    }
87
88    pub(crate) fn is_right(self) -> bool {
89        self.contains(Self::RIGHT)
90    }
91
92    #[must_use]
93    pub fn retain_top_like_or_neutral(self) -> Self {
94        self & (Self::TOP | Self::SUB_TOP | Self::NEUTRAL)
95    }
96}
97
98impl core::ops::Not for BlueZones {
99    type Output = Self;
100
101    fn not(self) -> Self::Output {
102        Self(!self.0)
103    }
104}
105
106impl core::ops::BitOr for BlueZones {
107    type Output = Self;
108
109    fn bitor(self, rhs: Self) -> Self::Output {
110        Self(self.0 | rhs.0)
111    }
112}
113
114impl core::ops::BitOrAssign for BlueZones {
115    fn bitor_assign(&mut self, rhs: Self) {
116        self.0 |= rhs.0;
117    }
118}
119
120impl core::ops::BitAnd for BlueZones {
121    type Output = Self;
122
123    fn bitand(self, rhs: Self) -> Self::Output {
124        Self(self.0 & rhs.0)
125    }
126}
127
128impl core::ops::BitAndAssign for BlueZones {
129    fn bitand_assign(&mut self, rhs: Self) {
130        self.0 &= rhs.0;
131    }
132}
133
134/// An unscaled alignment zone.
135// FreeType keeps a single array of blue values per metrics set
136// and mutates when the scale factor changes. We'll separate them so
137// that we can reuse unscaled metrics as immutable state without
138// recomputing them (which is the expensive part).
139// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L77>
140#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
141pub struct UnscaledBlue {
142    /// Position of the blue.
143    pub position: i32,
144    /// Overshoot value of the blue.
145    pub overshoot: i32,
146    /// Maximum extent of outlines used to compute this blue.
147    pub ascender: i32,
148    /// Minimum extent of outlines used to compute this blue.
149    pub descender: i32,
150    /// Active zones for this blue.
151    pub zones: BlueZones,
152}
153
154pub(crate) type UnscaledBlues = SmallVec<UnscaledBlue, MAX_BLUES>;
155
156/// A scaled alignment zone.
157#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
158pub struct ScaledBlue {
159    /// Scaled position of the blue.
160    pub position: ScaledWidth,
161    /// Scaled overshoot for the blue.
162    pub overshoot: ScaledWidth,
163    /// Active zones for this blue.
164    pub zones: BlueZones,
165    /// True if the blue is active.
166    pub is_active: bool,
167}
168
169pub(crate) type ScaledBlues = SmallVec<ScaledBlue, MAX_BLUES>;
170
171/// Compute unscaled blues values for each axis.
172pub(crate) fn compute_unscaled_blues(
173    shaper: &Shaper,
174    coords: &[F2Dot14],
175    style: &StyleClass,
176) -> [UnscaledBlues; 2] {
177    match style.script.group {
178        ScriptGroup::Default => [
179            // Default group doesn't have horizontal blues
180            Default::default(),
181            compute_default_blues(shaper, coords, style),
182        ],
183        ScriptGroup::Cjk => compute_cjk_blues(shaper, coords, style),
184        // Indic group doesn't use blue values (yet?)
185        ScriptGroup::Indic => Default::default(),
186    }
187}
188
189/// Compute unscaled blue values for the default script set.
190///
191/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L314>
192fn compute_default_blues(shaper: &Shaper, coords: &[F2Dot14], style: &StyleClass) -> UnscaledBlues {
193    let mut blues = UnscaledBlues::new();
194    let (mut outline_buf, mut flats, mut rounds) = buffers();
195    let (glyphs, units_per_em) = things_all_blues_need(shaper.font());
196    let flat_threshold = units_per_em / 14;
197    let mut cluster_shaper = shaper.cluster_shaper(style);
198    let mut shaped_cluster = ShapedCluster::default();
199    // Walk over each of the blue character sets for our script.
200    for (blue_str, blue_zones) in style.script.blues {
201        let mut ascender = i32::MIN;
202        let mut descender = i32::MAX;
203        let mut n_flats = 0;
204        let mut n_rounds = 0;
205        for cluster in blue_str.split(' ') {
206            let mut best_y_extremum = if blue_zones.is_top() {
207                i32::MIN
208            } else {
209                i32::MAX
210            };
211            let mut best_is_round = false;
212            cluster_shaper.shape(cluster, &mut shaped_cluster);
213            for (glyph, y_offset) in shaped_cluster
214                .iter()
215                .filter(|g| g.id.to_u32() != 0)
216                .filter_map(|g| Some((glyphs.get(g.id)?, g.y_offset)))
217            {
218                outline_buf.clear();
219                if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
220                    continue;
221                }
222                let outline = outline_buf.as_ref();
223                // Reject glyphs that can't produce any rendering
224                if outline.points.len() <= 2 {
225                    continue;
226                }
227                let mut best_y: Option<i16> = None;
228                // Find the extreme point depending on whether this is a top or
229                // bottom blue
230                let best_contour_and_point = if blue_zones.is_top_like() {
231                    outline.find_last_contour(|point| {
232                        if best_y.is_none() || Some(point.y) > best_y {
233                            best_y = Some(point.y);
234                            ascender = ascender.max(point.y as i32 + y_offset);
235                            true
236                        } else {
237                            descender = descender.min(point.y as i32 + y_offset);
238                            false
239                        }
240                    })
241                } else {
242                    outline.find_last_contour(|point| {
243                        if best_y.is_none() || Some(point.y) < best_y {
244                            best_y = Some(point.y);
245                            descender = descender.min(point.y as i32 + y_offset);
246                            true
247                        } else {
248                            ascender = ascender.max(point.y as i32 + y_offset);
249                            false
250                        }
251                    })
252                };
253                let Some((best_contour_range, best_point_ix)) = best_contour_and_point else {
254                    continue;
255                };
256                let best_contour = &outline.points[best_contour_range];
257                // If we have a contour and point then best_y is guaranteed to
258                // be Some
259                let mut best_y = best_y.unwrap() as i32;
260                let best_x = best_contour[best_point_ix].x as i32;
261                // Now determine whether the point belongs to a straight or
262                // round segment by examining the previous and next points.
263                let [mut on_point_first, mut on_point_last] =
264                    if best_contour[best_point_ix].is_on_curve() {
265                        [Some(best_point_ix); 2]
266                    } else {
267                        [None; 2]
268                    };
269                let mut segment_first = best_point_ix;
270                let mut segment_last = best_point_ix;
271                // Look for the previous and next points on the contour that
272                // are not on the same Y coordinate, then threshold the
273                // "closeness"
274                for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
275                    let dist = (prev.y as i32 - best_y).abs();
276                    // Allow a small distance or angle (20 == roughly 2.9 degrees)
277                    if dist > 5 && ((prev.x as i32 - best_x).abs() <= (20 * dist)) {
278                        break;
279                    }
280                    segment_first = ix;
281                    if prev.is_on_curve() {
282                        on_point_first = Some(ix);
283                        if on_point_last.is_none() {
284                            on_point_last = Some(ix);
285                        }
286                    }
287                }
288                let mut next_ix = 0;
289                for (ix, next) in cycle_forward(best_contour, best_point_ix) {
290                    // Save next_ix which is used in "long" blue computation
291                    // later
292                    next_ix = ix;
293                    let dist = (next.y as i32 - best_y).abs();
294                    // Allow a small distance or angle (20 == roughly 2.9 degrees)
295                    if dist > 5 && ((next.x as i32 - best_x).abs() <= (20 * dist)) {
296                        break;
297                    }
298                    segment_last = ix;
299                    if next.is_on_curve() {
300                        on_point_last = Some(ix);
301                        if on_point_first.is_none() {
302                            on_point_first = Some(ix);
303                        }
304                    }
305                }
306                if blue_zones.is_long() {
307                    // Taken verbatim from FreeType:
308                    //
309                    // "If this flag is set, we have an additional constraint to
310                    // get the blue zone distance: Find a segment of the topmost
311                    // (or bottommost) contour that is longer than a heuristic
312                    // threshold.  This ensures that small bumps in the outline
313                    // are ignored (for example, the `vertical serifs' found in
314                    // many Hebrew glyph designs).
315                    //
316                    // If this segment is long enough, we are done.  Otherwise,
317                    // search the segment next to the extremum that is long
318                    // enough, has the same direction, and a not too large
319                    // vertical distance from the extremum.  Note that the
320                    // algorithm doesn't check whether the found segment is
321                    // actually the one (vertically) nearest to the extremum.""
322                    //
323                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L641>
324                    // heuristic threshold value
325                    let length_threshold = units_per_em / 25;
326                    let dist = (best_contour[segment_last].x as i32
327                        - best_contour[segment_first].x as i32)
328                        .abs();
329                    if dist < length_threshold
330                        && satisfies_min_long_segment_len(
331                            segment_first,
332                            segment_last,
333                            best_contour.len() - 1,
334                        )
335                    {
336                        // heuristic threshold value
337                        let height_threshold = units_per_em / 4;
338                        // find previous point with different x value
339                        let mut prev_ix = best_point_ix;
340                        for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
341                            if prev.x as i32 != best_x {
342                                prev_ix = ix;
343                                break;
344                            }
345                        }
346                        // skip for degenerate case
347                        if prev_ix == best_point_ix {
348                            continue;
349                        }
350                        let is_ltr = (best_contour[prev_ix].x as i32) < best_x;
351                        let mut first = segment_last;
352                        let mut last = first;
353                        let mut p_first = None;
354                        let mut p_last = None;
355                        let mut hit = false;
356                        loop {
357                            if !hit {
358                                // no hit, adjust first point
359                                first = last;
360                                // also adjust first and last on curve point
361                                if best_contour[first].is_on_curve() {
362                                    p_first = Some(first);
363                                    p_last = Some(first);
364                                } else {
365                                    p_first = None;
366                                    p_last = None;
367                                }
368                                hit = true;
369                            }
370                            if last < best_contour.len() - 1 {
371                                last += 1;
372                            } else {
373                                last = 0;
374                            }
375                            if (best_y - best_contour[first].y as i32).abs() > height_threshold {
376                                // vertical distance too large
377                                hit = false;
378                                continue;
379                            }
380                            let dist =
381                                (best_contour[last].y as i32 - best_contour[first].y as i32).abs();
382                            if dist > 5
383                                && (best_contour[last].x as i32 - best_contour[first].x as i32)
384                                    .abs()
385                                    <= 20 * dist
386                            {
387                                hit = false;
388                                if last == segment_first {
389                                    break;
390                                }
391                                continue;
392                            }
393                            if best_contour[last].is_on_curve() {
394                                p_last = Some(last);
395                                if p_first.is_none() {
396                                    p_first = Some(last);
397                                }
398                            }
399                            let first_x = best_contour[first].x as i32;
400                            let last_x = best_contour[last].x as i32;
401                            let is_cur_ltr = first_x < last_x;
402                            let dx = (last_x - first_x).abs();
403                            if is_cur_ltr == is_ltr && dx >= length_threshold {
404                                loop {
405                                    if last < best_contour.len() - 1 {
406                                        last += 1;
407                                    } else {
408                                        last = 0;
409                                    }
410                                    let dy = (best_contour[last].y as i32
411                                        - best_contour[first].y as i32)
412                                        .abs();
413                                    if dy > 5
414                                        && (best_contour[next_ix].x as i32
415                                            - best_contour[first].x as i32)
416                                            .abs()
417                                            <= 20 * dist
418                                    {
419                                        if last > 0 {
420                                            last -= 1;
421                                        } else {
422                                            last = best_contour.len() - 1;
423                                        }
424                                        break;
425                                    }
426                                    p_last = Some(last);
427                                    if best_contour[last].is_on_curve() {
428                                        p_last = Some(last);
429                                        if p_first.is_none() {
430                                            p_first = Some(last);
431                                        }
432                                    }
433                                    if last == segment_first {
434                                        break;
435                                    }
436                                }
437                                best_y = best_contour[first].y as i32;
438                                segment_first = first;
439                                segment_last = last;
440                                on_point_first = p_first;
441                                on_point_last = p_last;
442                                break;
443                            }
444                            if last == segment_first {
445                                break;
446                            }
447                        }
448                    }
449                }
450                best_y += y_offset;
451                // Is the segment round?
452                // 1. horizontal distance between first and last oncurve point
453                //    is larger than a heuristic flat threshold, then it's flat
454                // 2. either first or last point of segment is offcurve then
455                //    it's round
456                let is_round = match (on_point_first, on_point_last) {
457                    (Some(first), Some(last))
458                        if (best_contour[last].x as i32 - best_contour[first].x as i32).abs()
459                            > flat_threshold =>
460                    {
461                        false
462                    }
463                    _ => {
464                        !best_contour[segment_first].is_on_curve()
465                            || !best_contour[segment_last].is_on_curve()
466                    }
467                };
468                if is_round && blue_zones.is_neutral() {
469                    // Ignore round segments for neutral zone
470                    continue;
471                }
472                // This seems to ignore LATIN_SUB_TOP?
473                if blue_zones.is_top() {
474                    if best_y > best_y_extremum {
475                        best_y_extremum = best_y;
476                        best_is_round = is_round;
477                    }
478                } else if best_y < best_y_extremum {
479                    best_y_extremum = best_y;
480                    best_is_round = is_round;
481                }
482            }
483            if best_y_extremum != i32::MIN && best_y_extremum != i32::MAX {
484                if best_is_round {
485                    rounds[n_rounds] = best_y_extremum;
486                    n_rounds += 1;
487                } else {
488                    flats[n_flats] = best_y_extremum;
489                    n_flats += 1;
490                }
491            }
492        }
493        if n_flats == 0 && n_rounds == 0 {
494            continue;
495        }
496        rounds[..n_rounds].sort_unstable();
497        flats[..n_flats].sort_unstable();
498        let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
499            let val = rounds[n_rounds / 2];
500            (val, val)
501        } else if n_rounds == 0 {
502            let val = flats[n_flats / 2];
503            (val, val)
504        } else {
505            (flats[n_flats / 2], rounds[n_rounds / 2])
506        };
507        if blue_shoot != blue_ref {
508            let over_ref = blue_shoot > blue_ref;
509            if blue_zones.is_top_like() ^ over_ref {
510                let val = (blue_shoot + blue_ref) / 2;
511                blue_ref = val;
512                blue_shoot = val;
513            }
514        }
515        let mut blue = UnscaledBlue {
516            position: blue_ref,
517            overshoot: blue_shoot,
518            ascender,
519            descender,
520            zones: blue_zones.retain_top_like_or_neutral(),
521        };
522        if blue_zones.is_x_height() {
523            blue.zones |= BlueZones::ADJUSTMENT;
524        }
525        blues.push(blue);
526    }
527    // sort bottoms
528    let mut sorted_indices: [usize; MAX_BLUES] = core::array::from_fn(|ix| ix);
529    let blue_values = blues.as_mut_slice();
530    let len = blue_values.len();
531    if len == 0 {
532        return blues;
533    }
534    // sort from bottom to top
535    for i in 1..len {
536        for j in (1..=i).rev() {
537            let first = &blue_values[sorted_indices[j - 1]];
538            let second = &blue_values[sorted_indices[j]];
539            let a = if first.zones.is_top_like() {
540                first.position
541            } else {
542                first.overshoot
543            };
544            let b = if second.zones.is_top_like() {
545                second.position
546            } else {
547                second.overshoot
548            };
549            if b >= a {
550                break;
551            }
552            sorted_indices.swap(j, j - 1);
553        }
554    }
555    // and adjust tops
556    for i in 0..len - 1 {
557        let index1 = sorted_indices[i];
558        let index2 = sorted_indices[i + 1];
559        let first = &blue_values[index1];
560        let second = &blue_values[index2];
561        let a = if first.zones.is_top_like() {
562            first.overshoot
563        } else {
564            first.position
565        };
566        let b = if second.zones.is_top_like() {
567            second.overshoot
568        } else {
569            second.position
570        };
571        if a > b {
572            if first.zones.is_top_like() {
573                blue_values[index1].overshoot = b;
574            } else {
575                blue_values[index1].position = b;
576            }
577        }
578    }
579    blues
580}
581
582/// Given inclusive indices and a contour length, returns true if the segment
583/// is of sufficient size to test for bumps when detecting "long" Hebrew
584/// alignment zones.
585fn satisfies_min_long_segment_len(first_ix: usize, last_ix: usize, contour_last: usize) -> bool {
586    let inclusive_diff = if first_ix <= last_ix {
587        last_ix - first_ix
588    } else {
589        // If first_ix > last_ix, then we want to capture the sum of the ranges
590        // [first_ix, contour_last] and [0, last_ix]
591        // We add 1 here to ensure the element that crosses the boundary is
592        // included. For example, if first_ix == contour_last and
593        // last_ix == 0, then we want the result to be 1
594        contour_last - first_ix + 1 + last_ix
595    };
596    // The +2 matches FreeType. The assumption is that this includes sufficient
597    // points to detect a bump and extend the segment?
598    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L663>
599    inclusive_diff + 2 <= contour_last
600}
601
602/// Compute unscaled blue values for the CJK script set.
603///
604/// Note: unlike the default code above, this produces two sets of blues,
605/// one for horizontal zones and one for vertical zones, respectively. The
606/// horizontal set is currently not generated because this has been
607/// disabled in FreeType but the code remains because we may want to revisit
608/// in the future.
609///
610/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L277>
611fn compute_cjk_blues(
612    shaper: &Shaper,
613    coords: &[F2Dot14],
614    style: &StyleClass,
615) -> [UnscaledBlues; 2] {
616    let mut blues = [UnscaledBlues::new(), UnscaledBlues::new()];
617    let (mut outline_buf, mut flats, mut fills) = buffers();
618    let (glyphs, _) = things_all_blues_need(shaper.font());
619    let mut cluster_shaper = shaper.cluster_shaper(style);
620    let mut shaped_cluster = ShapedCluster::default();
621    // Walk over each of the blue character sets for our script.
622    for (blue_str, blue_zones) in style.script.blues {
623        let is_horizontal = blue_zones.is_horizontal();
624        // Note: horizontal blue zones are disabled by default and have been
625        // for many years in FreeType:
626        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L35>
627        // and <https://gitlab.freedesktop.org/freetype/freetype/-/commit/084abf0469d32a94b1c315bee10f621284694328>
628        if is_horizontal {
629            continue;
630        }
631        let is_right = blue_zones.is_right();
632        let is_top = blue_zones.is_top();
633        let blues = &mut blues[!is_horizontal as usize];
634        if blues.len() >= MAX_BLUES {
635            continue;
636        }
637        let mut n_flats = 0;
638        let mut n_fills = 0;
639        let mut is_fill = true;
640        for cluster in blue_str.split(' ') {
641            // The '|' character is used as a sentinel in the blue string that
642            // signifies a switch to characters that define "flat" values
643            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L380>
644            if cluster == "|" {
645                is_fill = false;
646                continue;
647            }
648            cluster_shaper.shape(cluster, &mut shaped_cluster);
649            for glyph in shaped_cluster
650                .iter()
651                .filter(|g| g.id.to_u32() != 0)
652                .filter_map(|g| glyphs.get(g.id))
653            {
654                outline_buf.clear();
655                if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
656                    continue;
657                }
658                let outline = outline_buf.as_ref();
659                // Reject glyphs that can't produce any rendering
660                if outline.points.len() <= 2 {
661                    continue;
662                }
663                // Step right up and find an extrema!
664                // Unwrap is safe because we know per ^ that we have at least 3 points
665                let best_pos = outline
666                    .points
667                    .iter()
668                    .map(|p| if is_horizontal { p.x } else { p.y })
669                    .reduce(
670                        if (is_horizontal && is_right) || (!is_horizontal && is_top) {
671                            |a: i16, c: i16| a.max(c)
672                        } else {
673                            |a: i16, c: i16| a.min(c)
674                        },
675                    )
676                    .unwrap();
677                if is_fill {
678                    fills[n_fills] = best_pos;
679                    n_fills += 1;
680                } else {
681                    flats[n_flats] = best_pos;
682                    n_flats += 1;
683                }
684            }
685        }
686        if n_flats == 0 && n_fills == 0 {
687            continue;
688        }
689        // Now determine the reference and overshoot of the blue; simply
690        // take the median after a sort
691        fills[..n_fills].sort_unstable();
692        flats[..n_flats].sort_unstable();
693        let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
694            let value = fills[n_fills / 2] as i32;
695            (value, value)
696        } else if n_fills == 0 {
697            let value = flats[n_flats / 2] as i32;
698            (value, value)
699        } else {
700            (fills[n_fills / 2] as i32, flats[n_flats / 2] as i32)
701        };
702        // Make sure blue_ref >= blue_shoot for top/right or vice versa for
703        // bottom left
704        if blue_shoot != blue_ref {
705            let under_ref = blue_shoot < blue_ref;
706            if blue_zones.is_top() ^ under_ref {
707                blue_ref = (blue_shoot + blue_ref) / 2;
708                blue_shoot = blue_ref;
709            }
710        }
711        blues.push(UnscaledBlue {
712            position: blue_ref,
713            overshoot: blue_shoot,
714            ascender: 0,
715            descender: 0,
716            zones: *blue_zones & BlueZones::TOP,
717        });
718    }
719    blues
720}
721
722#[inline(always)]
723fn buffers<T: Copy + Default>() -> (
724    UnscaledOutlineBuf<MAX_INLINE_POINTS>,
725    [T; BLUE_STRING_MAX_LEN],
726    [T; BLUE_STRING_MAX_LEN],
727) {
728    (
729        UnscaledOutlineBuf::<MAX_INLINE_POINTS>::new(),
730        [T::default(); BLUE_STRING_MAX_LEN],
731        [T::default(); BLUE_STRING_MAX_LEN],
732    )
733}
734
735/// A thneed is something everyone needs
736#[inline(always)]
737fn things_all_blues_need<'a>(font: &FontRef<'a>) -> (OutlineGlyphCollection<'a>, i32) {
738    (
739        font.outline_glyphs(),
740        font.head()
741            .map(|head| head.units_per_em())
742            .unwrap_or_default() as i32,
743    )
744}
745
746/// Iterator that begins at `start + 1` and cycles through all items
747/// of the slice in forward order, ending with `start`.
748pub(super) fn cycle_forward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
749    let len = items.len();
750    let start = start + 1;
751    (0..len).map(move |ix| {
752        let real_ix = (ix + start) % len;
753        (real_ix, &items[real_ix])
754    })
755}
756
757/// Iterator that begins at `start - 1` and cycles through all items
758/// of the slice in reverse order, ending with `start`.
759pub(super) fn cycle_backward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
760    let len = items.len();
761    (0..len).rev().map(move |ix| {
762        let real_ix = (ix + start) % len;
763        (real_ix, &items[real_ix])
764    })
765}
766
767#[cfg(test)]
768mod tests {
769    use crate::outline::autohint::metrics::BlueZones;
770
771    use super::{
772        super::super::{
773            shape::{Shaper, ShaperMode},
774            style,
775        },
776        satisfies_min_long_segment_len, UnscaledBlue,
777    };
778    use raw::FontRef;
779
780    #[test]
781    fn latin_blues() {
782        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
783        let shaper = Shaper::new(&font, ShaperMode::Nominal);
784        let style = &style::STYLE_CLASSES[super::StyleClass::LATN];
785        let blues = super::compute_default_blues(&shaper, &[], style);
786        let values = blues.as_slice();
787        let expected = [
788            UnscaledBlue {
789                position: 714,
790                overshoot: 725,
791                ascender: 725,
792                descender: -230,
793                zones: BlueZones::TOP,
794            },
795            UnscaledBlue {
796                position: 0,
797                overshoot: -10,
798                ascender: 725,
799                descender: -10,
800                zones: BlueZones::default(),
801            },
802            UnscaledBlue {
803                position: 760,
804                overshoot: 760,
805                ascender: 770,
806                descender: -240,
807                zones: BlueZones::TOP,
808            },
809            UnscaledBlue {
810                position: 536,
811                overshoot: 546,
812                ascender: 546,
813                descender: -10,
814                zones: BlueZones::TOP | BlueZones::ADJUSTMENT,
815            },
816            UnscaledBlue {
817                position: 0,
818                overshoot: -10,
819                ascender: 546,
820                descender: -10,
821                zones: BlueZones::default(),
822            },
823            UnscaledBlue {
824                position: -240,
825                overshoot: -240,
826                ascender: 760,
827                descender: -240,
828                zones: BlueZones::default(),
829            },
830        ];
831        assert_eq!(values, &expected);
832    }
833
834    #[test]
835    fn hebrew_long_blues() {
836        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
837        let shaper = Shaper::new(&font, ShaperMode::Nominal);
838        // Hebrew triggers "long" blue code path
839        let style = &style::STYLE_CLASSES[super::StyleClass::HEBR];
840        let blues = super::compute_default_blues(&shaper, &[], style);
841        let values = blues.as_slice();
842        assert_eq!(values.len(), 3);
843        let expected = [
844            UnscaledBlue {
845                position: 592,
846                overshoot: 592,
847                ascender: 647,
848                descender: -240,
849                zones: BlueZones::TOP,
850            },
851            UnscaledBlue {
852                position: 0,
853                overshoot: -9,
854                ascender: 647,
855                descender: -9,
856                zones: BlueZones::default(),
857            },
858            UnscaledBlue {
859                position: -240,
860                overshoot: -240,
861                ascender: 647,
862                descender: -240,
863                zones: BlueZones::default(),
864            },
865        ];
866        assert_eq!(values, &expected);
867    }
868
869    #[test]
870    fn cjk_blues() {
871        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
872        let shaper = Shaper::new(&font, ShaperMode::Nominal);
873        let style = &style::STYLE_CLASSES[super::StyleClass::HANI];
874        let blues = super::compute_cjk_blues(&shaper, &[], style);
875        let values = blues[1].as_slice();
876        let expected = [
877            UnscaledBlue {
878                position: 837,
879                overshoot: 824,
880                ascender: 0,
881                descender: 0,
882                zones: BlueZones::TOP,
883            },
884            UnscaledBlue {
885                position: -78,
886                overshoot: -66,
887                ascender: 0,
888                descender: 0,
889                zones: BlueZones::default(),
890            },
891        ];
892        assert_eq!(values, &expected);
893    }
894
895    #[test]
896    fn c2sc_shaped_blues() {
897        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
898        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
899        let style = &style::STYLE_CLASSES[super::StyleClass::LATN_C2SC];
900        let blues = super::compute_default_blues(&shaper, &[], style);
901        let values = blues.as_slice();
902        // Captured from FreeType with HarfBuzz enabled
903        let expected = [
904            UnscaledBlue {
905                position: 571,
906                overshoot: 571,
907                ascender: 571,
908                descender: 0,
909                zones: BlueZones::TOP,
910            },
911            UnscaledBlue {
912                position: 0,
913                overshoot: 0,
914                ascender: 571,
915                descender: 0,
916                zones: BlueZones::default(),
917            },
918        ];
919        assert_eq!(values, &expected);
920    }
921
922    /// Avoid subtraction overflow raised in
923    /// <https://github.com/googlefonts/fontations/issues/1218>
924    #[test]
925    fn long_segment_len_avoid_overflow() {
926        // Test font in issue above triggers overflow with
927        // first = 22, last = 0, contour_last = 22 (all inclusive).
928        // FreeType succeeds on this with suspicious signed
929        // arithmetic and we should too with our code that
930        // takes the boundary into account
931        assert!(satisfies_min_long_segment_len(22, 0, 22));
932    }
933
934    #[test]
935    fn cycle_iter_forward() {
936        let items = [0, 1, 2, 3, 4, 5, 6, 7];
937        let from_5 = super::cycle_forward(&items, 5)
938            .map(|(_, val)| *val)
939            .collect::<Vec<_>>();
940        assert_eq!(from_5, &[6, 7, 0, 1, 2, 3, 4, 5]);
941        let from_last = super::cycle_forward(&items, 7)
942            .map(|(_, val)| *val)
943            .collect::<Vec<_>>();
944        assert_eq!(from_last, &items);
945        // Don't panic on empty slice
946        let _ = super::cycle_forward::<i32>(&[], 5).count();
947    }
948
949    #[test]
950    fn cycle_iter_backward() {
951        let items = [0, 1, 2, 3, 4, 5, 6, 7];
952        let from_5 = super::cycle_backward(&items, 5)
953            .map(|(_, val)| *val)
954            .collect::<Vec<_>>();
955        assert_eq!(from_5, &[4, 3, 2, 1, 0, 7, 6, 5]);
956        let from_0 = super::cycle_backward(&items, 0)
957            .map(|(_, val)| *val)
958            .collect::<Vec<_>>();
959        assert_eq!(from_0, &[7, 6, 5, 4, 3, 2, 1, 0]);
960        // Don't panic on empty slice
961        let _ = super::cycle_backward::<i32>(&[], 5).count();
962    }
963}