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