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        if blues.len() >= MAX_BLUES {
206            continue;
207        }
208        for cluster in blue_str.split(' ') {
209            let mut best_y_extremum = if blue_zones.is_top() {
210                i32::MIN
211            } else {
212                i32::MAX
213            };
214            let mut best_is_round = false;
215            cluster_shaper.shape(cluster, &mut shaped_cluster);
216            for (glyph, y_offset) in shaped_cluster
217                .iter()
218                .filter(|g| g.id.to_u32() != 0)
219                .filter_map(|g| Some((glyphs.get(g.id)?, g.y_offset)))
220            {
221                outline_buf.clear();
222                if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
223                    continue;
224                }
225                let outline = outline_buf.as_ref();
226                // Reject glyphs that can't produce any rendering
227                if outline.points.len() <= 2 {
228                    continue;
229                }
230                let mut best_y: Option<i16> = None;
231                // Find the extreme point depending on whether this is a top or
232                // bottom blue
233                let best_contour_and_point = if blue_zones.is_top_like() {
234                    outline.find_last_contour(|point| {
235                        if best_y.is_none() || Some(point.y) > best_y {
236                            best_y = Some(point.y);
237                            ascender = ascender.max(point.y as i32 + y_offset);
238                            true
239                        } else {
240                            descender = descender.min(point.y as i32 + y_offset);
241                            false
242                        }
243                    })
244                } else {
245                    outline.find_last_contour(|point| {
246                        if best_y.is_none() || Some(point.y) < best_y {
247                            best_y = Some(point.y);
248                            descender = descender.min(point.y as i32 + y_offset);
249                            true
250                        } else {
251                            ascender = ascender.max(point.y as i32 + y_offset);
252                            false
253                        }
254                    })
255                };
256                let Some((best_contour_range, best_point_ix)) = best_contour_and_point else {
257                    continue;
258                };
259                let best_contour = &outline.points[best_contour_range];
260                let Some(mut best_y) = best_y.map(i32::from) else {
261                    continue;
262                };
263                let best_x = best_contour[best_point_ix].x as i32;
264                // Now determine whether the point belongs to a straight or
265                // round segment by examining the previous and next points.
266                let [mut on_point_first, mut on_point_last] =
267                    if best_contour[best_point_ix].is_on_curve() {
268                        [Some(best_point_ix); 2]
269                    } else {
270                        [None; 2]
271                    };
272                let mut segment_first = best_point_ix;
273                let mut segment_last = best_point_ix;
274                // Look for the previous and next points on the contour that
275                // are not on the same Y coordinate, then threshold the
276                // "closeness"
277                for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
278                    let dist = (prev.y as i32 - best_y).abs();
279                    // Allow a small distance or angle (20 == roughly 2.9 degrees)
280                    if dist > 5 && ((prev.x as i32 - best_x).abs() <= (20 * dist)) {
281                        break;
282                    }
283                    segment_first = ix;
284                    if prev.is_on_curve() {
285                        on_point_first = Some(ix);
286                        if on_point_last.is_none() {
287                            on_point_last = Some(ix);
288                        }
289                    }
290                }
291                let mut next_ix = 0;
292                for (ix, next) in cycle_forward(best_contour, best_point_ix) {
293                    // Save next_ix which is used in "long" blue computation
294                    // later
295                    next_ix = ix;
296                    let dist = (next.y as i32 - best_y).abs();
297                    // Allow a small distance or angle (20 == roughly 2.9 degrees)
298                    if dist > 5 && ((next.x as i32 - best_x).abs() <= (20 * dist)) {
299                        break;
300                    }
301                    segment_last = ix;
302                    if next.is_on_curve() {
303                        on_point_last = Some(ix);
304                        if on_point_first.is_none() {
305                            on_point_first = Some(ix);
306                        }
307                    }
308                }
309                if blue_zones.is_long() {
310                    // Taken verbatim from FreeType:
311                    //
312                    // "If this flag is set, we have an additional constraint to
313                    // get the blue zone distance: Find a segment of the topmost
314                    // (or bottommost) contour that is longer than a heuristic
315                    // threshold.  This ensures that small bumps in the outline
316                    // are ignored (for example, the `vertical serifs' found in
317                    // many Hebrew glyph designs).
318                    //
319                    // If this segment is long enough, we are done.  Otherwise,
320                    // search the segment next to the extremum that is long
321                    // enough, has the same direction, and a not too large
322                    // vertical distance from the extremum.  Note that the
323                    // algorithm doesn't check whether the found segment is
324                    // actually the one (vertically) nearest to the extremum.""
325                    //
326                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L641>
327                    // heuristic threshold value
328                    let length_threshold = units_per_em / 25;
329                    let dist = (best_contour[segment_last].x as i32
330                        - best_contour[segment_first].x as i32)
331                        .abs();
332                    if dist < length_threshold
333                        && satisfies_min_long_segment_len(
334                            segment_first,
335                            segment_last,
336                            best_contour.len() - 1,
337                        )
338                    {
339                        // heuristic threshold value
340                        let height_threshold = units_per_em / 4;
341                        // find previous point with different x value
342                        let mut prev_ix = best_point_ix;
343                        for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
344                            if prev.x as i32 != best_x {
345                                prev_ix = ix;
346                                break;
347                            }
348                        }
349                        // skip for degenerate case
350                        if prev_ix == best_point_ix {
351                            continue;
352                        }
353                        let is_ltr = (best_contour[prev_ix].x as i32) < best_x;
354                        let mut first = segment_last;
355                        let mut last = first;
356                        let mut p_first = None;
357                        let mut p_last = None;
358                        let mut hit = false;
359                        loop {
360                            if !hit {
361                                // no hit, adjust first point
362                                first = last;
363                                // also adjust first and last on curve point
364                                if best_contour[first].is_on_curve() {
365                                    p_first = Some(first);
366                                    p_last = Some(first);
367                                } else {
368                                    p_first = None;
369                                    p_last = None;
370                                }
371                                hit = true;
372                            }
373                            if last < best_contour.len() - 1 {
374                                last += 1;
375                            } else {
376                                last = 0;
377                            }
378                            if (best_y - best_contour[first].y as i32).abs() > height_threshold {
379                                // vertical distance too large
380                                hit = false;
381                                if last == segment_first {
382                                    break;
383                                }
384                                continue;
385                            }
386                            let dist =
387                                (best_contour[last].y as i32 - best_contour[first].y as i32).abs();
388                            if dist > 5
389                                && (best_contour[last].x as i32 - best_contour[first].x as i32)
390                                    .abs()
391                                    <= 20 * dist
392                            {
393                                hit = false;
394                                if last == segment_first {
395                                    break;
396                                }
397                                continue;
398                            }
399                            if best_contour[last].is_on_curve() {
400                                p_last = Some(last);
401                                if p_first.is_none() {
402                                    p_first = Some(last);
403                                }
404                            }
405                            let first_x = best_contour[first].x as i32;
406                            let last_x = best_contour[last].x as i32;
407                            let is_cur_ltr = first_x < last_x;
408                            let dx = (last_x - first_x).abs();
409                            if is_cur_ltr == is_ltr && dx >= length_threshold {
410                                loop {
411                                    if last < best_contour.len() - 1 {
412                                        last += 1;
413                                    } else {
414                                        last = 0;
415                                    }
416                                    let dy = (best_contour[last].y as i32
417                                        - best_contour[first].y as i32)
418                                        .abs();
419                                    if dy > 5
420                                        && (best_contour[next_ix].x as i32
421                                            - best_contour[first].x as i32)
422                                            .abs()
423                                            <= 20 * dist
424                                    {
425                                        if last > 0 {
426                                            last -= 1;
427                                        } else {
428                                            last = best_contour.len() - 1;
429                                        }
430                                        break;
431                                    }
432                                    p_last = Some(last);
433                                    if best_contour[last].is_on_curve() {
434                                        p_last = Some(last);
435                                        if p_first.is_none() {
436                                            p_first = Some(last);
437                                        }
438                                    }
439                                    if last == segment_first {
440                                        break;
441                                    }
442                                }
443                                best_y = best_contour[first].y as i32;
444                                segment_first = first;
445                                segment_last = last;
446                                on_point_first = p_first;
447                                on_point_last = p_last;
448                                break;
449                            }
450                            if last == segment_first {
451                                break;
452                            }
453                        }
454                    }
455                }
456                best_y += y_offset;
457                // Is the segment round?
458                // 1. horizontal distance between first and last oncurve point
459                //    is larger than a heuristic flat threshold, then it's flat
460                // 2. either first or last point of segment is offcurve then
461                //    it's round
462                let is_round = match (on_point_first, on_point_last) {
463                    (Some(first), Some(last))
464                        if (best_contour[last].x as i32 - best_contour[first].x as i32).abs()
465                            > flat_threshold =>
466                    {
467                        false
468                    }
469                    _ => {
470                        !best_contour[segment_first].is_on_curve()
471                            || !best_contour[segment_last].is_on_curve()
472                    }
473                };
474                if is_round && blue_zones.is_neutral() {
475                    // Ignore round segments for neutral zone
476                    continue;
477                }
478                // This seems to ignore LATIN_SUB_TOP?
479                if blue_zones.is_top() {
480                    if best_y > best_y_extremum {
481                        best_y_extremum = best_y;
482                        best_is_round = is_round;
483                    }
484                } else if best_y < best_y_extremum {
485                    best_y_extremum = best_y;
486                    best_is_round = is_round;
487                }
488            }
489            if best_y_extremum != i32::MIN && best_y_extremum != i32::MAX {
490                if best_is_round {
491                    if let Some(round) = rounds.get_mut(n_rounds) {
492                        *round = best_y_extremum;
493                        n_rounds += 1;
494                    }
495                } else if let Some(flat) = flats.get_mut(n_flats) {
496                    *flat = best_y_extremum;
497                    n_flats += 1;
498                }
499            }
500        }
501        if n_flats == 0 && n_rounds == 0 {
502            continue;
503        }
504        rounds[..n_rounds].sort_unstable();
505        flats[..n_flats].sort_unstable();
506        let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
507            let val = rounds[n_rounds / 2];
508            (val, val)
509        } else if n_rounds == 0 {
510            let val = flats[n_flats / 2];
511            (val, val)
512        } else {
513            (flats[n_flats / 2], rounds[n_rounds / 2])
514        };
515        if blue_shoot != blue_ref {
516            let over_ref = blue_shoot > blue_ref;
517            if blue_zones.is_top_like() ^ over_ref {
518                let val = (blue_shoot + blue_ref) / 2;
519                blue_ref = val;
520                blue_shoot = val;
521            }
522        }
523        let mut blue = UnscaledBlue {
524            position: blue_ref,
525            overshoot: blue_shoot,
526            ascender,
527            descender,
528            zones: blue_zones.retain_top_like_or_neutral(),
529        };
530        if blue_zones.is_x_height() {
531            blue.zones |= BlueZones::ADJUSTMENT;
532        }
533        blues.push(blue);
534    }
535    // sort bottoms
536    let mut sorted_indices: [usize; MAX_BLUES] = core::array::from_fn(|ix| ix);
537    let blue_values = blues.as_mut_slice();
538    let len = blue_values.len();
539    if len == 0 {
540        return blues;
541    }
542    // sort from bottom to top
543    for i in 1..len {
544        for j in (1..=i).rev() {
545            let first = &blue_values[sorted_indices[j - 1]];
546            let second = &blue_values[sorted_indices[j]];
547            let a = if first.zones.is_top_like() {
548                first.position
549            } else {
550                first.overshoot
551            };
552            let b = if second.zones.is_top_like() {
553                second.position
554            } else {
555                second.overshoot
556            };
557            if b >= a {
558                break;
559            }
560            sorted_indices.swap(j, j - 1);
561        }
562    }
563    // and adjust tops
564    for i in 0..len - 1 {
565        let index1 = sorted_indices[i];
566        let index2 = sorted_indices[i + 1];
567        let first = &blue_values[index1];
568        let second = &blue_values[index2];
569        let a = if first.zones.is_top_like() {
570            first.overshoot
571        } else {
572            first.position
573        };
574        let b = if second.zones.is_top_like() {
575            second.overshoot
576        } else {
577            second.position
578        };
579        if a > b {
580            if first.zones.is_top_like() {
581                blue_values[index1].overshoot = b;
582            } else {
583                blue_values[index1].position = b;
584            }
585        }
586    }
587    blues
588}
589
590/// Given inclusive indices and a contour length, returns true if the segment
591/// is of sufficient size to test for bumps when detecting "long" Hebrew
592/// alignment zones.
593fn satisfies_min_long_segment_len(first_ix: usize, last_ix: usize, contour_last: usize) -> bool {
594    let inclusive_diff = if first_ix <= last_ix {
595        last_ix - first_ix
596    } else {
597        // If first_ix > last_ix, then we want to capture the sum of the ranges
598        // [first_ix, contour_last] and [0, last_ix]
599        // We add 1 here to ensure the element that crosses the boundary is
600        // included. For example, if first_ix == contour_last and
601        // last_ix == 0, then we want the result to be 1
602        contour_last - first_ix + 1 + last_ix
603    };
604    // The +2 matches FreeType. The assumption is that this includes sufficient
605    // points to detect a bump and extend the segment?
606    // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L663>
607    inclusive_diff + 2 <= contour_last
608}
609
610/// Compute unscaled blue values for the CJK script set.
611///
612/// Note: unlike the default code above, this produces two sets of blues,
613/// one for horizontal zones and one for vertical zones, respectively. The
614/// horizontal set is currently not generated because this has been
615/// disabled in FreeType but the code remains because we may want to revisit
616/// in the future.
617///
618/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L277>
619fn compute_cjk_blues(
620    shaper: &Shaper,
621    coords: &[F2Dot14],
622    style: &StyleClass,
623) -> [UnscaledBlues; 2] {
624    let mut blues = [UnscaledBlues::new(), UnscaledBlues::new()];
625    let (mut outline_buf, mut flats, mut fills) = buffers();
626    let (glyphs, _) = things_all_blues_need(shaper.font());
627    let mut cluster_shaper = shaper.cluster_shaper(style);
628    let mut shaped_cluster = ShapedCluster::default();
629    // Walk over each of the blue character sets for our script.
630    for (blue_str, blue_zones) in style.script.blues {
631        let is_horizontal = blue_zones.is_horizontal();
632        // Note: horizontal blue zones are disabled by default and have been
633        // for many years in FreeType:
634        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L35>
635        // and <https://gitlab.freedesktop.org/freetype/freetype/-/commit/084abf0469d32a94b1c315bee10f621284694328>
636        if is_horizontal {
637            continue;
638        }
639        let is_right = blue_zones.is_right();
640        let is_top = blue_zones.is_top();
641        let blues = &mut blues[!is_horizontal as usize];
642        if blues.len() >= MAX_BLUES {
643            continue;
644        }
645        let mut n_flats = 0;
646        let mut n_fills = 0;
647        let mut is_fill = true;
648        for cluster in blue_str.split(' ') {
649            // The '|' character is used as a sentinel in the blue string that
650            // signifies a switch to characters that define "flat" values
651            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L380>
652            if cluster == "|" {
653                is_fill = false;
654                continue;
655            }
656            cluster_shaper.shape(cluster, &mut shaped_cluster);
657            for glyph in shaped_cluster
658                .iter()
659                .filter(|g| g.id.to_u32() != 0)
660                .filter_map(|g| glyphs.get(g.id))
661            {
662                outline_buf.clear();
663                if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
664                    continue;
665                }
666                let outline = outline_buf.as_ref();
667                // Reject glyphs that can't produce any rendering
668                if outline.points.len() <= 2 {
669                    continue;
670                }
671                // Step right up and find an extrema!
672                let best_pos = outline
673                    .points
674                    .iter()
675                    .map(|p| if is_horizontal { p.x } else { p.y })
676                    .reduce(
677                        if (is_horizontal && is_right) || (!is_horizontal && is_top) {
678                            |a: i16, c: i16| a.max(c)
679                        } else {
680                            |a: i16, c: i16| a.min(c)
681                        },
682                    );
683                let Some(best_pos) = best_pos else {
684                    continue;
685                };
686                if is_fill {
687                    if let Some(fill) = fills.get_mut(n_fills) {
688                        *fill = best_pos;
689                        n_fills += 1;
690                    }
691                } else if let Some(flat) = flats.get_mut(n_flats) {
692                    *flat = best_pos;
693                    n_flats += 1;
694                }
695            }
696        }
697        if n_flats == 0 && n_fills == 0 {
698            continue;
699        }
700        // Now determine the reference and overshoot of the blue; simply
701        // take the median after a sort
702        fills[..n_fills].sort_unstable();
703        flats[..n_flats].sort_unstable();
704        let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
705            let value = fills[n_fills / 2] as i32;
706            (value, value)
707        } else if n_fills == 0 {
708            let value = flats[n_flats / 2] as i32;
709            (value, value)
710        } else {
711            (fills[n_fills / 2] as i32, flats[n_flats / 2] as i32)
712        };
713        // Make sure blue_ref >= blue_shoot for top/right or vice versa for
714        // bottom left
715        if blue_shoot != blue_ref {
716            let under_ref = blue_shoot < blue_ref;
717            if blue_zones.is_top() ^ under_ref {
718                blue_ref = (blue_shoot + blue_ref) / 2;
719                blue_shoot = blue_ref;
720            }
721        }
722        blues.push(UnscaledBlue {
723            position: blue_ref,
724            overshoot: blue_shoot,
725            ascender: 0,
726            descender: 0,
727            zones: *blue_zones & BlueZones::TOP,
728        });
729    }
730    blues
731}
732
733#[inline(always)]
734fn buffers<T: Copy + Default>() -> (
735    UnscaledOutlineBuf<MAX_INLINE_POINTS>,
736    [T; BLUE_STRING_MAX_LEN],
737    [T; BLUE_STRING_MAX_LEN],
738) {
739    (
740        UnscaledOutlineBuf::<MAX_INLINE_POINTS>::new(),
741        [T::default(); BLUE_STRING_MAX_LEN],
742        [T::default(); BLUE_STRING_MAX_LEN],
743    )
744}
745
746/// A thneed is something everyone needs
747#[inline(always)]
748fn things_all_blues_need<'a>(font: &FontRef<'a>) -> (OutlineGlyphCollection<'a>, i32) {
749    (
750        font.outline_glyphs(),
751        font.head()
752            .map(|head| head.units_per_em())
753            .unwrap_or_default() as i32,
754    )
755}
756
757/// Iterator that begins at `start + 1` and cycles through all items
758/// of the slice in forward order, ending with `start`.
759pub(super) fn cycle_forward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
760    let len = items.len();
761    let start = start + 1;
762    (0..len).map(move |ix| {
763        let real_ix = (ix + start) % len;
764        (real_ix, &items[real_ix])
765    })
766}
767
768/// Iterator that begins at `start - 1` and cycles through all items
769/// of the slice in reverse order, ending with `start`.
770pub(super) fn cycle_backward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
771    let len = items.len();
772    (0..len).rev().map(move |ix| {
773        let real_ix = (ix + start) % len;
774        (real_ix, &items[real_ix])
775    })
776}
777
778#[cfg(test)]
779mod tests {
780    use crate::outline::autohint::metrics::BlueZones;
781
782    use super::{
783        super::super::{
784            shape::{Shaper, ShaperMode},
785            style,
786        },
787        satisfies_min_long_segment_len, UnscaledBlue,
788    };
789    use raw::{types::Tag, FontRef};
790
791    #[test]
792    fn latin_blues() {
793        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
794        let shaper = Shaper::new(&font, ShaperMode::Nominal);
795        let style = &style::STYLE_CLASSES[super::StyleClass::LATN];
796        let blues = super::compute_default_blues(&shaper, &[], style);
797        let values = blues.as_slice();
798        let expected = [
799            UnscaledBlue {
800                position: 714,
801                overshoot: 725,
802                ascender: 725,
803                descender: -230,
804                zones: BlueZones::TOP,
805            },
806            UnscaledBlue {
807                position: 0,
808                overshoot: -10,
809                ascender: 725,
810                descender: -10,
811                zones: BlueZones::default(),
812            },
813            UnscaledBlue {
814                position: 760,
815                overshoot: 760,
816                ascender: 770,
817                descender: -240,
818                zones: BlueZones::TOP,
819            },
820            UnscaledBlue {
821                position: 536,
822                overshoot: 546,
823                ascender: 546,
824                descender: -10,
825                zones: BlueZones::TOP | BlueZones::ADJUSTMENT,
826            },
827            UnscaledBlue {
828                position: 0,
829                overshoot: -10,
830                ascender: 546,
831                descender: -10,
832                zones: BlueZones::default(),
833            },
834            UnscaledBlue {
835                position: -240,
836                overshoot: -240,
837                ascender: 760,
838                descender: -240,
839                zones: BlueZones::default(),
840            },
841        ];
842        assert_eq!(values, &expected);
843    }
844
845    #[test]
846    fn hebrew_long_blues() {
847        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
848        let shaper = Shaper::new(&font, ShaperMode::Nominal);
849        // Hebrew triggers "long" blue code path
850        let style = &style::STYLE_CLASSES[super::StyleClass::HEBR];
851        let blues = super::compute_default_blues(&shaper, &[], style);
852        let values = blues.as_slice();
853        assert_eq!(values.len(), 3);
854        let expected = [
855            UnscaledBlue {
856                position: 592,
857                overshoot: 592,
858                ascender: 647,
859                descender: -240,
860                zones: BlueZones::TOP,
861            },
862            UnscaledBlue {
863                position: 0,
864                overshoot: -9,
865                ascender: 647,
866                descender: -9,
867                zones: BlueZones::default(),
868            },
869            UnscaledBlue {
870                position: -240,
871                overshoot: -240,
872                ascender: 647,
873                descender: -240,
874                zones: BlueZones::default(),
875            },
876        ];
877        assert_eq!(values, &expected);
878    }
879
880    #[test]
881    fn cjk_blues() {
882        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
883        let shaper = Shaper::new(&font, ShaperMode::Nominal);
884        let style = &style::STYLE_CLASSES[super::StyleClass::HANI];
885        let blues = super::compute_cjk_blues(&shaper, &[], style);
886        let values = blues[1].as_slice();
887        let expected = [
888            UnscaledBlue {
889                position: 837,
890                overshoot: 824,
891                ascender: 0,
892                descender: 0,
893                zones: BlueZones::TOP,
894            },
895            UnscaledBlue {
896                position: -78,
897                overshoot: -66,
898                ascender: 0,
899                descender: 0,
900                zones: BlueZones::default(),
901            },
902        ];
903        assert_eq!(values, &expected);
904    }
905
906    #[test]
907    fn c2sc_shaped_blues() {
908        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
909        let shaper = Shaper::new(&font, ShaperMode::BestEffort);
910        let style = &style::STYLE_CLASSES[super::StyleClass::LATN_C2SC];
911        let blues = super::compute_default_blues(&shaper, &[], style);
912        let values = blues.as_slice();
913        // Captured from FreeType with HarfBuzz enabled
914        let expected = [
915            UnscaledBlue {
916                position: 571,
917                overshoot: 571,
918                ascender: 571,
919                descender: 0,
920                zones: BlueZones::TOP,
921            },
922            UnscaledBlue {
923                position: 0,
924                overshoot: 0,
925                ascender: 571,
926                descender: 0,
927                zones: BlueZones::default(),
928            },
929        ];
930        assert_eq!(values, &expected);
931    }
932
933    /// Avoid subtraction overflow raised in
934    /// <https://github.com/googlefonts/fontations/issues/1218>
935    #[test]
936    fn long_segment_len_avoid_overflow() {
937        // Test font in issue above triggers overflow with
938        // first = 22, last = 0, contour_last = 22 (all inclusive).
939        // FreeType succeeds on this with suspicious signed
940        // arithmetic and we should too with our code that
941        // takes the boundary into account
942        assert!(satisfies_min_long_segment_len(22, 0, 22));
943    }
944
945    #[test]
946    fn cycle_iter_forward() {
947        let items = [0, 1, 2, 3, 4, 5, 6, 7];
948        let from_5 = super::cycle_forward(&items, 5)
949            .map(|(_, val)| *val)
950            .collect::<Vec<_>>();
951        assert_eq!(from_5, &[6, 7, 0, 1, 2, 3, 4, 5]);
952        let from_last = super::cycle_forward(&items, 7)
953            .map(|(_, val)| *val)
954            .collect::<Vec<_>>();
955        assert_eq!(from_last, &items);
956        // Don't panic on empty slice
957        let _ = super::cycle_forward::<i32>(&[], 5).count();
958    }
959
960    #[test]
961    fn cycle_iter_backward() {
962        let items = [0, 1, 2, 3, 4, 5, 6, 7];
963        let from_5 = super::cycle_backward(&items, 5)
964            .map(|(_, val)| *val)
965            .collect::<Vec<_>>();
966        assert_eq!(from_5, &[4, 3, 2, 1, 0, 7, 6, 5]);
967        let from_0 = super::cycle_backward(&items, 0)
968            .map(|(_, val)| *val)
969            .collect::<Vec<_>>();
970        assert_eq!(from_0, &[7, 6, 5, 4, 3, 2, 1, 0]);
971        // Don't panic on empty slice
972        let _ = super::cycle_backward::<i32>(&[], 5).count();
973    }
974
975    #[test]
976    fn blue_string_cluster_count_saturates() {
977        let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
978        let shaper = Shaper::new(&font, ShaperMode::Nominal);
979        let style_51 = synthetic_default_style(1, super::BLUE_STRING_MAX_LEN);
980        let style_70 = synthetic_default_style(1, super::BLUE_STRING_MAX_LEN + 19);
981        let blues_51 = super::compute_default_blues(&shaper, &[], &style_51);
982        let blues_70 = super::compute_default_blues(&shaper, &[], &style_70);
983        assert_eq!(blues_70, blues_51);
984    }
985
986    #[test]
987    fn blue_count_saturates_at_max_blues() {
988        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
989        let shaper = Shaper::new(&font, ShaperMode::Nominal);
990        let known_good_blues = style::STYLE_CLASSES[super::StyleClass::LATN].script.blues;
991        let mut repeated = Vec::with_capacity(super::MAX_BLUES + 4);
992        for i in 0..(super::MAX_BLUES + 4) {
993            repeated.push(known_good_blues[i % known_good_blues.len()]);
994        }
995        let style = synthetic_style_with_blues(repeated);
996        let blues = super::compute_default_blues(&shaper, &[], &style);
997        assert_eq!(blues.len(), super::MAX_BLUES);
998    }
999
1000    fn synthetic_default_style(num_blues: usize, clusters_per_blue: usize) -> style::StyleClass {
1001        let cluster = (0..clusters_per_blue)
1002            .map(|_| "o")
1003            .collect::<Vec<_>>()
1004            .join(" ");
1005        let cluster: &'static str = Box::leak(cluster.into_boxed_str());
1006        let mut blues = Vec::with_capacity(num_blues);
1007        for _ in 0..num_blues {
1008            blues.push((cluster, BlueZones::TOP));
1009        }
1010        synthetic_style_with_blues(blues)
1011    }
1012
1013    fn synthetic_style_with_blues(blues: Vec<(&'static str, BlueZones)>) -> style::StyleClass {
1014        let blues: &'static [(&'static str, BlueZones)] = Box::leak(blues.into_boxed_slice());
1015        let script = Box::leak(Box::new(style::ScriptClass {
1016            name: "synthetic",
1017            group: style::ScriptGroup::Default,
1018            tag: Tag::new(b"DFLT"),
1019            hint_top_to_bottom: true,
1020            std_chars: "o",
1021            blues,
1022        }));
1023        style::StyleClass {
1024            name: "synthetic",
1025            index: 0,
1026            script,
1027            feature: None,
1028        }
1029    }
1030}