Coverage Report

Created: 2025-07-23 06:50

/src/fontations/skrifa/src/outline/autohint/topo/edges.rs
Line
Count
Source (jump to first uncovered line)
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
9
use 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>
21
3.85M
pub(crate) fn compute_edges(
22
3.85M
    axis: &mut Axis,
23
3.85M
    metrics: &ScaledAxisMetrics,
24
3.85M
    top_to_bottom_hinting: bool,
25
3.85M
    y_scale: i32,
26
3.85M
    group: ScriptGroup,
27
3.85M
) {
28
3.85M
    axis.edges.clear();
29
3.85M
    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
3.85M
    let top_to_bottom_hinting = if axis.dim == Axis::HORIZONTAL || group != ScriptGroup::Default {
34
2.05M
        false
35
    } else {
36
1.80M
        top_to_bottom_hinting
37
    };
38
    // Ignore horizontal segments less than 1 pixel in length
39
3.85M
    let segment_length_threshold = if axis.dim == Axis::HORIZONTAL {
40
1.41M
        fixed_div(64, y_scale)
41
    } else {
42
2.44M
        0
43
    };
44
    // Also ignore segments with a width delta larger than 0.5 pixels
45
3.85M
    let segment_width_threshold = fixed_div(32, scale);
46
3.85M
    // Ensure that edge distance threshold is less than or equal to
47
3.85M
    // 0.25 pixels
48
3.85M
    let initial_threshold = metrics.width_metrics.edge_distance_threshold;
49
    const EDGE_DISTANCE_THRESHOLD_MAX: i32 = 64 / 4;
50
3.85M
    let edge_distance_threshold = if group == ScriptGroup::Default {
51
2.57M
        fixed_div(
52
2.57M
            fixed_mul(initial_threshold, scale).min(EDGE_DISTANCE_THRESHOLD_MAX),
53
2.57M
            scale,
54
2.57M
        )
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
1.28M
        let threshold = fixed_mul(initial_threshold, scale);
59
1.28M
        if threshold > EDGE_DISTANCE_THRESHOLD_MAX {
60
758k
            fixed_div(EDGE_DISTANCE_THRESHOLD_MAX, scale)
61
        } else {
62
524k
            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
13.7M
    for segment_ix in 0..axis.segments.len() {
70
13.7M
        let segment = &axis.segments[segment_ix];
71
13.7M
        if group == ScriptGroup::Default {
72
            // Ignore segments that are too short, too wide or direction-less
73
9.36M
            if (segment.height as i32) < segment_length_threshold
74
9.18M
                || (segment.delta as i32 > segment_width_threshold)
75
9.05M
                || segment.dir == Direction::None
76
            {
77
702k
                continue;
78
8.66M
            }
79
8.66M
            // Ignore serif edges that are smaller than 1.5 pixels
80
8.66M
            if segment.serif_ix.is_some()
81
563k
                && (2 * segment.height as i32) < (3 * segment_length_threshold)
82
            {
83
3.30k
                continue;
84
8.66M
            }
85
4.36M
        }
86
        // Look for a corresponding edge for this segment
87
13.0M
        let mut best_dist = i32::MAX;
88
13.0M
        let mut best_edge_ix = None;
89
63.3M
        for edge_ix in 0..axis.edges.len() {
90
63.3M
            let edge = &axis.edges[edge_ix];
91
63.3M
            let dist = (segment.pos as i32 - edge.fpos as i32).abs();
92
63.3M
            if dist < edge_distance_threshold && edge.dir == segment.dir && dist < best_dist {
93
635k
                if group == ScriptGroup::Default {
94
330k
                    best_edge_ix = Some(edge_ix);
95
330k
                    break;
96
305k
                }
97
                // For CJK, we add some additional checks
98
                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1073>
99
305k
                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
235k
                    let first_ix = edge.first_ix as usize;
103
235k
                    let mut seg1 = &axis.segments[first_ix];
104
235k
                    let mut dist2 = 0;
105
                    loop {
106
308k
                        if let Some(link1) = seg1.link(&axis.segments).copied() {
107
281k
                            dist2 = (link.pos as i32 - link1.pos as i32).abs();
108
281k
                            if dist2 >= edge_distance_threshold {
109
10.8k
                                break;
110
270k
                            }
111
27.7k
                        }
112
298k
                        if seg1.edge_next_ix == Some(first_ix as u16) {
113
224k
                            break;
114
73.8k
                        }
115
73.8k
                        if let Some(next) = seg1.next_in_edge(&axis.segments) {
116
73.8k
                            seg1 = next;
117
73.8k
                        } else {
118
0
                            break;
119
                        }
120
                    }
121
235k
                    if dist2 >= edge_distance_threshold {
122
10.8k
                        continue;
123
224k
                    }
124
69.9k
                }
125
294k
                best_dist = dist;
126
294k
                best_edge_ix = Some(edge_ix);
127
62.7M
            }
128
        }
129
13.0M
        if let Some(edge_ix) = best_edge_ix {
130
624k
            axis.append_segment_to_edge(segment_ix, edge_ix);
131
12.4M
        } else {
132
12.4M
            // We couldn't find an edge, so add a new one for this segment
133
12.4M
            let opos = fixed_mul(segment.pos as i32, scale);
134
12.4M
            let edge = Edge {
135
12.4M
                fpos: segment.pos,
136
12.4M
                opos,
137
12.4M
                pos: opos,
138
12.4M
                dir: segment.dir,
139
12.4M
                first_ix: segment_ix as u16,
140
12.4M
                last_ix: segment_ix as u16,
141
12.4M
                ..Default::default()
142
12.4M
            };
143
12.4M
            axis.insert_edge(edge, top_to_bottom_hinting);
144
12.4M
            axis.segments[segment_ix].edge_next_ix = Some(segment_ix as u16);
145
12.4M
        }
146
    }
147
3.85M
    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
9.36M
        for segment_ix in 0..axis.segments.len() {
151
9.36M
            let segment = &axis.segments[segment_ix];
152
9.36M
            if segment.dir != Direction::None {
153
8.82M
                continue;
154
549k
            }
155
            // Try to find an edge that coincides with this segment within the
156
            // threshold
157
549k
            if let Some(edge_ix) = axis
158
549k
                .edges
159
549k
                .iter()
160
549k
                .enumerate()
161
2.17M
                .filter_map(|(ix, edge)| {
162
2.17M
                    ((segment.pos as i32 - edge.fpos as i32).abs() < edge_distance_threshold)
163
2.17M
                        .then_some(ix)
164
2.17M
                })
165
549k
                .next()
166
12.0k
            {
167
12.0k
                // We found an edge, link everything up
168
12.0k
                axis.append_segment_to_edge(segment_ix, edge_ix);
169
537k
            }
170
        }
171
1.28M
    }
172
3.85M
    link_segments_to_edges(axis);
173
3.85M
    compute_edge_properties(axis);
174
3.85M
}
175
176
/// Edges get reordered as they're built so we need to assign edge indices to
177
/// segments in a second pass.
178
3.85M
fn link_segments_to_edges(axis: &mut Axis) {
179
3.85M
    let segments = axis.segments.as_mut_slice();
180
12.4M
    for edge_ix in 0..axis.edges.len() {
181
12.4M
        let edge = &axis.edges[edge_ix];
182
12.4M
        let mut ix = edge.first_ix as usize;
183
12.4M
        let last_ix = edge.last_ix as usize;
184
        loop {
185
13.0M
            let segment = &mut segments[ix];
186
13.0M
            segment.edge_ix = Some(edge_ix as u16);
187
13.0M
            if ix == last_ix {
188
12.4M
                break;
189
636k
            }
190
636k
            ix = segment
191
636k
                .edge_next_ix
192
636k
                .map(|ix| ix as usize)
193
636k
                .unwrap_or(last_ix);
194
        }
195
    }
196
3.85M
}
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>
202
3.85M
fn compute_edge_properties(axis: &mut Axis) {
203
3.85M
    let edges = axis.edges.as_mut_slice();
204
3.85M
    let segments = axis.segments.as_slice();
205
12.4M
    for edge_ix in 0..edges.len() {
206
12.4M
        let mut roundness = 0;
207
12.4M
        let mut straightness = 0;
208
12.4M
        let edge = edges[edge_ix];
209
12.4M
        let mut segment_ix = edge.first_ix as usize;
210
12.4M
        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
13.0M
            let edge = edges[edge_ix];
215
13.0M
            let segment = &segments[segment_ix];
216
13.0M
            let next_segment_ix = segment.edge_next_ix;
217
13.0M
            // Check roundness
218
13.0M
            if segment.flags & Segment::ROUND != 0 {
219
5.01M
                roundness += 1;
220
8.03M
            } else {
221
8.03M
                straightness += 1;
222
8.03M
            }
223
            // Check for serifs
224
13.0M
            let is_serif = if let Some(serif_ix) = segment.serif_ix {
225
884k
                let serif = &segments[serif_ix as usize];
226
884k
                serif.edge_ix.is_some() && serif.edge_ix != Some(edge_ix as u16)
227
            } else {
228
12.1M
                false
229
            };
230
            // Check for links
231
13.0M
            if is_serif
232
12.2M
                || (segment.link_ix.is_some()
233
10.5M
                    && segments[segment.link_ix.unwrap() as usize]
234
10.5M
                        .edge_ix
235
10.5M
                        .is_some())
236
            {
237
11.3M
                let (edge2_ix, segment2_ix) = if is_serif {
238
798k
                    (edge.serif_ix, segment.serif_ix)
239
                } else {
240
10.5M
                    (edge.link_ix, segment.link_ix)
241
                };
242
11.3M
                let edge2_ix = if let (Some(edge2_ix), Some(segment2_ix)) = (edge2_ix, segment2_ix)
243
                {
244
438k
                    let edge2 = &edges[edge2_ix as usize];
245
438k
                    let edge_delta = (edge.fpos as i32 - edge2.fpos as i32).abs();
246
438k
                    let segment2 = &segments[segment2_ix as usize];
247
438k
                    let segment_delta = (segment.pos as i32 - segment2.pos as i32).abs();
248
438k
                    if segment_delta < edge_delta {
249
34.9k
                        segment2.edge_ix
250
                    } else {
251
403k
                        Some(edge2_ix)
252
                    }
253
10.8M
                } else if let Some(segment2_ix) = segment2_ix {
254
10.8M
                    segments[segment2_ix as usize].edge_ix
255
                } else {
256
0
                    edge2_ix
257
                };
258
11.3M
                if is_serif {
259
798k
                    edges[edge_ix].serif_ix = edge2_ix;
260
798k
                    edges[edge2_ix.unwrap() as usize].flags |= Edge::SERIF;
261
10.5M
                } else {
262
10.5M
                    edges[edge_ix].link_ix = edge2_ix;
263
10.5M
                }
264
1.70M
            }
265
13.0M
            if segment_ix == last_segment_ix {
266
12.4M
                break;
267
636k
            }
268
636k
            segment_ix = next_segment_ix
269
636k
                .map(|ix| ix as usize)
270
636k
                .unwrap_or(last_segment_ix);
271
        }
272
12.4M
        let edge = &mut edges[edge_ix];
273
12.4M
        edge.flags = Edge::NORMAL;
274
12.4M
        if roundness > 0 && roundness >= straightness {
275
4.75M
            edge.flags |= Edge::ROUND;
276
7.65M
        }
277
        // Drop serifs for linked edges
278
12.4M
        if edge.serif_ix.is_some() && edge.link_ix.is_some() {
279
22.8k
            edge.serif_ix = None;
280
12.3M
        }
281
    }
282
3.85M
}
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>
289
2.09M
pub(crate) fn compute_blue_edges(
290
2.09M
    axis: &mut Axis,
291
2.09M
    scale: &Scale,
292
2.09M
    unscaled_blues: &[UnscaledBlue],
293
2.09M
    blues: &[ScaledBlue],
294
2.09M
    group: ScriptGroup,
295
2.09M
) {
296
2.09M
    // For the default script group, don't compute blues in the horizontal
297
2.09M
    // direction
298
2.09M
    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3572>
299
2.09M
    if axis.dim != Axis::VERTICAL && group == ScriptGroup::Default {
300
0
        return;
301
2.09M
    }
302
2.09M
    let axis_scale = if axis.dim == Axis::HORIZONTAL {
303
0
        scale.x_scale
304
    } else {
305
2.09M
        scale.y_scale
306
    };
307
    // Initial threshold
308
2.09M
    let initial_best_dest = fixed_mul(scale.units_per_em / 40, axis_scale).min(64 / 2);
309
9.28M
    for edge in &mut axis.edges {
310
7.18M
        let mut best_blue = None;
311
7.18M
        let mut best_is_neutral = false;
312
7.18M
        // Initial threshold as a fraction of em size with a max distance
313
7.18M
        // of 0.5 pixels
314
7.18M
        let mut best_dist = initial_best_dest;
315
14.4M
        for (unscaled_blue, blue) in unscaled_blues.iter().zip(blues) {
316
            // Ignore inactive blue zones
317
14.4M
            if !blue.is_active {
318
1.17M
                continue;
319
13.2M
            }
320
13.2M
            let is_top = blue.zones.is_top_like();
321
13.2M
            let is_neutral = blue.zones.is_neutral();
322
13.2M
            let is_major_dir = edge.dir == axis.major_dir;
323
13.2M
            // Both directions are handled for neutral blues
324
13.2M
            if is_top ^ is_major_dir || is_neutral {
325
                // Compare to reference position
326
7.27M
                let (ref_pos, matching_blue) = if group == ScriptGroup::Default {
327
7.16M
                    (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
110k
                    if (edge.fpos as i32 - unscaled_blue.position).abs()
333
110k
                        > (edge.fpos as i32 - unscaled_blue.overshoot).abs()
334
                    {
335
0
                        (unscaled_blue.overshoot, blue.overshoot)
336
                    } else {
337
110k
                        (unscaled_blue.position, blue.position)
338
                    }
339
                };
340
7.27M
                let dist = fixed_mul((edge.fpos as i32 - ref_pos).abs(), axis_scale);
341
7.27M
                if dist < best_dist {
342
2.05M
                    best_dist = dist;
343
2.05M
                    best_blue = Some(matching_blue);
344
2.05M
                    best_is_neutral = is_neutral;
345
5.22M
                }
346
7.27M
                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
7.16M
                    if edge.flags & Edge::ROUND != 0 && dist != 0 && !is_neutral {
351
1.77M
                        let is_under_ref = (edge.fpos as i32) < unscaled_blue.position;
352
1.77M
                        if is_top ^ is_under_ref {
353
305k
                            let dist = fixed_mul(
354
305k
                                (edge.fpos as i32 - unscaled_blue.overshoot).abs(),
355
305k
                                axis_scale,
356
305k
                            );
357
305k
                            if dist < best_dist {
358
8.28k
                                best_dist = dist;
359
8.28k
                                best_blue = Some(blue.overshoot);
360
8.28k
                                best_is_neutral = is_neutral;
361
297k
                            }
362
1.46M
                        }
363
5.39M
                    }
364
110k
                }
365
5.97M
            }
366
        }
367
7.18M
        if let Some(best_blue) = best_blue {
368
2.05M
            edge.blue_edge = Some(best_blue);
369
2.05M
            if best_is_neutral {
370
12.5k
                edge.flags |= Edge::NEUTRAL;
371
2.04M
            }
372
5.13M
        }
373
    }
374
2.09M
}
375
376
#[cfg(test)]
377
mod 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
}