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