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},
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 == Axis::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                    if prev_segment_ix.is_none()
357                        || axis.segments[segment_ix].first_ix
358                            != axis.segments[prev_segment_ix.unwrap()].last_ix
359                    {
360                        // The points are different signifying that we are
361                        // leaving an edge, so create a new segment
362                        let segment = &mut axis.segments[segment_ix];
363                        segment.last_ix = point_ix as u16;
364                        state.apply_to_segment(segment, flat_threshold);
365                        prev_segment_ix = Some(segment_ix);
366                        prev_state = state;
367                    } else {
368                        // The points are the same, so merge the segments
369                        let prev_segment = &mut axis.segments[prev_segment_ix.unwrap()];
370                        if prev_segment.last_point(points).in_dir == point.in_dir {
371                            // We have identical directions; unify segments
372                            // and update constraints
373                            state.min_pos = prev_state.min_pos.min(state.min_pos);
374                            state.max_pos = prev_state.max_pos.max(state.max_pos);
375                            if prev_state.min_coord < state.min_coord {
376                                state.min_coord = prev_state.min_coord;
377                                state.min_flags = prev_state.min_flags;
378                            }
379                            if prev_state.max_coord > state.max_coord {
380                                state.max_coord = prev_state.max_coord;
381                                state.max_flags = prev_state.max_flags;
382                            }
383                            state.min_on_coord = prev_state.min_on_coord.min(state.min_on_coord);
384                            state.max_on_coord = prev_state.max_on_coord.max(state.max_on_coord);
385                            prev_segment.last_ix = point_ix as u16;
386                            state.apply_to_segment(prev_segment, flat_threshold);
387                        } else {
388                            // We have different directions; use the
389                            // properties of the longer segment
390                            if (prev_state.max_coord - prev_state.min_coord).abs()
391                                > (state.max_coord - state.min_coord).abs()
392                            {
393                                // Discard current segment
394                                prev_state.min_pos = prev_state.min_pos.min(state.min_pos);
395                                prev_state.max_pos = prev_state.max_pos.max(state.max_pos);
396                                prev_segment.last_ix = point_ix as u16;
397                                prev_segment.pos =
398                                    ((prev_state.min_pos + prev_state.max_pos) >> 1) as i16;
399                                prev_segment.delta =
400                                    ((prev_state.max_pos - prev_state.min_pos) >> 1) as i16;
401                            } else {
402                                // Discard previous segment
403                                state.min_pos = state.min_pos.min(prev_state.min_pos);
404                                state.max_pos = state.max_pos.max(prev_state.max_pos);
405                                let mut segment = axis.segments[segment_ix];
406                                segment.last_ix = point_ix as u16;
407                                state.apply_to_segment(&mut segment, flat_threshold);
408                                axis.segments[prev_segment_ix.unwrap()] = segment;
409                                prev_state = state;
410                            }
411                        }
412                        axis.segments.pop();
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 |= Segment::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        fn adjust_height(segment: &mut Segment, v1: i32, v2: i32) {
480            segment.height = (segment.height as i32 + ((v1 - v2) >> 1)) as i16;
481        }
482        let prev = &points[first.prev()];
483        let next = &points[last.next()];
484        if first.v < last.v {
485            if prev.v < first.v {
486                adjust_height(segment, first.v, prev.v);
487            }
488            if next.v > last.v {
489                adjust_height(segment, next.v, last.v);
490            }
491        } else {
492            if prev.v > first.v {
493                adjust_height(segment, prev.v, first.v);
494            }
495            if next.v < last.v {
496                adjust_height(segment, last.v, next.v);
497            }
498        }
499    }
500}
501
502/// Performs the additional step of detecting round segments for the CJK script
503/// group.
504///
505/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L818>
506fn _detect_round_segments_cjk(outline: &mut Outline, axis: &mut Axis) {
507    let points = outline.points.as_slice();
508    // A segment is considered round if it doesn't have successive on-curve
509    // points
510    for segment in &mut axis.segments {
511        segment.flags &= !Segment::ROUND;
512        let mut point_ix = segment.first();
513        let last_ix = segment.last();
514        let first_point = &points[point_ix];
515        let mut is_prev_on_curve = first_point.is_on_curve();
516        point_ix = first_point.next();
517        loop {
518            let point = &points[point_ix];
519            let is_on_curve = point.is_on_curve();
520            if is_prev_on_curve && is_on_curve {
521                // Two on-curves in a row means we're not a round segment
522                break;
523            }
524            is_prev_on_curve = is_on_curve;
525            point_ix = point.next();
526            if point_ix == last_ix {
527                // We've reached the last point without two successive
528                // on-curves so we're round
529                segment.flags |= Segment::ROUND;
530                break;
531            }
532        }
533    }
534}
535
536/// Capture current and previous state while computing segments.
537///
538/// Values measured along a segment (point.v) are called "coordinates" and
539/// values orthogonal to it (point.u) are called "positions"
540#[derive(Copy, Clone)]
541struct State {
542    min_pos: i32,
543    max_pos: i32,
544    min_coord: i32,
545    max_coord: i32,
546    min_flags: PointFlags,
547    max_flags: PointFlags,
548    min_on_coord: i32,
549    max_on_coord: i32,
550}
551
552impl Default for State {
553    fn default() -> Self {
554        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1598>
555        Self {
556            min_pos: MAX_SCORE,
557            max_pos: MIN_SCORE,
558            min_coord: MAX_SCORE,
559            max_coord: MIN_SCORE,
560            min_flags: PointFlags::default(),
561            max_flags: PointFlags::default(),
562            min_on_coord: MAX_SCORE,
563            max_on_coord: MIN_SCORE,
564        }
565    }
566}
567
568impl State {
569    fn apply_to_segment(&self, segment: &mut Segment, flat_threshold: i32) {
570        segment.pos = ((self.min_pos + self.max_pos) >> 1) as i16;
571        segment.delta = ((self.max_pos - self.min_pos) >> 1) as i16;
572        // A segment is round if either end point is a
573        // control and the length of the on points in
574        // between fits within a heuristic limit.
575        if (!self.min_flags.is_on_curve() || !self.max_flags.is_on_curve())
576            && (self.max_on_coord - self.min_on_coord) < flat_threshold
577        {
578            segment.flags |= Segment::ROUND;
579        }
580        segment.min_coord = self.min_coord as i16;
581        segment.max_coord = self.max_coord as i16;
582        segment.height = segment.max_coord - segment.min_coord;
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::{super::super::outline::Direction, *};
589    use crate::MetadataProvider;
590    use raw::{types::GlyphId, FontRef};
591
592    #[test]
593    fn horizontal_segments() {
594        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
595        let glyphs = font.outline_glyphs();
596        let glyph = glyphs.get(GlyphId::new(8)).unwrap();
597        let mut outline = Outline::default();
598        outline.fill(&glyph, Default::default()).unwrap();
599        let mut axis = Axis::new(Axis::HORIZONTAL, outline.orientation);
600        compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
601        link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
602        let segments = retain_segment_test_fields(&axis.segments);
603        let expected = [
604            Segment {
605                flags: 0,
606                dir: Direction::Up,
607                pos: 55,
608                delta: 0,
609                min_coord: 26,
610                max_coord: 360,
611                height: 372,
612                link_ix: Some(3),
613                serif_ix: None,
614                ..Default::default()
615            },
616            Segment {
617                flags: 0,
618                dir: Direction::Up,
619                pos: 112,
620                delta: 0,
621                min_coord: 481,
622                max_coord: 504,
623                height: 34,
624                link_ix: Some(2),
625                serif_ix: None,
626                ..Default::default()
627            },
628            Segment {
629                flags: 0,
630                dir: Direction::Down,
631                pos: 168,
632                delta: 0,
633                min_coord: 483,
634                max_coord: 504,
635                height: 26,
636                link_ix: Some(1),
637                serif_ix: None,
638                ..Default::default()
639            },
640            Segment {
641                flags: 0,
642                dir: Direction::Down,
643                pos: 109,
644                delta: 0,
645                min_coord: 109,
646                max_coord: 366,
647                height: 288,
648                link_ix: Some(0),
649                serif_ix: None,
650                ..Default::default()
651            },
652            Segment {
653                flags: 0,
654                dir: Direction::Up,
655                pos: 453,
656                delta: 0,
657                min_coord: 169,
658                max_coord: 432,
659                height: 304,
660                link_ix: Some(7),
661                serif_ix: None,
662                ..Default::default()
663            },
664            Segment {
665                flags: 1,
666                dir: Direction::Up,
667                pos: 62,
668                delta: 0,
669                min_coord: 517,
670                max_coord: 566,
671                height: 76,
672                link_ix: None,
673                serif_ix: None,
674                ..Default::default()
675            },
676            Segment {
677                flags: 1,
678                dir: Direction::Down,
679                pos: 103,
680                delta: 0,
681                min_coord: 619,
682                max_coord: 647,
683                height: 41,
684                link_ix: None,
685                serif_ix: None,
686                ..Default::default()
687            },
688            Segment {
689                flags: 0,
690                dir: Direction::Down,
691                pos: 507,
692                delta: 0,
693                min_coord: 40,
694                max_coord: 485,
695                height: 498,
696                link_ix: Some(4),
697                serif_ix: None,
698                ..Default::default()
699            },
700        ];
701        assert_eq!(segments, &expected);
702    }
703
704    #[test]
705    fn vertical_segments() {
706        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
707        let glyphs = font.outline_glyphs();
708        let glyph = glyphs.get(GlyphId::new(8)).unwrap();
709        let mut outline = Outline::default();
710        outline.fill(&glyph, Default::default()).unwrap();
711        let mut axis = Axis::new(Axis::VERTICAL, outline.orientation);
712        compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
713        link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
714        let segments = retain_segment_test_fields(&axis.segments);
715        let expected = [
716            Segment {
717                flags: 0,
718                dir: Direction::Left,
719                pos: 0,
720                delta: 0,
721                min_coord: 85,
722                max_coord: 470,
723                height: 418,
724                link_ix: Some(2),
725                serif_ix: None,
726                ..Default::default()
727            },
728            Segment {
729                flags: 0,
730                dir: Direction::Right,
731                pos: 504,
732                delta: 0,
733                min_coord: 112,
734                max_coord: 168,
735                height: 56,
736                link_ix: Some(3),
737                serif_ix: None,
738                ..Default::default()
739            },
740            Segment {
741                flags: 0,
742                dir: Direction::Right,
743                pos: 109,
744                delta: 0,
745                min_coord: 109,
746                max_coord: 427,
747                height: 327,
748                link_ix: Some(0),
749                serif_ix: None,
750                ..Default::default()
751            },
752            Segment {
753                flags: 0,
754                dir: Direction::Left,
755                pos: 483,
756                delta: 0,
757                min_coord: 86,
758                max_coord: 400,
759                height: 352,
760                link_ix: Some(1),
761                serif_ix: None,
762                ..Default::default()
763            },
764            Segment {
765                flags: 0,
766                dir: Direction::Right,
767                pos: 647,
768                delta: 0,
769                min_coord: 76,
770                max_coord: 103,
771                height: 29,
772                link_ix: None,
773                serif_ix: Some(1),
774                ..Default::default()
775            },
776            Segment {
777                flags: 0,
778                dir: Direction::Right,
779                pos: 592,
780                delta: 0,
781                min_coord: 131,
782                max_coord: 437,
783                height: 346,
784                link_ix: None,
785                serif_ix: Some(1),
786                ..Default::default()
787            },
788        ];
789        assert_eq!(segments, &expected);
790    }
791
792    #[test]
793    fn cjk_horizontal_segments() {
794        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
795        let glyphs = font.outline_glyphs();
796        let glyph = glyphs.get(GlyphId::new(9)).unwrap();
797        let mut outline = Outline::default();
798        outline.fill(&glyph, Default::default()).unwrap();
799        let mut axis = Axis::new(Axis::HORIZONTAL, outline.orientation);
800        compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
801        link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
802        let segments = retain_segment_test_fields(&axis.segments);
803        let expected = [
804            Segment {
805                flags: 0,
806                dir: Direction::Down,
807                pos: 731,
808                delta: 0,
809                min_coord: 155,
810                max_coord: 676,
811                height: 524,
812                link_ix: Some(1),
813                serif_ix: None,
814                ..Default::default()
815            },
816            Segment {
817                flags: 0,
818                dir: Direction::Up,
819                pos: 670,
820                delta: 0,
821                min_coord: 133,
822                max_coord: 712,
823                height: 579,
824                link_ix: Some(0),
825                serif_ix: None,
826                ..Default::default()
827            },
828            Segment {
829                flags: 0,
830                dir: Direction::Down,
831                pos: 458,
832                delta: 0,
833                min_coord: 741,
834                max_coord: 757,
835                height: 88,
836                link_ix: None,
837                serif_ix: None,
838                ..Default::default()
839            },
840            Segment {
841                flags: 0,
842                dir: Direction::Down,
843                pos: 911,
844                delta: 0,
845                min_coord: -9,
846                max_coord: 791,
847                height: 821,
848                link_ix: Some(5),
849                serif_ix: None,
850                ..Default::default()
851            },
852            Segment {
853                flags: 0,
854                dir: Direction::Up,
855                pos: 693,
856                delta: 0,
857                min_coord: -7,
858                max_coord: 9,
859                height: 18,
860                link_ix: None,
861                serif_ix: Some(5),
862                ..Default::default()
863            },
864            Segment {
865                flags: 0,
866                dir: Direction::Up,
867                pos: 849,
868                delta: 0,
869                min_coord: 11,
870                max_coord: 829,
871                height: 823,
872                link_ix: Some(3),
873                serif_ix: None,
874                ..Default::default()
875            },
876            Segment {
877                flags: 0,
878                dir: Direction::Down,
879                pos: 569,
880                delta: 0,
881                min_coord: 547,
882                max_coord: 576,
883                height: 29,
884                link_ix: None,
885                serif_ix: None,
886                ..Default::default()
887            },
888            Segment {
889                flags: 0,
890                dir: Direction::Down,
891                pos: 201,
892                delta: 0,
893                min_coord: -57,
894                max_coord: 540,
895                height: 599,
896                link_ix: Some(8),
897                serif_ix: None,
898                ..Default::default()
899            },
900            Segment {
901                flags: 0,
902                dir: Direction::Up,
903                pos: 138,
904                delta: 0,
905                min_coord: -78,
906                max_coord: 543,
907                height: 640,
908                link_ix: Some(7),
909                serif_ix: None,
910                ..Default::default()
911            },
912        ];
913        assert_eq!(segments, &expected);
914    }
915
916    #[test]
917    fn cjk_vertical_segments() {
918        let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
919        let glyphs = font.outline_glyphs();
920        let glyph = glyphs.get(GlyphId::new(9)).unwrap();
921        let mut outline = Outline::default();
922        outline.fill(&glyph, Default::default()).unwrap();
923        let mut axis = Axis::new(Axis::VERTICAL, outline.orientation);
924        compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
925        link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
926        let segments = retain_segment_test_fields(&axis.segments);
927        let expected = [
928            Segment {
929                flags: 0,
930                dir: Direction::Right,
931                pos: 758,
932                delta: 0,
933                min_coord: 280,
934                max_coord: 545,
935                height: 288,
936                link_ix: Some(1),
937                serif_ix: None,
938                ..Default::default()
939            },
940            Segment {
941                flags: 0,
942                dir: Direction::Left,
943                pos: 729,
944                delta: 0,
945                min_coord: 288,
946                max_coord: 674,
947                height: 391,
948                link_ix: Some(0),
949                serif_ix: None,
950                ..Default::default()
951            },
952            Segment {
953                flags: 1,
954                dir: Direction::Left,
955                pos: 133,
956                delta: 0,
957                min_coord: 670,
958                max_coord: 693,
959                height: 34,
960                link_ix: None,
961                serif_ix: None,
962                ..Default::default()
963            },
964            Segment {
965                flags: 0,
966                dir: Direction::Right,
967                pos: 757,
968                delta: 0,
969                min_coord: 393,
970                max_coord: 458,
971                height: 70,
972                link_ix: None,
973                serif_ix: Some(0),
974                ..Default::default()
975            },
976            Segment {
977                flags: 1,
978                dir: Direction::Right,
979                pos: 3,
980                delta: 2,
981                min_coord: 727,
982                max_coord: 838,
983                height: 133,
984                link_ix: None,
985                serif_ix: None,
986                ..Default::default()
987            },
988            Segment {
989                flags: 0,
990                dir: Direction::Right,
991                pos: 576,
992                delta: 0,
993                min_coord: 397,
994                max_coord: 569,
995                height: 177,
996                link_ix: Some(7),
997                serif_ix: None,
998                ..Default::default()
999            },
1000            Segment {
1001                flags: 0,
1002                dir: Direction::Left,
1003                pos: 547,
1004                delta: 0,
1005                min_coord: 387,
1006                max_coord: 569,
1007                height: 182,
1008                link_ix: None,
1009                serif_ix: Some(7),
1010                ..Default::default()
1011            },
1012            Segment {
1013                flags: 0,
1014                dir: Direction::Left,
1015                pos: 576,
1016                delta: 0,
1017                min_coord: 536,
1018                max_coord: 546,
1019                height: 10,
1020                link_ix: Some(5),
1021                serif_ix: None,
1022                ..Default::default()
1023            },
1024            Segment {
1025                flags: 1,
1026                dir: Direction::Left,
1027                pos: -78,
1028                delta: 0,
1029                min_coord: 138,
1030                max_coord: 161,
1031                height: 34,
1032                link_ix: None,
1033                serif_ix: None,
1034                ..Default::default()
1035            },
1036            Segment {
1037                flags: 1,
1038                dir: Direction::Left,
1039                pos: 788,
1040                delta: 0,
1041                min_coord: 262,
1042                max_coord: 294,
1043                height: 46,
1044                link_ix: None,
1045                serif_ix: None,
1046                ..Default::default()
1047            },
1048        ];
1049        assert_eq!(segments, &expected);
1050    }
1051
1052    // Retain the fields that are valid and comparable after
1053    // the segment pass.
1054    fn retain_segment_test_fields(segments: &[Segment]) -> Vec<Segment> {
1055        segments
1056            .iter()
1057            .map(|segment| Segment {
1058                flags: segment.flags,
1059                dir: segment.dir,
1060                pos: segment.pos,
1061                delta: segment.delta,
1062                min_coord: segment.min_coord,
1063                max_coord: segment.max_coord,
1064                height: segment.height,
1065                link_ix: segment.link_ix,
1066                serif_ix: segment.serif_ix,
1067                ..Default::default()
1068            })
1069            .collect()
1070    }
1071}