Skip to main content

skrifa/outline/autohint/metrics/
blues.rs

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