Skip to main content

skrifa/outline/autohint/topo/
segments.rs

1//! Segment computation and linking.
2//!
3//! A segment is a series of at least two consecutive points that are
4//! appropriately aligned along a coordinate axis.
5//!
6//! The linking stage associates pairs of segments to form stems and
7//! identifies serifs with a post-process pass.
8
9use super::super::{
10    derived_constant,
11    metrics::fixed_div,
12    outline::Outline,
13    style::ScriptGroup,
14    topo::{Axis, Dimension, Segment, TopoFlags},
15};
16use raw::tables::glyf::PointFlags;
17
18// Bounds for score, position and coordinate values.
19// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1598>
20const MAX_SCORE: i32 = 32000;
21const MIN_SCORE: i32 = -32000;
22
23/// Computes segments for the Latin writing system.
24///
25/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1537>
26pub(crate) fn compute_segments(
27    outline: &mut Outline,
28    axis: &mut Axis,
29    _group: ScriptGroup,
30) -> bool {
31    assign_point_uvs(outline, axis.dim);
32    if !build_segments(outline, axis) {
33        return false;
34    }
35    adjust_segment_heights(outline, axis);
36    // This is never actually executed due to a bug in FreeType
37    // See point 2 at <https://github.com/googlefonts/fontations/issues/1129>
38    // if group != ScriptGroup::Default {
39    //     _detect_round_segments_cjk(outline, axis);
40    // }
41    true
42}
43
44/// Link segments to form stems and serifs.
45///
46/// If `max_width` is provided, use it to refine the scoring function.
47///
48/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1990>
49pub(crate) fn link_segments(
50    outline: &Outline,
51    axis: &mut Axis,
52    scale: i32,
53    group: ScriptGroup,
54    max_width: Option<i32>,
55) {
56    if group == ScriptGroup::Default {
57        link_segments_default(outline, axis, max_width);
58    } else {
59        link_segments_cjk(outline, axis, scale)
60    }
61}
62
63/// Link segments to form stems and serifs.
64///
65/// If `max_width` is provided, use it to refine the scoring function.
66///
67/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1990>
68fn link_segments_default(outline: &Outline, axis: &mut Axis, max_width: Option<i32>) {
69    let max_width = max_width.unwrap_or_default();
70    // Heuristic value to set up a minimum for overlapping
71    let len_threshold = derived_constant(outline.units_per_em, 8).max(1);
72    // Heuristic value to weight lengths
73    let len_score = derived_constant(outline.units_per_em, 6000);
74    // Heuristic value to weight distances (not a latin constant since
75    // it works on multiples of stem width)
76    let dist_score = 3000;
77    // Compare each segment to the others.. O(n^2)
78    let segments = axis.segments.as_mut_slice();
79    for ix1 in 0..segments.len() {
80        let seg1 = segments[ix1];
81        if seg1.dir != axis.major_dir {
82            continue;
83        }
84        let pos1 = seg1.pos as i32;
85        // Search for stems having opposite directions with seg1 to the
86        // "left" of seg2
87        for ix2 in 0..segments.len() {
88            let seg1 = segments[ix1];
89            let seg2 = segments[ix2];
90            let pos2 = seg2.pos as i32;
91            if seg1.dir.is_opposite(seg2.dir) && pos2 > pos1 {
92                // Compute distance between the segments
93                // Note: the min/max functions chosen here are intentional
94                let min = seg1.min_coord.max(seg2.min_coord) as i32;
95                let max = seg1.max_coord.min(seg2.max_coord) as i32;
96                // Compute maximum coordinate difference or how much they
97                // overlap
98                let len = max - min;
99                if len >= len_threshold {
100                    // verbatim from FreeType:
101                    // "The score is the sum of two demerits indicating the
102                    //  `badness' of a fit, measured along the segments' main axis
103                    //  and orthogonal to it, respectively.
104                    //
105                    // - The less overlapping along the main axis, the worse it
106                    //   is, causing a larger demerit.
107                    //
108                    // - The nearer the orthogonal distance to a stem width, the
109                    //   better it is, causing a smaller demerit.  For simplicity,
110                    //   however, we only increase the demerit for values that
111                    //   exceed the largest stem width."
112                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2054>
113                    let dist = pos2 - pos1;
114                    let dist_demerit = if max_width != 0 {
115                        // Distance demerits are based on multiples of max_width
116                        let delta = (dist << 10) / max_width - (1 << 10);
117                        if delta > 10_000 {
118                            MAX_SCORE
119                        } else if delta > 0 {
120                            delta * delta / dist_score
121                        } else {
122                            0
123                        }
124                    } else {
125                        dist
126                    };
127                    let score = dist_demerit + len_score / len;
128                    if score < seg1.score {
129                        let seg1 = &mut segments[ix1];
130                        seg1.score = score;
131                        seg1.link_ix = Some(ix2 as u16);
132                    }
133                    if score < seg2.score {
134                        let seg2 = &mut segments[ix2];
135                        seg2.score = score;
136                        seg2.link_ix = Some(ix1 as u16);
137                    }
138                }
139            }
140        }
141    }
142    // Now compute "serif" segments
143    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2109>
144    for ix1 in 0..segments.len() {
145        let Some(ix2) = segments[ix1].link_ix else {
146            continue;
147        };
148        let seg2_link = segments[ix2 as usize].link_ix;
149        if seg2_link != Some(ix1 as u16) {
150            let seg1 = &mut segments[ix1];
151            seg1.link_ix = None;
152            seg1.serif_ix = seg2_link;
153        }
154    }
155}
156
157/// Link segments to form stems and serifs for the CJK script group.
158///
159/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L848>
160fn link_segments_cjk(outline: &Outline, axis: &mut Axis, scale: i32) {
161    // Heuristic value to set up a minimum for overlapping
162    let len_threshold = derived_constant(outline.units_per_em, 8);
163    let dist_threshold = fixed_div(64 * 3, scale);
164    // Compare each segment to the others.. O(n^2)
165    let segments = axis.segments.as_mut_slice();
166    for ix1 in 0..segments.len() {
167        let seg1 = segments[ix1];
168        if seg1.dir != axis.major_dir {
169            continue;
170        }
171        let pos1 = seg1.pos as i32;
172        // Search for stems having opposite directions with seg1 to the
173        // "left" of seg2
174        for ix2 in 0..segments.len() {
175            let seg1 = segments[ix1];
176            let seg2 = segments[ix2];
177            if ix1 == ix2 || !seg1.dir.is_opposite(seg2.dir) {
178                continue;
179            }
180            let pos2 = seg2.pos as i32;
181            let dist = pos2 - pos1;
182            if dist < 0 {
183                continue;
184            }
185            // Compute distance between the segments
186            // Note: the min/max functions chosen here are intentional
187            let min = seg1.min_coord.max(seg2.min_coord) as i32;
188            let max = seg1.max_coord.min(seg2.max_coord) as i32;
189            // Compute maximum coordinate difference or how much they
190            // overlap
191            let len = max - min;
192            if len >= len_threshold {
193                let check_seg = |seg: &Segment| {
194                    // Some more magic heuristics...
195                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L896>
196                    (dist * 8 < seg.score * 9) && (dist * 8 < seg.score * 7 || seg.len < len)
197                };
198                if check_seg(&seg1) {
199                    let seg = &mut segments[ix1];
200                    seg.score = dist;
201                    seg.len = len;
202                    seg.link_ix = Some(ix2 as _);
203                }
204                if check_seg(&seg2) {
205                    let seg = &mut segments[ix2];
206                    seg.score = dist;
207                    seg.len = len;
208                    seg.link_ix = Some(ix1 as _);
209                }
210            }
211        }
212    }
213    // Now compute "serif" segments
214    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L917>
215    for ix1 in 0..segments.len() {
216        let seg1 = segments[ix1];
217        if seg1.score >= dist_threshold {
218            continue;
219        }
220        let Some(link1) = seg1.link(segments).copied() else {
221            continue;
222        };
223        // Unwrap is fine because we checked for existence above
224        let link1_ix = seg1.link_ix.unwrap() as usize;
225        if link1.link_ix != Some(ix1 as u16) || link1.pos <= seg1.pos {
226            continue;
227        }
228        for ix2 in 0..segments.len() {
229            let seg2 = segments[ix2];
230            if seg2.pos > seg1.pos || ix1 == ix2 {
231                continue;
232            }
233            let Some(link2) = seg2.link(segments).copied() else {
234                continue;
235            };
236            if link2.link_ix != Some(ix2 as u16) || link2.pos < link1.pos {
237                continue;
238            }
239            if seg1.pos == seg2.pos && link1.pos == link2.pos {
240                continue;
241            }
242            if seg2.score <= seg1.score || seg1.score * 4 <= seg2.score {
243                continue;
244            }
245            if seg1.len >= seg2.len * 3 {
246                // Again, we definitely have a valid link2
247                let link2_ix = seg2.link_ix.unwrap() as usize;
248                for seg in segments.iter_mut() {
249                    let link_ix = seg.link_ix;
250                    if link_ix == Some(ix2 as u16) {
251                        seg.link_ix = None;
252                        seg.serif_ix = Some(link1_ix as u16);
253                    } else if link_ix == Some(link2_ix as u16) {
254                        seg.link_ix = None;
255                        seg.serif_ix = Some(ix1 as u16);
256                    }
257                }
258            } else {
259                segments[ix1].link_ix = None;
260                segments[link1_ix].link_ix = None;
261                break;
262            }
263        }
264    }
265    for ix1 in 0..segments.len() {
266        let seg1 = segments[ix1];
267        let Some(seg2) = seg1.link(segments).copied() else {
268            continue;
269        };
270        if seg2.link_ix != Some(ix1 as u16) {
271            segments[ix1].link_ix = None;
272            if seg2.score < dist_threshold || seg1.score < seg2.score * 4 {
273                segments[ix1].serif_ix = seg2.link_ix;
274            }
275        }
276    }
277}
278
279/// Set the (u, v) values to font unit coords for each point depending
280/// on the axis dimension.
281///
282/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1562>
283fn assign_point_uvs(outline: &mut Outline, dim: Dimension) {
284    if dim == Dimension::Horizontal {
285        for point in &mut outline.points {
286            point.u = point.fx;
287            point.v = point.fy;
288        }
289    } else {
290        for point in &mut outline.points {
291            point.u = point.fy;
292            point.v = point.fx;
293        }
294    }
295}
296
297/// Build the set of segments for each contour.
298///
299/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1588>
300fn build_segments(outline: &mut Outline, axis: &mut Axis) -> bool {
301    let flat_threshold = outline.units_per_em / 14;
302    axis.segments.clear();
303    let major_dir = axis.major_dir.normalize();
304    let mut segment_dir = major_dir;
305    let points = outline.points.as_mut_slice();
306    for contour in &outline.contours {
307        let is_single_point_contour = contour.range().len() == 1;
308        let mut point_ix = contour.first();
309        let mut last_ix = contour.prev(point_ix);
310        let mut state = State::default();
311        let mut prev_state = state;
312        let mut prev_segment_ix: Option<usize> = None;
313        let mut segment_ix = 0;
314        // Check if we're starting on an edge and if so, find
315        // the starting point
316        if points[point_ix].out_dir.is_same_axis(major_dir)
317            && points[last_ix].out_dir.is_same_axis(major_dir)
318        {
319            last_ix = point_ix;
320            loop {
321                point_ix = contour.prev(point_ix);
322                if !points[point_ix].out_dir.is_same_axis(major_dir) {
323                    point_ix = contour.next(point_ix);
324                    break;
325                }
326                if point_ix == last_ix {
327                    break;
328                }
329            }
330        }
331        last_ix = point_ix;
332        let mut on_edge = false;
333        let mut passed = false;
334        loop {
335            if on_edge {
336                // Get min and max position
337                let point = points[point_ix];
338                state.min_pos = state.min_pos.min(point.u);
339                state.max_pos = state.max_pos.max(point.u);
340                // Get min and max coordinate and flags
341                let v = point.v;
342                if v < state.min_coord {
343                    state.min_coord = v;
344                    state.min_flags = point.flags;
345                }
346                if v > state.max_coord {
347                    state.max_coord = v;
348                    state.max_flags = point.flags;
349                }
350                // Get min and max coord of on curve points
351                if point.is_on_curve() {
352                    state.min_on_coord = state.min_on_coord.min(point.v);
353                    state.max_on_coord = state.max_on_coord.max(point.v);
354                }
355                if point.out_dir != segment_dir || point_ix == last_ix {
356                    prev_segment_ix.take_if(|idx| {
357                        axis.segments[segment_ix].first_ix != axis.segments[*idx].last_ix
358                    });
359                    if let Some(prev_segment_ix) = prev_segment_ix {
360                        // The points are the same, so merge the segments
361                        let prev_segment = &mut axis.segments[prev_segment_ix];
362                        if prev_segment.last_point(points).in_dir == point.in_dir {
363                            // We have identical directions; unify segments
364                            // and update constraints
365                            state.min_pos = prev_state.min_pos.min(state.min_pos);
366                            state.max_pos = prev_state.max_pos.max(state.max_pos);
367                            if prev_state.min_coord < state.min_coord {
368                                state.min_coord = prev_state.min_coord;
369                                state.min_flags = prev_state.min_flags;
370                            }
371                            if prev_state.max_coord > state.max_coord {
372                                state.max_coord = prev_state.max_coord;
373                                state.max_flags = prev_state.max_flags;
374                            }
375                            state.min_on_coord = prev_state.min_on_coord.min(state.min_on_coord);
376                            state.max_on_coord = prev_state.max_on_coord.max(state.max_on_coord);
377                            prev_segment.last_ix = point_ix as u16;
378                            state.apply_to_segment(prev_segment, flat_threshold);
379                        } else {
380                            // We have different directions; use the
381                            // properties of the longer segment
382                            if (prev_state.max_coord - prev_state.min_coord).abs()
383                                > (state.max_coord - state.min_coord).abs()
384                            {
385                                // Discard current segment
386                                prev_state.min_pos = prev_state.min_pos.min(state.min_pos);
387                                prev_state.max_pos = prev_state.max_pos.max(state.max_pos);
388                                prev_segment.last_ix = point_ix as u16;
389                                prev_segment.pos =
390                                    compute_mid_pos(prev_state.min_pos, prev_state.max_pos);
391                                prev_segment.delta =
392                                    compute_mid_delta(prev_state.min_pos, prev_state.max_pos);
393                            } else {
394                                // Discard previous segment
395                                state.min_pos = state.min_pos.min(prev_state.min_pos);
396                                state.max_pos = state.max_pos.max(prev_state.max_pos);
397                                let mut segment = axis.segments[segment_ix];
398                                segment.last_ix = point_ix as u16;
399                                state.apply_to_segment(&mut segment, flat_threshold);
400                                axis.segments[prev_segment_ix] = segment;
401                                prev_state = state;
402                            }
403                        }
404                        axis.segments.pop();
405                    } else {
406                        // The points are different signifying that we are
407                        // leaving an edge, so create a new segment
408                        let segment = &mut axis.segments[segment_ix];
409                        segment.last_ix = point_ix as u16;
410                        state.apply_to_segment(segment, flat_threshold);
411                        prev_segment_ix = Some(segment_ix);
412                        prev_state = state;
413                    };
414                    on_edge = false;
415                }
416            }
417            if point_ix == last_ix {
418                if passed {
419                    break;
420                }
421                passed = true;
422            }
423            let point = points[point_ix];
424            if !on_edge && (point.out_dir.is_same_axis(major_dir) || is_single_point_contour) {
425                if axis.segments.len() > 1000 {
426                    axis.segments.clear();
427                    return false;
428                }
429                segment_ix = axis.segments.len();
430                segment_dir = point.out_dir;
431                let mut segment = Segment {
432                    dir: segment_dir,
433                    first_ix: point_ix as u16,
434                    last_ix: point_ix as u16,
435                    score: MAX_SCORE,
436                    ..Default::default()
437                };
438                state.min_pos = point.u;
439                state.max_pos = point.u;
440                state.min_coord = point.v;
441                state.max_coord = point.v;
442                state.min_flags = point.flags;
443                state.max_flags = point.flags;
444                if !point.is_on_curve() {
445                    state.min_on_coord = MAX_SCORE;
446                    state.max_on_coord = MIN_SCORE;
447                } else {
448                    state.min_on_coord = point.v;
449                    state.max_on_coord = point.v;
450                }
451                on_edge = true;
452                if is_single_point_contour {
453                    segment.pos = state.min_pos as i16;
454                    if !point.is_on_curve() {
455                        segment.flags |= TopoFlags::ROUND;
456                    }
457                    segment.min_coord = point.v as i16;
458                    segment.max_coord = point.v as i16;
459                    segment.height = 0;
460                    on_edge = false;
461                }
462                axis.segments.push(segment);
463            }
464            point_ix = contour.next(point_ix);
465        }
466    }
467    true
468}
469
470/// Slightly increase the height of segments when it makes sense to better
471/// detect and ignore serifs.
472///
473/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1933>
474fn adjust_segment_heights(outline: &mut Outline, axis: &mut Axis) {
475    let points = outline.points.as_slice();
476    for segment in &mut axis.segments {
477        let first = segment.first_point(points);
478        let last = segment.last_point(points);
479        let prev = &points[first.prev()];
480        let next = &points[last.next()];
481        if first.v < last.v {
482            if prev.v < first.v {
483                segment.adjust_height(first.v, prev.v);
484            }
485            if next.v > last.v {
486                segment.adjust_height(next.v, last.v);
487            }
488        } else {
489            if prev.v > first.v {
490                segment.adjust_height(prev.v, first.v);
491            }
492            if next.v < last.v {
493                segment.adjust_height(last.v, next.v);
494            }
495        }
496    }
497}
498
499/// Performs the additional step of detecting round segments for the CJK script
500/// group.
501///
502/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L818>
503fn _detect_round_segments_cjk(outline: &mut Outline, axis: &mut Axis) {
504    let points = outline.points.as_slice();
505    // A segment is considered round if it doesn't have successive on-curve
506    // points
507    for segment in &mut axis.segments {
508        segment.flags &= !TopoFlags::ROUND;
509        let mut point_ix = segment.first();
510        let last_ix = segment.last();
511        let first_point = &points[point_ix];
512        let mut is_prev_on_curve = first_point.is_on_curve();
513        point_ix = first_point.next();
514        loop {
515            let point = &points[point_ix];
516            let is_on_curve = point.is_on_curve();
517            if is_prev_on_curve && is_on_curve {
518                // Two on-curves in a row means we're not a round segment
519                break;
520            }
521            is_prev_on_curve = is_on_curve;
522            point_ix = point.next();
523            if point_ix == last_ix {
524                // We've reached the last point without two successive
525                // on-curves so we're round
526                segment.flags |= TopoFlags::ROUND;
527                break;
528            }
529        }
530    }
531}
532
533/// Capture current and previous state while computing segments.
534///
535/// Values measured along a segment (point.v) are called "coordinates" and
536/// values orthogonal to it (point.u) are called "positions"
537#[derive(Copy, Clone)]
538struct State {
539    min_pos: i32,
540    max_pos: i32,
541    min_coord: i32,
542    max_coord: i32,
543    min_flags: PointFlags,
544    max_flags: PointFlags,
545    min_on_coord: i32,
546    max_on_coord: i32,
547}
548
549impl Default for State {
550    fn default() -> Self {
551        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1598>
552        Self {
553            min_pos: MAX_SCORE,
554            max_pos: MIN_SCORE,
555            min_coord: MAX_SCORE,
556            max_coord: MIN_SCORE,
557            min_flags: PointFlags::default(),
558            max_flags: PointFlags::default(),
559            min_on_coord: MAX_SCORE,
560            max_on_coord: MIN_SCORE,
561        }
562    }
563}
564
565impl State {
566    fn apply_to_segment(&self, segment: &mut Segment, flat_threshold: i32) {
567        segment.pos = compute_mid_pos(self.min_pos, self.max_pos);
568        segment.delta = compute_mid_delta(self.min_pos, self.max_pos);
569        // A segment is round if either end point is a
570        // control and the length of the on points in
571        // between fits within a heuristic limit.
572        if (!self.min_flags.is_on_curve() || !self.max_flags.is_on_curve())
573            && (self.max_on_coord.wrapping_sub(self.min_on_coord)) < flat_threshold
574        {
575            segment.flags |= TopoFlags::ROUND;
576        }
577        segment.min_coord = self.min_coord as i16;
578        segment.max_coord = self.max_coord as i16;
579        segment.height = self.max_coord.wrapping_sub(self.min_coord) as i16;
580    }
581}
582
583/// Compute the mid position between two values, using wrapping arithmetic.
584fn compute_mid_pos(min: i32, max: i32) -> i16 {
585    ((min.wrapping_add(max)) >> 1) as i16
586}
587
588/// Compute the mid delta between two values, using wrapping arithmetic.
589fn compute_mid_delta(min: i32, max: i32) -> i16 {
590    ((max.wrapping_sub(min)) >> 1) as i16
591}
592
593#[cfg(test)]
594mod tests {
595    use super::{super::super::outline::Direction, *};
596    use crate::MetadataProvider;
597    use raw::{types::GlyphId, FontRef};
598
599    #[test]
600    fn horizontal_segments() {
601        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
602        let glyphs = font.outline_glyphs();
603        let glyph = glyphs.get(GlyphId::new(8)).unwrap();
604        let mut outline = Outline::default();
605        outline.fill(&glyph, &[], Default::default()).unwrap();
606        let mut axis = Axis::new(Dimension::Horizontal, outline.orientation);
607        compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
608        link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
609        let segments = retain_segment_test_fields(&axis.segments);
610        let expected = [
611            Segment {
612                flags: TopoFlags::NORMAL,
613                dir: Direction::Up,
614                pos: 55,
615                delta: 0,
616                min_coord: 26,
617                max_coord: 360,
618                height: 372,
619                link_ix: Some(3),
620                serif_ix: None,
621                ..Default::default()
622            },
623            Segment {
624                flags: TopoFlags::NORMAL,
625                dir: Direction::Up,
626                pos: 112,
627                delta: 0,
628                min_coord: 481,
629                max_coord: 504,
630                height: 34,
631                link_ix: Some(2),
632                serif_ix: None,
633                ..Default::default()
634            },
635            Segment {
636                flags: TopoFlags::NORMAL,
637                dir: Direction::Down,
638                pos: 168,
639                delta: 0,
640                min_coord: 483,
641                max_coord: 504,
642                height: 26,
643                link_ix: Some(1),
644                serif_ix: None,
645                ..Default::default()
646            },
647            Segment {
648                flags: TopoFlags::NORMAL,
649                dir: Direction::Down,
650                pos: 109,
651                delta: 0,
652                min_coord: 109,
653                max_coord: 366,
654                height: 288,
655                link_ix: Some(0),
656                serif_ix: None,
657                ..Default::default()
658            },
659            Segment {
660                flags: TopoFlags::NORMAL,
661                dir: Direction::Up,
662                pos: 453,
663                delta: 0,
664                min_coord: 169,
665                max_coord: 432,
666                height: 304,
667                link_ix: Some(7),
668                serif_ix: None,
669                ..Default::default()
670            },
671            Segment {
672                flags: TopoFlags::ROUND,
673                dir: Direction::Up,
674                pos: 62,
675                delta: 0,
676                min_coord: 517,
677                max_coord: 566,
678                height: 76,
679                link_ix: None,
680                serif_ix: None,
681                ..Default::default()
682            },
683            Segment {
684                flags: TopoFlags::ROUND,
685                dir: Direction::Down,
686                pos: 103,
687                delta: 0,
688                min_coord: 619,
689                max_coord: 647,
690                height: 41,
691                link_ix: None,
692                serif_ix: None,
693                ..Default::default()
694            },
695            Segment {
696                flags: TopoFlags::NORMAL,
697                dir: Direction::Down,
698                pos: 507,
699                delta: 0,
700                min_coord: 40,
701                max_coord: 485,
702                height: 498,
703                link_ix: Some(4),
704                serif_ix: None,
705                ..Default::default()
706            },
707        ];
708        assert_eq!(segments, &expected);
709    }
710
711    #[test]
712    fn vertical_segments() {
713        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
714        let glyphs = font.outline_glyphs();
715        let glyph = glyphs.get(GlyphId::new(8)).unwrap();
716        let mut outline = Outline::default();
717        outline.fill(&glyph, &[], Default::default()).unwrap();
718        let mut axis = Axis::new(Dimension::Vertical, outline.orientation);
719        compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
720        link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
721        let segments = retain_segment_test_fields(&axis.segments);
722        let expected = [
723            Segment {
724                flags: TopoFlags::NORMAL,
725                dir: Direction::Left,
726                pos: 0,
727                delta: 0,
728                min_coord: 85,
729                max_coord: 470,
730                height: 418,
731                link_ix: Some(2),
732                serif_ix: None,
733                ..Default::default()
734            },
735            Segment {
736                flags: TopoFlags::NORMAL,
737                dir: Direction::Right,
738                pos: 504,
739                delta: 0,
740                min_coord: 112,
741                max_coord: 168,
742                height: 56,
743                link_ix: Some(3),
744                serif_ix: None,
745                ..Default::default()
746            },
747            Segment {
748                flags: TopoFlags::NORMAL,
749                dir: Direction::Right,
750                pos: 109,
751                delta: 0,
752                min_coord: 109,
753                max_coord: 427,
754                height: 327,
755                link_ix: Some(0),
756                serif_ix: None,
757                ..Default::default()
758            },
759            Segment {
760                flags: TopoFlags::NORMAL,
761                dir: Direction::Left,
762                pos: 483,
763                delta: 0,
764                min_coord: 86,
765                max_coord: 400,
766                height: 352,
767                link_ix: Some(1),
768                serif_ix: None,
769                ..Default::default()
770            },
771            Segment {
772                flags: TopoFlags::NORMAL,
773                dir: Direction::Right,
774                pos: 647,
775                delta: 0,
776                min_coord: 76,
777                max_coord: 103,
778                height: 29,
779                link_ix: None,
780                serif_ix: Some(1),
781                ..Default::default()
782            },
783            Segment {
784                flags: TopoFlags::NORMAL,
785                dir: Direction::Right,
786                pos: 592,
787                delta: 0,
788                min_coord: 131,
789                max_coord: 437,
790                height: 346,
791                link_ix: None,
792                serif_ix: Some(1),
793                ..Default::default()
794            },
795        ];
796        assert_eq!(segments, &expected);
797    }
798
799    #[test]
800    fn cjk_horizontal_segments() {
801        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
802        let glyphs = font.outline_glyphs();
803        let glyph = glyphs.get(GlyphId::new(9)).unwrap();
804        let mut outline = Outline::default();
805        outline.fill(&glyph, &[], Default::default()).unwrap();
806        let mut axis = Axis::new(Dimension::Horizontal, outline.orientation);
807        compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
808        link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
809        let segments = retain_segment_test_fields(&axis.segments);
810        let expected = [
811            Segment {
812                flags: TopoFlags::NORMAL,
813                dir: Direction::Down,
814                pos: 731,
815                delta: 0,
816                min_coord: 155,
817                max_coord: 676,
818                height: 524,
819                link_ix: Some(1),
820                serif_ix: None,
821                ..Default::default()
822            },
823            Segment {
824                flags: TopoFlags::NORMAL,
825                dir: Direction::Up,
826                pos: 670,
827                delta: 0,
828                min_coord: 133,
829                max_coord: 712,
830                height: 579,
831                link_ix: Some(0),
832                serif_ix: None,
833                ..Default::default()
834            },
835            Segment {
836                flags: TopoFlags::NORMAL,
837                dir: Direction::Down,
838                pos: 458,
839                delta: 0,
840                min_coord: 741,
841                max_coord: 757,
842                height: 88,
843                link_ix: None,
844                serif_ix: None,
845                ..Default::default()
846            },
847            Segment {
848                flags: TopoFlags::NORMAL,
849                dir: Direction::Down,
850                pos: 911,
851                delta: 0,
852                min_coord: -9,
853                max_coord: 791,
854                height: 821,
855                link_ix: Some(5),
856                serif_ix: None,
857                ..Default::default()
858            },
859            Segment {
860                flags: TopoFlags::NORMAL,
861                dir: Direction::Up,
862                pos: 693,
863                delta: 0,
864                min_coord: -7,
865                max_coord: 9,
866                height: 18,
867                link_ix: None,
868                serif_ix: Some(5),
869                ..Default::default()
870            },
871            Segment {
872                flags: TopoFlags::NORMAL,
873                dir: Direction::Up,
874                pos: 849,
875                delta: 0,
876                min_coord: 11,
877                max_coord: 829,
878                height: 823,
879                link_ix: Some(3),
880                serif_ix: None,
881                ..Default::default()
882            },
883            Segment {
884                flags: TopoFlags::NORMAL,
885                dir: Direction::Down,
886                pos: 569,
887                delta: 0,
888                min_coord: 547,
889                max_coord: 576,
890                height: 29,
891                link_ix: None,
892                serif_ix: None,
893                ..Default::default()
894            },
895            Segment {
896                flags: TopoFlags::NORMAL,
897                dir: Direction::Down,
898                pos: 201,
899                delta: 0,
900                min_coord: -57,
901                max_coord: 540,
902                height: 599,
903                link_ix: Some(8),
904                serif_ix: None,
905                ..Default::default()
906            },
907            Segment {
908                flags: TopoFlags::NORMAL,
909                dir: Direction::Up,
910                pos: 138,
911                delta: 0,
912                min_coord: -78,
913                max_coord: 543,
914                height: 640,
915                link_ix: Some(7),
916                serif_ix: None,
917                ..Default::default()
918            },
919        ];
920        assert_eq!(segments, &expected);
921    }
922
923    #[test]
924    fn cjk_vertical_segments() {
925        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
926        let glyphs = font.outline_glyphs();
927        let glyph = glyphs.get(GlyphId::new(9)).unwrap();
928        let mut outline = Outline::default();
929        outline.fill(&glyph, &[], Default::default()).unwrap();
930        let mut axis = Axis::new(Dimension::Vertical, outline.orientation);
931        compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
932        link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
933        let segments = retain_segment_test_fields(&axis.segments);
934        let expected = [
935            Segment {
936                flags: TopoFlags::NORMAL,
937                dir: Direction::Right,
938                pos: 758,
939                delta: 0,
940                min_coord: 280,
941                max_coord: 545,
942                height: 288,
943                link_ix: Some(1),
944                serif_ix: None,
945                ..Default::default()
946            },
947            Segment {
948                flags: TopoFlags::NORMAL,
949                dir: Direction::Left,
950                pos: 729,
951                delta: 0,
952                min_coord: 288,
953                max_coord: 674,
954                height: 391,
955                link_ix: Some(0),
956                serif_ix: None,
957                ..Default::default()
958            },
959            Segment {
960                flags: TopoFlags::ROUND,
961                dir: Direction::Left,
962                pos: 133,
963                delta: 0,
964                min_coord: 670,
965                max_coord: 693,
966                height: 34,
967                link_ix: None,
968                serif_ix: None,
969                ..Default::default()
970            },
971            Segment {
972                flags: TopoFlags::NORMAL,
973                dir: Direction::Right,
974                pos: 757,
975                delta: 0,
976                min_coord: 393,
977                max_coord: 458,
978                height: 70,
979                link_ix: None,
980                serif_ix: Some(0),
981                ..Default::default()
982            },
983            Segment {
984                flags: TopoFlags::ROUND,
985                dir: Direction::Right,
986                pos: 3,
987                delta: 2,
988                min_coord: 727,
989                max_coord: 838,
990                height: 133,
991                link_ix: None,
992                serif_ix: None,
993                ..Default::default()
994            },
995            Segment {
996                flags: TopoFlags::NORMAL,
997                dir: Direction::Right,
998                pos: 576,
999                delta: 0,
1000                min_coord: 397,
1001                max_coord: 569,
1002                height: 177,
1003                link_ix: Some(7),
1004                serif_ix: None,
1005                ..Default::default()
1006            },
1007            Segment {
1008                flags: TopoFlags::NORMAL,
1009                dir: Direction::Left,
1010                pos: 547,
1011                delta: 0,
1012                min_coord: 387,
1013                max_coord: 569,
1014                height: 182,
1015                link_ix: None,
1016                serif_ix: Some(7),
1017                ..Default::default()
1018            },
1019            Segment {
1020                flags: TopoFlags::NORMAL,
1021                dir: Direction::Left,
1022                pos: 576,
1023                delta: 0,
1024                min_coord: 536,
1025                max_coord: 546,
1026                height: 10,
1027                link_ix: Some(5),
1028                serif_ix: None,
1029                ..Default::default()
1030            },
1031            Segment {
1032                flags: TopoFlags::ROUND,
1033                dir: Direction::Left,
1034                pos: -78,
1035                delta: 0,
1036                min_coord: 138,
1037                max_coord: 161,
1038                height: 34,
1039                link_ix: None,
1040                serif_ix: None,
1041                ..Default::default()
1042            },
1043            Segment {
1044                flags: TopoFlags::ROUND,
1045                dir: Direction::Left,
1046                pos: 788,
1047                delta: 0,
1048                min_coord: 262,
1049                max_coord: 294,
1050                height: 46,
1051                link_ix: None,
1052                serif_ix: None,
1053                ..Default::default()
1054            },
1055        ];
1056        assert_eq!(segments, &expected);
1057    }
1058
1059    // Retain the fields that are valid and comparable after
1060    // the segment pass.
1061    fn retain_segment_test_fields(segments: &[Segment]) -> Vec<Segment> {
1062        segments
1063            .iter()
1064            .map(|segment| Segment {
1065                flags: segment.flags,
1066                dir: segment.dir,
1067                pos: segment.pos,
1068                delta: segment.delta,
1069                min_coord: segment.min_coord,
1070                max_coord: segment.max_coord,
1071                height: segment.height,
1072                link_ix: segment.link_ix,
1073                serif_ix: segment.serif_ix,
1074                ..Default::default()
1075            })
1076            .collect()
1077    }
1078
1079    /// OSS Fuzz caught subtract with overflow in State::apply_to_segment.
1080    /// See <https://oss-fuzz.com/testcase-detail/5446493076258816>
1081    /// and <https://issues.oss-fuzz.com/issues/438909305>
1082    #[test]
1083    fn state_apply_overflow() {
1084        let mut segment = Segment::default();
1085        let state = State {
1086            min_coord: MAX_SCORE,
1087            max_coord: MIN_SCORE,
1088            ..Default::default()
1089        };
1090        // Just don't panic with overflow
1091        state.apply_to_segment(&mut segment, 0);
1092    }
1093
1094    #[test]
1095    fn mid_extreme_values_do_not_panic() {
1096        // Just don't panic with overflow
1097        let _ = compute_mid_pos(i32::MIN, i32::MAX);
1098        let _ = compute_mid_pos(i32::MAX, i32::MIN);
1099        let _ = compute_mid_delta(i32::MIN, i32::MAX);
1100        let _ = compute_mid_delta(i32::MAX, i32::MIN);
1101    }
1102}