Skip to main content

skrifa/outline/autohint/topo/
edges.rs

1//! Edge detection.
2//!
3//! Edges are sets of segments that all lie within a threshold based on
4//! stem widths.
5//!
6//! Here we compute edges from the segment list, assign properties (round,
7//! serif, links) and then associate them with blue zones.
8
9use super::{
10    super::{
11        metrics::{fixed_div, fixed_mul, Scale, ScaledAxisMetrics, ScaledBlue, UnscaledBlue},
12        outline::Direction,
13        style::ScriptGroup,
14    },
15    Axis, BlueProvenance, Dimension, Edge, TopoFlags,
16};
17
18/// Links segments to edges, using feature analysis for selection.
19///
20/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2128>
21pub(crate) fn compute_edges(
22    axis: &mut Axis,
23    metrics: &ScaledAxisMetrics,
24    top_to_bottom_hinting: bool,
25    y_scale: i32,
26    group: ScriptGroup,
27) {
28    axis.edges.clear();
29    let scale = metrics.scale;
30    // This is always passed as 0 in functions that take hinting direction
31    // in CJK
32    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1114>
33    let top_to_bottom_hinting =
34        if axis.dim == Dimension::Horizontal || group != ScriptGroup::Default {
35            false
36        } else {
37            top_to_bottom_hinting
38        };
39    // Ignore horizontal segments less than 1 pixel in length
40    let segment_length_threshold = if axis.dim == Dimension::Horizontal {
41        fixed_div(64, y_scale)
42    } else {
43        0
44    };
45    // Also ignore segments with a width delta larger than 0.5 pixels
46    let segment_width_threshold = fixed_div(32, scale);
47    // Ensure that edge distance threshold is less than or equal to
48    // 0.25 pixels
49    let initial_threshold = metrics.width_metrics.edge_distance_threshold;
50    const EDGE_DISTANCE_THRESHOLD_MAX: i32 = 64 / 4;
51    let edge_distance_threshold = if group == ScriptGroup::Default {
52        fixed_div(
53            fixed_mul(initial_threshold, scale).min(EDGE_DISTANCE_THRESHOLD_MAX),
54            scale,
55        )
56    } else {
57        // CJK uses a slightly different computation here
58        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1043>
59        let threshold = fixed_mul(initial_threshold, scale);
60        if threshold > EDGE_DISTANCE_THRESHOLD_MAX {
61            fixed_div(EDGE_DISTANCE_THRESHOLD_MAX, scale)
62        } else {
63            initial_threshold
64        }
65    };
66    // Now build the sorted table of edges by looping over all segments
67    // to find a matching edge, adding a new one if not found.
68    // We can't iterate segments because we make mutable calls on `axis`
69    // below which causes overlapping borrows
70    for segment_ix in 0..axis.segments.len() {
71        let segment = &axis.segments[segment_ix];
72        if group == ScriptGroup::Default {
73            // Ignore segments that are too short, too wide or direction-less
74            if (segment.height as i32) < segment_length_threshold
75                || (segment.delta as i32 > segment_width_threshold)
76                || segment.dir == Direction::None
77            {
78                continue;
79            }
80            // Ignore serif edges that are smaller than 1.5 pixels
81            if segment.serif_ix.is_some()
82                && (2 * segment.height as i32) < (3 * segment_length_threshold)
83            {
84                continue;
85            }
86        }
87        // Look for a corresponding edge for this segment
88        let mut best_dist = i32::MAX;
89        let mut best_edge_ix = None;
90        for edge_ix in 0..axis.edges.len() {
91            let edge = &axis.edges[edge_ix];
92            let dist = (segment.pos as i32 - edge.fpos as i32).abs();
93            if dist < edge_distance_threshold && edge.dir == segment.dir && dist < best_dist {
94                if group == ScriptGroup::Default {
95                    best_edge_ix = Some(edge_ix);
96                    break;
97                }
98                // For CJK, we add some additional checks
99                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1073>
100                if let Some(link) = segment.link(&axis.segments).copied() {
101                    // Check whether all linked segments of the candidate edge
102                    // can make a single edge
103                    let first_ix = edge.first_ix as usize;
104                    let mut seg1 = &axis.segments[first_ix];
105                    let mut dist2 = 0;
106                    loop {
107                        if let Some(link1) = seg1.link(&axis.segments).copied() {
108                            dist2 = (link.pos as i32 - link1.pos as i32).abs();
109                            if dist2 >= edge_distance_threshold {
110                                break;
111                            }
112                        }
113                        if seg1.edge_next_ix == Some(first_ix as u16) {
114                            break;
115                        }
116                        if let Some(next) = seg1.next_in_edge(&axis.segments) {
117                            seg1 = next;
118                        } else {
119                            break;
120                        }
121                    }
122                    if dist2 >= edge_distance_threshold {
123                        continue;
124                    }
125                }
126                best_dist = dist;
127                best_edge_ix = Some(edge_ix);
128            }
129        }
130        if let Some(edge_ix) = best_edge_ix {
131            axis.append_segment_to_edge(segment_ix, edge_ix);
132        } else {
133            // We couldn't find an edge, so add a new one for this segment
134            let opos = fixed_mul(segment.pos as i32, scale);
135            let edge = Edge {
136                fpos: segment.pos,
137                opos,
138                pos: opos,
139                dir: segment.dir,
140                first_ix: segment_ix as u16,
141                last_ix: segment_ix as u16,
142                ..Default::default()
143            };
144            axis.insert_edge(edge, top_to_bottom_hinting);
145            axis.segments[segment_ix].edge_next_ix = Some(segment_ix as u16);
146        }
147    }
148    if group == ScriptGroup::Default {
149        // Loop again to find single point segments without a direction and
150        // associate them with an existing edge if possible
151        for segment_ix in 0..axis.segments.len() {
152            let segment = &axis.segments[segment_ix];
153            if segment.dir != Direction::None {
154                continue;
155            }
156            // Try to find an edge that coincides with this segment within the
157            // threshold
158            if let Some(edge_ix) = axis
159                .edges
160                .iter()
161                .enumerate()
162                .filter_map(|(ix, edge)| {
163                    ((segment.pos as i32 - edge.fpos as i32).abs() < edge_distance_threshold)
164                        .then_some(ix)
165                })
166                .next()
167            {
168                // We found an edge, link everything up
169                axis.append_segment_to_edge(segment_ix, edge_ix);
170            }
171        }
172    }
173    link_segments_to_edges(axis);
174    compute_edge_properties(axis);
175}
176
177/// Edges get reordered as they're built so we need to assign edge indices to
178/// segments in a second pass.
179fn link_segments_to_edges(axis: &mut Axis) {
180    let segments = axis.segments.as_mut_slice();
181    for edge_ix in 0..axis.edges.len() {
182        let edge = &axis.edges[edge_ix];
183        let mut ix = edge.first_ix as usize;
184        let last_ix = edge.last_ix as usize;
185        loop {
186            let segment = &mut segments[ix];
187            segment.edge_ix = Some(edge_ix as u16);
188            if ix == last_ix {
189                break;
190            }
191            ix = segment
192                .edge_next_ix
193                .map(|ix| ix as usize)
194                .unwrap_or(last_ix);
195        }
196    }
197}
198
199/// Compute the edge properties based on the series of segments that make
200/// up the edge.
201///
202/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2339>
203fn compute_edge_properties(axis: &mut Axis) {
204    let edges = axis.edges.as_mut_slice();
205    let segments = axis.segments.as_slice();
206    for edge_ix in 0..edges.len() {
207        let mut roundness = 0;
208        let mut straightness = 0;
209        let edge = edges[edge_ix];
210        let mut segment_ix = edge.first_ix as usize;
211        let last_segment_ix = edge.last_ix as usize;
212        loop {
213            // This loop can modify the current edge, so make sure we
214            // reload it here
215            let edge = edges[edge_ix];
216            let segment = &segments[segment_ix];
217            let next_segment_ix = segment.edge_next_ix;
218            // Check roundness
219            if segment.flags.contains(TopoFlags::ROUND) {
220                roundness += 1;
221            } else {
222                straightness += 1;
223            }
224            // Check for serifs
225            let is_serif = if let Some(serif_ix) = segment.serif_ix {
226                let serif = &segments[serif_ix as usize];
227                serif.edge_ix.is_some() && serif.edge_ix != Some(edge_ix as u16)
228            } else {
229                false
230            };
231            // Check for links
232            if is_serif
233                || (segment.link_ix.is_some()
234                    && segments[segment.link_ix.unwrap() as usize]
235                        .edge_ix
236                        .is_some())
237            {
238                let (edge2_ix, segment2_ix) = if is_serif {
239                    (edge.serif_ix, segment.serif_ix)
240                } else {
241                    (edge.link_ix, segment.link_ix)
242                };
243                let edge2_ix = if let (Some(edge2_ix), Some(segment2_ix)) = (edge2_ix, segment2_ix)
244                {
245                    let edge2 = &edges[edge2_ix as usize];
246                    let edge_delta = (edge.fpos as i32 - edge2.fpos as i32).abs();
247                    let segment2 = &segments[segment2_ix as usize];
248                    let segment_delta = (segment.pos as i32 - segment2.pos as i32).abs();
249                    if segment_delta < edge_delta {
250                        segment2.edge_ix
251                    } else {
252                        Some(edge2_ix)
253                    }
254                } else if let Some(segment2_ix) = segment2_ix {
255                    segments[segment2_ix as usize].edge_ix
256                } else {
257                    edge2_ix
258                };
259                if is_serif {
260                    edges[edge_ix].serif_ix = edge2_ix;
261                    edges[edge2_ix.unwrap() as usize].flags |= TopoFlags::SERIF;
262                } else {
263                    edges[edge_ix].link_ix = edge2_ix;
264                }
265            }
266            if segment_ix == last_segment_ix {
267                break;
268            }
269            segment_ix = next_segment_ix
270                .map(|ix| ix as usize)
271                .unwrap_or(last_segment_ix);
272        }
273        let edge = &mut edges[edge_ix];
274        edge.flags = TopoFlags::NORMAL;
275        if roundness > 0 && roundness >= straightness {
276            edge.flags |= TopoFlags::ROUND;
277        }
278        // Drop serifs for linked edges
279        if edge.serif_ix.is_some() && edge.link_ix.is_some() {
280            edge.serif_ix = None;
281        }
282    }
283}
284
285/// Compute all edges which lie within blue zones.
286///
287/// For Latin, this is only done for the vertical axis.
288///
289/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2503>
290pub(crate) fn compute_blue_edges(
291    axis: &mut Axis,
292    scale: &Scale,
293    unscaled_blues: &[UnscaledBlue],
294    blues: &[ScaledBlue],
295    group: ScriptGroup,
296) {
297    // For the default script group, don't compute blues in the horizontal
298    // direction
299    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3572>
300    if axis.dim != Dimension::Vertical && group == ScriptGroup::Default {
301        return;
302    }
303    let axis_scale = if axis.dim == Dimension::Horizontal {
304        scale.x_scale
305    } else {
306        scale.y_scale
307    };
308    // Initial threshold
309    let initial_best_dest = fixed_mul(scale.units_per_em / 40, axis_scale).min(64 / 2);
310    for edge in &mut axis.edges {
311        let mut best_blue = None;
312        let mut best_is_neutral = false;
313        let mut best_blue_idx = None;
314        let mut best_blue_is_shoot = false;
315        // Initial threshold as a fraction of em size with a max distance
316        // of 0.5 pixels
317        let mut best_dist = initial_best_dest;
318        for (blue_ix, (unscaled_blue, blue)) in unscaled_blues.iter().zip(blues).enumerate() {
319            // Ignore inactive blue zones
320            if !blue.is_active {
321                continue;
322            }
323            let is_top = blue.zones.is_top_like();
324            let is_neutral = blue.zones.is_neutral();
325            let is_major_dir = edge.dir == axis.major_dir;
326            // Both directions are handled for neutral blues
327            if is_top ^ is_major_dir || is_neutral {
328                // Compare to reference position
329                let (ref_pos, matching_blue) = if group == ScriptGroup::Default {
330                    (unscaled_blue.position, blue.position)
331                } else {
332                    // For CJK, we take the blue with the smallest delta
333                    // from the edge
334                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1356>
335                    if (edge.fpos as i32 - unscaled_blue.position).abs()
336                        > (edge.fpos as i32 - unscaled_blue.overshoot).abs()
337                    {
338                        (unscaled_blue.overshoot, blue.overshoot)
339                    } else {
340                        (unscaled_blue.position, blue.position)
341                    }
342                };
343                let dist = fixed_mul((edge.fpos as i32 - ref_pos).abs(), axis_scale);
344                if dist < best_dist {
345                    best_dist = dist;
346                    best_blue = Some(matching_blue);
347                    best_is_neutral = is_neutral;
348                    best_blue_idx = Some(blue_ix as u16);
349                    best_blue_is_shoot = false;
350                }
351                if group == ScriptGroup::Default {
352                    // Now compare to overshoot position for the default script
353                    // group
354                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2579>
355                    if edge.flags.contains(TopoFlags::ROUND) && dist != 0 && !is_neutral {
356                        let is_under_ref = (edge.fpos as i32) < unscaled_blue.position;
357                        if is_top ^ is_under_ref {
358                            let dist = fixed_mul(
359                                (edge.fpos as i32 - unscaled_blue.overshoot).abs(),
360                                axis_scale,
361                            );
362                            if dist < best_dist {
363                                best_dist = dist;
364                                best_blue = Some(blue.overshoot);
365                                best_is_neutral = is_neutral;
366                                best_blue_idx = Some(blue_ix as u16);
367                                best_blue_is_shoot = true;
368                            }
369                        }
370                    }
371                }
372            }
373        }
374        if let Some(best_blue) = best_blue {
375            edge.blue_edge = Some(best_blue);
376            edge.blue_provenance = Some(BlueProvenance {
377                index: best_blue_idx.unwrap_or_default(),
378                is_shoot: best_blue_is_shoot,
379            });
380            if best_is_neutral {
381                edge.flags |= TopoFlags::NEUTRAL;
382            }
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::{
390        super::super::{
391            metrics::{self, ScaledWidth},
392            outline::Outline,
393            shape::{Shaper, ShaperMode},
394            style,
395        },
396        super::segments,
397        *,
398    };
399    use crate::{attribute::Style, MetadataProvider};
400    use raw::{types::GlyphId, FontRef, TableProvider};
401
402    #[test]
403    fn edges_default() {
404        let expected_h_edges = [
405            Edge {
406                fpos: 15,
407                opos: 15,
408                pos: 15,
409                flags: TopoFlags::ROUND,
410                dir: Direction::Up,
411                blue_edge: None,
412                blue_provenance: None,
413                link_ix: Some(3),
414                serif_ix: None,
415                scale: 0,
416                first_ix: 1,
417                last_ix: 1,
418            },
419            Edge {
420                fpos: 123,
421                opos: 126,
422                pos: 126,
423                flags: TopoFlags::NORMAL,
424                dir: Direction::Up,
425                blue_edge: None,
426                blue_provenance: None,
427                link_ix: Some(2),
428                serif_ix: None,
429                scale: 0,
430                first_ix: 0,
431                last_ix: 0,
432            },
433            Edge {
434                fpos: 186,
435                opos: 190,
436                pos: 190,
437                flags: TopoFlags::NORMAL,
438                dir: Direction::Down,
439                blue_edge: None,
440                blue_provenance: None,
441                link_ix: Some(1),
442                serif_ix: None,
443                scale: 0,
444                first_ix: 4,
445                last_ix: 4,
446            },
447            Edge {
448                fpos: 205,
449                opos: 210,
450                pos: 210,
451                flags: TopoFlags::ROUND,
452                dir: Direction::Down,
453                blue_edge: None,
454                blue_provenance: None,
455                link_ix: Some(0),
456                serif_ix: None,
457                scale: 0,
458                first_ix: 3,
459                last_ix: 3,
460            },
461        ];
462        let expected_v_edges = [
463            Edge {
464                fpos: -240,
465                opos: -246,
466                pos: -246,
467                flags: TopoFlags::NORMAL,
468                dir: Direction::Left,
469                blue_edge: Some(ScaledWidth {
470                    scaled: -246,
471                    fitted: -256,
472                }),
473                blue_provenance: Some(BlueProvenance {
474                    index: 2,
475                    is_shoot: false,
476                }),
477                link_ix: None,
478                serif_ix: Some(1),
479                scale: 0,
480                first_ix: 3,
481                last_ix: 3,
482            },
483            Edge {
484                fpos: 481,
485                opos: 493,
486                pos: 493,
487                flags: TopoFlags::NORMAL,
488                dir: Direction::Left,
489                blue_edge: None,
490                blue_provenance: None,
491                link_ix: Some(2),
492                serif_ix: None,
493                scale: 0,
494                first_ix: 0,
495                last_ix: 0,
496            },
497            Edge {
498                fpos: 592,
499                opos: 606,
500                pos: 606,
501                flags: TopoFlags::ROUND | TopoFlags::SERIF,
502                dir: Direction::Right,
503                blue_edge: Some(ScaledWidth {
504                    scaled: 606,
505                    fitted: 576,
506                }),
507                blue_provenance: Some(BlueProvenance {
508                    index: 0,
509                    is_shoot: false,
510                }),
511                link_ix: Some(1),
512                serif_ix: None,
513                scale: 0,
514                first_ix: 2,
515                last_ix: 2,
516            },
517            Edge {
518                fpos: 647,
519                opos: 663,
520                pos: 663,
521                flags: TopoFlags::NORMAL,
522                dir: Direction::Right,
523                blue_edge: None,
524                blue_provenance: None,
525                link_ix: None,
526                serif_ix: Some(2),
527                scale: 0,
528                first_ix: 1,
529                last_ix: 1,
530            },
531        ];
532        check_edges(
533            font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
534            GlyphId::new(9),
535            style::StyleClass::HEBR,
536            &expected_h_edges,
537            &expected_v_edges,
538        );
539    }
540
541    #[test]
542    fn edges_cjk() {
543        let expected_h_edges = [
544            Edge {
545                fpos: 138,
546                opos: 141,
547                pos: 141,
548                flags: TopoFlags::NORMAL,
549                dir: Direction::Up,
550                blue_edge: None,
551                blue_provenance: None,
552                link_ix: Some(1),
553                serif_ix: None,
554                scale: 0,
555                first_ix: 8,
556                last_ix: 8,
557            },
558            Edge {
559                fpos: 201,
560                opos: 206,
561                pos: 206,
562                flags: TopoFlags::NORMAL,
563                dir: Direction::Down,
564                blue_edge: None,
565                blue_provenance: None,
566                link_ix: Some(0),
567                serif_ix: None,
568                scale: 0,
569                first_ix: 7,
570                last_ix: 7,
571            },
572            Edge {
573                fpos: 458,
574                opos: 469,
575                pos: 469,
576                flags: TopoFlags::NORMAL,
577                dir: Direction::Down,
578                blue_edge: None,
579                blue_provenance: None,
580                link_ix: None,
581                serif_ix: None,
582                scale: 0,
583                first_ix: 2,
584                last_ix: 2,
585            },
586            Edge {
587                fpos: 569,
588                opos: 583,
589                pos: 583,
590                flags: TopoFlags::NORMAL,
591                dir: Direction::Down,
592                blue_edge: None,
593                blue_provenance: None,
594                link_ix: None,
595                serif_ix: None,
596                scale: 0,
597                first_ix: 6,
598                last_ix: 6,
599            },
600            Edge {
601                fpos: 670,
602                opos: 686,
603                pos: 686,
604                flags: TopoFlags::NORMAL,
605                dir: Direction::Up,
606                blue_edge: None,
607                blue_provenance: None,
608                link_ix: Some(6),
609                serif_ix: None,
610                scale: 0,
611                first_ix: 1,
612                last_ix: 1,
613            },
614            Edge {
615                fpos: 693,
616                opos: 710,
617                pos: 710,
618                flags: TopoFlags::NORMAL,
619                dir: Direction::Up,
620                blue_edge: None,
621                blue_provenance: None,
622                link_ix: None,
623                serif_ix: Some(7),
624                scale: 0,
625                first_ix: 4,
626                last_ix: 4,
627            },
628            Edge {
629                fpos: 731,
630                opos: 749,
631                pos: 749,
632                flags: TopoFlags::NORMAL,
633                dir: Direction::Down,
634                blue_edge: None,
635                blue_provenance: None,
636                link_ix: Some(4),
637                serif_ix: None,
638                scale: 0,
639                first_ix: 0,
640                last_ix: 0,
641            },
642            Edge {
643                fpos: 849,
644                opos: 869,
645                pos: 869,
646                flags: TopoFlags::NORMAL,
647                dir: Direction::Up,
648                blue_edge: None,
649                blue_provenance: None,
650                link_ix: Some(8),
651                serif_ix: None,
652                scale: 0,
653                first_ix: 5,
654                last_ix: 5,
655            },
656            Edge {
657                fpos: 911,
658                opos: 933,
659                pos: 933,
660                flags: TopoFlags::NORMAL,
661                dir: Direction::Down,
662                blue_edge: None,
663                blue_provenance: None,
664                link_ix: Some(7),
665                serif_ix: None,
666                scale: 0,
667                first_ix: 3,
668                last_ix: 3,
669            },
670        ];
671        let expected_v_edges = [
672            Edge {
673                fpos: -78,
674                opos: -80,
675                pos: -80,
676                flags: TopoFlags::ROUND,
677                dir: Direction::Left,
678                blue_edge: Some(ScaledWidth {
679                    scaled: -80,
680                    fitted: -64,
681                }),
682                blue_provenance: Some(BlueProvenance {
683                    index: 1,
684                    is_shoot: false,
685                }),
686                link_ix: None,
687                serif_ix: None,
688                scale: 0,
689                first_ix: 8,
690                last_ix: 8,
691            },
692            Edge {
693                fpos: 3,
694                opos: 3,
695                pos: 3,
696                flags: TopoFlags::ROUND,
697                dir: Direction::Right,
698                blue_edge: None,
699                blue_provenance: None,
700                link_ix: None,
701                serif_ix: None,
702                scale: 0,
703                first_ix: 4,
704                last_ix: 4,
705            },
706            Edge {
707                fpos: 133,
708                opos: 136,
709                pos: 136,
710                flags: TopoFlags::ROUND,
711                dir: Direction::Left,
712                blue_edge: None,
713                blue_provenance: None,
714                link_ix: None,
715                serif_ix: None,
716                scale: 0,
717                first_ix: 2,
718                last_ix: 2,
719            },
720            Edge {
721                fpos: 547,
722                opos: 560,
723                pos: 560,
724                flags: TopoFlags::NORMAL,
725                dir: Direction::Left,
726                blue_edge: None,
727                blue_provenance: None,
728                link_ix: None,
729                serif_ix: Some(5),
730                scale: 0,
731                first_ix: 6,
732                last_ix: 6,
733            },
734            Edge {
735                fpos: 576,
736                opos: 590,
737                pos: 590,
738                flags: TopoFlags::NORMAL,
739                dir: Direction::Right,
740                blue_edge: None,
741                blue_provenance: None,
742                link_ix: Some(5),
743                serif_ix: None,
744                scale: 0,
745                first_ix: 5,
746                last_ix: 5,
747            },
748            Edge {
749                fpos: 576,
750                opos: 590,
751                pos: 590,
752                flags: TopoFlags::NORMAL,
753                dir: Direction::Left,
754                blue_edge: None,
755                blue_provenance: None,
756                link_ix: Some(4),
757                serif_ix: None,
758                scale: 0,
759                first_ix: 7,
760                last_ix: 7,
761            },
762            Edge {
763                fpos: 729,
764                opos: 746,
765                pos: 746,
766                flags: TopoFlags::NORMAL,
767                dir: Direction::Left,
768                blue_edge: None,
769                blue_provenance: None,
770                link_ix: Some(7),
771                serif_ix: None,
772                scale: 0,
773                first_ix: 1,
774                last_ix: 1,
775            },
776            Edge {
777                fpos: 758,
778                opos: 776,
779                pos: 776,
780                flags: TopoFlags::NORMAL,
781                dir: Direction::Right,
782                blue_edge: None,
783                blue_provenance: None,
784                link_ix: Some(6),
785                serif_ix: None,
786                scale: 0,
787                first_ix: 0,
788                last_ix: 3,
789            },
790            Edge {
791                fpos: 788,
792                opos: 807,
793                pos: 807,
794                flags: TopoFlags::ROUND,
795                dir: Direction::Left,
796                blue_edge: None,
797                blue_provenance: None,
798                link_ix: None,
799                serif_ix: None,
800                scale: 0,
801                first_ix: 9,
802                last_ix: 9,
803            },
804        ];
805        check_edges(
806            font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
807            GlyphId::new(9),
808            style::StyleClass::HANI,
809            &expected_h_edges,
810            &expected_v_edges,
811        );
812    }
813
814    fn check_edges(
815        font_data: &[u8],
816        glyph_id: GlyphId,
817        style_class: usize,
818        expected_h_edges: &[Edge],
819        expected_v_edges: &[Edge],
820    ) {
821        let font = FontRef::new(font_data).unwrap();
822        let shaper = Shaper::new(&font, ShaperMode::Nominal);
823        let class = &style::STYLE_CLASSES[style_class];
824        let unscaled_metrics = metrics::compute_unscaled_style_metrics(
825            &shaper,
826            Default::default(),
827            class,
828            Default::default(),
829        );
830        let scale = metrics::Scale::new(
831            16.0,
832            font.head().unwrap().units_per_em() as i32,
833            Style::Normal,
834            Default::default(),
835            class.script.group,
836        );
837        let scaled_metrics =
838            metrics::scale_style_metrics(&unscaled_metrics, scale, Default::default());
839        let glyphs = font.outline_glyphs();
840        let glyph = glyphs.get(glyph_id).unwrap();
841        let mut outline = Outline::default();
842        outline.fill(&glyph, &[], Default::default()).unwrap();
843        let mut axes = [
844            Axis::new(Dimension::Horizontal, outline.orientation),
845            Axis::new(Dimension::Vertical, outline.orientation),
846        ];
847        for (dim, axis) in axes.iter_mut().enumerate() {
848            segments::compute_segments(&mut outline, axis, class.script.group);
849            segments::link_segments(
850                &outline,
851                axis,
852                scaled_metrics.axes[dim].scale,
853                class.script.group,
854                unscaled_metrics.axes[dim].max_width(),
855            );
856            compute_edges(
857                axis,
858                &scaled_metrics.axes[dim],
859                class.script.hint_top_to_bottom,
860                scaled_metrics.axes[1].scale,
861                class.script.group,
862            );
863            compute_blue_edges(
864                axis,
865                &scale,
866                &unscaled_metrics.axes[dim].blues,
867                &scaled_metrics.axes[dim].blues,
868                class.script.group,
869            );
870        }
871        assert_eq!(
872            axes[Dimension::Horizontal].edges.as_slice(),
873            expected_h_edges
874        );
875        assert_eq!(axes[Dimension::Vertical].edges.as_slice(), expected_v_edges);
876    }
877}