Skip to main content

skrifa/outline/autohint/hint/
edges.rs

1//! Edge hinting.
2//!
3//! Let's actually do some grid fitting. Here we align edges to the pixel
4//! grid. This is the final step before applying the edge adjustments to
5//! the original outline points.
6
7use super::super::{
8    metrics::{fixed_mul_div, pix_floor, pix_round, Scale, ScaledAxisMetrics, ScaledWidth},
9    recorder::{EdgeAction, HintsRecorder},
10    style::ScriptGroup,
11    topo::{Axis, Dimension, Edge, TopoFlags},
12    ScaleFlags,
13};
14
15/// Main Latin grid-fitting routine.
16///
17/// Note: this is one huge function in FreeType, broken up into several below.
18///
19/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2999>
20pub(crate) fn hint_edges(
21    axis: &mut Axis,
22    metrics: &ScaledAxisMetrics,
23    group: ScriptGroup,
24    scale: &Scale,
25    mut top_to_bottom_hinting: bool,
26    mut recorder: Option<&mut HintsRecorder>,
27) {
28    if axis.dim != Dimension::Vertical {
29        top_to_bottom_hinting = false;
30    }
31    // First align horizontal edges to blue zones if needed
32    let anchor_ix = align_edges_to_blues(axis, metrics, group, scale, recorder.as_deref_mut());
33    // Now align the stem edges
34    let (serif_count, anchor_ix) = align_stem_edges(
35        axis,
36        metrics,
37        group,
38        scale,
39        top_to_bottom_hinting,
40        anchor_ix,
41        recorder.as_deref_mut(),
42    );
43    let edges = axis.edges.as_mut_slice();
44    // Special case for lowercase m
45    if axis.dim == Dimension::Horizontal && (edges.len() == 6 || edges.len() == 12) {
46        hint_lowercase_m(edges, group);
47    }
48    // Handle serifs and single segment edges
49    if serif_count > 0 || anchor_ix.is_none() {
50        align_remaining_edges(
51            axis,
52            group,
53            top_to_bottom_hinting,
54            serif_count,
55            anchor_ix,
56            recorder,
57        );
58    }
59}
60
61/// Align horizontal edges to blue zones.
62///
63/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3030>
64fn align_edges_to_blues(
65    axis: &mut Axis,
66    metrics: &ScaledAxisMetrics,
67    group: ScriptGroup,
68    scale: &Scale,
69    mut recorder: Option<&mut HintsRecorder>,
70) -> Option<usize> {
71    let mut anchor_ix = None;
72    // For default script group, only do vertical blues
73    if group == ScriptGroup::Default && axis.dim != Dimension::Vertical {
74        return anchor_ix;
75    }
76    for edge_ix in 0..axis.edges.len() {
77        let mut linked_edge_to_align = None;
78        {
79            let edges = axis.edges.as_mut_slice();
80            let edge = &edges[edge_ix];
81            if edge.flags.contains(TopoFlags::DONE) {
82                continue;
83            }
84            let edge2_ix = edge.link_ix.map(|x| x as usize);
85            let edge2 = edge2_ix.map(|ix| &edges[ix]);
86            // If we have two neutral zones, skip one of them.
87            if let (true, Some(edge2)) = (edge.blue_edge.is_some(), edge2) {
88                if edge2.blue_edge.is_some() {
89                    let skip_ix = if edge2.flags.contains(TopoFlags::NEUTRAL) {
90                        edge2_ix
91                    } else if edge.flags.contains(TopoFlags::NEUTRAL) {
92                        Some(edge_ix)
93                    } else {
94                        None
95                    };
96                    if let Some(skip_ix) = skip_ix {
97                        let skip_edge = &mut edges[skip_ix];
98                        skip_edge.blue_edge = None;
99                        skip_edge.flags &= !TopoFlags::NEUTRAL;
100                    }
101                }
102            }
103            // Flip edges if the other is aligned to a blue zone
104            let blue = edges[edge_ix].blue_edge;
105            let (blue, edge1_ix, edge2_ix) = if let Some(blue) = blue {
106                (blue, Some(edge_ix), edge2_ix)
107            } else if let Some(edge2_blue) = edge2_ix.and_then(|ix| edges[ix].blue_edge) {
108                (edge2_blue, edge2_ix, Some(edge_ix))
109            } else {
110                (Default::default(), None, None)
111            };
112            let Some(edge1_ix) = edge1_ix else {
113                continue;
114            };
115            // Skip if edge1 was already positioned by a previous iteration
116            // (e.g. edge[i] has no blue but its linked-edge does, and that
117            // linked-edge was already processed when the loop visited it directly).
118            if edges[edge1_ix].flags.contains(TopoFlags::DONE) {
119                continue;
120            }
121            let edge1 = &mut edges[edge1_ix];
122            edge1.pos = blue.fitted;
123            edge1.flags |= TopoFlags::DONE;
124            if let Some(recorder) = recorder.as_mut() {
125                let action = if anchor_ix.is_none() {
126                    EdgeAction::BlueAnchor
127                } else {
128                    EdgeAction::Blue
129                };
130                recorder.record_edge(
131                    axis.dim,
132                    action,
133                    edge1_ix,
134                    anchor_ix.is_none().then_some(edge_ix),
135                    None,
136                    None,
137                    None,
138                    edges[edge1_ix].blue_provenance,
139                );
140            }
141            if let Some(edge2_ix) = edge2_ix {
142                if edges[edge2_ix].blue_edge.is_none() {
143                    edges[edge2_ix].flags |= TopoFlags::DONE;
144                    linked_edge_to_align = Some((edge1_ix, edge2_ix));
145                }
146            }
147        }
148        if let Some((edge1_ix, edge2_ix)) = linked_edge_to_align {
149            align_linked_edge(
150                axis,
151                metrics,
152                group,
153                scale,
154                edge1_ix,
155                edge2_ix,
156                recorder.as_deref_mut(),
157            );
158        }
159        if anchor_ix.is_none() {
160            anchor_ix = Some(edge_ix);
161        }
162    }
163    anchor_ix
164}
165
166/// Align stem edges, trying to main relative order of stems in the glyph.
167///
168/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3123>
169fn align_stem_edges(
170    axis: &mut Axis,
171    metrics: &ScaledAxisMetrics,
172    group: ScriptGroup,
173    scale: &Scale,
174    top_to_bottom_hinting: bool,
175    mut anchor_ix: Option<usize>,
176    mut recorder: Option<&mut HintsRecorder>,
177) -> (usize, Option<usize>) {
178    let mut serif_count = 0;
179    let mut last_stem_pos = None;
180    let mut delta = 0;
181    // Now align all other stem edges
182    // This code starts at: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3123>
183    for edge_ix in 0..axis.edges.len() {
184        // Read the values we need to decide on early-exit paths before taking
185        // any &mut borrow that would conflict with align_linked_edge(&mut axis).
186        let (edge_flags, edge_link_ix, edge_pos, edge2_pos_for_cjk, edge2_has_blue) = {
187            let edges = axis.edges.as_slice();
188            let edge = &edges[edge_ix];
189            let link_ix = edge.link_ix.map(|ix| ix as usize);
190            let (edge2_pos, edge2_has_blue) = link_ix
191                .map(|ix| (edges[ix].pos, edges[ix].blue_edge.is_some()))
192                .unwrap_or((0, false));
193            (edge.flags, link_ix, edge.pos, edge2_pos, edge2_has_blue)
194        };
195        if edge_flags.contains(TopoFlags::DONE) {
196            continue;
197        }
198        // Skip all non-stem edges
199        let Some(edge2_ix) = edge_link_ix else {
200            serif_count += 1;
201            continue;
202        };
203        // For CJK, skip stems that are too close. We'll deal with them later
204        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1912>
205        if group != ScriptGroup::Default {
206            if let Some(last_pos) = last_stem_pos {
207                if edge_pos < last_pos + 64 || edge2_pos_for_cjk < last_pos + 64 {
208                    serif_count += 1;
209                    continue;
210                }
211            }
212        }
213        // This should not happen, but match the C fallback.
214        if edge2_has_blue {
215            align_linked_edge(
216                axis,
217                metrics,
218                group,
219                scale,
220                edge2_ix,
221                edge_ix,
222                recorder.as_deref_mut(),
223            );
224            axis.edges[edge_ix].flags |= TopoFlags::DONE;
225            continue;
226        }
227        let edges = axis.edges.as_mut_slice();
228        if group == ScriptGroup::Default {
229            // Now align the stem
230            // Note: the branches here are reversed from the FreeType code
231            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3155>
232            if let Some(anchor_ix) = anchor_ix {
233                let anchor = &edges[anchor_ix];
234                let edge = edges[edge_ix];
235                let edge2 = edges[edge2_ix];
236                let original_pos = anchor.pos + (edge.opos - anchor.opos);
237                let original_len = edge2.opos - edge.opos;
238                let original_center = original_pos + (original_len >> 1);
239                let cur_len = stem_width(
240                    metrics,
241                    group,
242                    scale,
243                    original_len,
244                    0,
245                    edge.flags,
246                    edge2.flags,
247                );
248                if edge2.flags.contains(TopoFlags::DONE) {
249                    let new_pos = edge2.pos - cur_len;
250                    edges[edge_ix].pos = new_pos;
251                    if let Some(recorder) = recorder.as_mut() {
252                        recorder.record_edge(
253                            axis.dim,
254                            EdgeAction::Adjust,
255                            edge_ix,
256                            Some(edge2_ix),
257                            None,
258                            edge_ix.checked_sub(1),
259                            None,
260                            None,
261                        );
262                    }
263                } else if cur_len < 96 {
264                    let cur_pos1 = pix_round(original_center);
265                    let (u_off, d_off) = if cur_len <= 64 { (32, 32) } else { (38, 26) };
266                    let delta1 = (original_center - (cur_pos1 - u_off)).abs();
267                    let delta2 = (original_center - (cur_pos1 + d_off)).abs();
268                    let cur_pos1 = if delta1 < delta2 {
269                        cur_pos1 - u_off
270                    } else {
271                        cur_pos1 + d_off
272                    };
273                    edges[edge_ix].pos = cur_pos1 - cur_len / 2;
274                    edges[edge2_ix].pos = cur_pos1 + cur_len / 2;
275                    if let Some(recorder) = recorder.as_mut() {
276                        recorder.record_edge(
277                            axis.dim,
278                            EdgeAction::Stem,
279                            edge_ix,
280                            Some(edge2_ix),
281                            None,
282                            edge_ix.checked_sub(1),
283                            None,
284                            None,
285                        );
286                    }
287                } else {
288                    let cur_pos1 = pix_round(original_pos);
289                    let delta1 = (cur_pos1 + (cur_len >> 1) - original_center).abs();
290                    let cur_pos2 = pix_round(original_pos + original_len) - cur_len;
291                    let delta2 = (cur_pos2 + (cur_len >> 1) - original_center).abs();
292                    let new_pos = if delta1 < delta2 { cur_pos1 } else { cur_pos2 };
293                    let new_pos2 = new_pos + cur_len;
294                    edges[edge_ix].pos = new_pos;
295                    edges[edge2_ix].pos = new_pos2;
296                    if let Some(recorder) = recorder.as_mut() {
297                        recorder.record_edge(
298                            axis.dim,
299                            EdgeAction::Stem,
300                            edge_ix,
301                            Some(edge2_ix),
302                            None,
303                            edge_ix.checked_sub(1),
304                            None,
305                            None,
306                        );
307                    }
308                }
309                edges[edge_ix].flags |= TopoFlags::DONE;
310                edges[edge2_ix].flags |= TopoFlags::DONE;
311                if edge_ix > 0 {
312                    adjust_link(
313                        edges,
314                        axis.dim,
315                        edge_ix,
316                        LinkDir::Prev,
317                        top_to_bottom_hinting,
318                        recorder.as_deref_mut(),
319                    );
320                }
321            } else {
322                // No stem has been aligned yet
323                let edge = edges[edge_ix];
324                let edge2 = edges[edge2_ix];
325                let original_len = edge2.opos - edge.opos;
326                let cur_len = stem_width(
327                    metrics,
328                    group,
329                    scale,
330                    original_len,
331                    0,
332                    edge.flags,
333                    edge2.flags,
334                );
335                // Some "voodoo" to specially round edges for small stem widths
336                let (u_off, d_off) = if cur_len <= 64 {
337                    // width <= 1px
338                    (32, 32)
339                } else {
340                    // 1px < width < 1.5px
341                    (38, 26)
342                };
343                if cur_len < 96 {
344                    let original_center = edge.opos + (original_len >> 1);
345                    let mut cur_pos1 = pix_round(original_center);
346                    let error1 = (original_center - (cur_pos1 - u_off)).abs();
347                    let error2 = (original_center - (cur_pos1 + d_off)).abs();
348                    if error1 < error2 {
349                        cur_pos1 -= u_off;
350                    } else {
351                        cur_pos1 += d_off;
352                    }
353                    let edge_pos = cur_pos1 - cur_len / 2;
354                    edges[edge_ix].pos = edge_pos;
355                    edges[edge2_ix].pos = edge_pos + cur_len;
356                } else {
357                    edges[edge_ix].pos = pix_round(edge.opos);
358                }
359                edges[edge_ix].flags |= TopoFlags::DONE;
360                if let Some(recorder) = recorder.as_mut() {
361                    recorder.record_edge(
362                        axis.dim,
363                        EdgeAction::Anchor,
364                        edge_ix,
365                        Some(edge2_ix),
366                        None,
367                        None,
368                        None,
369                        None,
370                    );
371                }
372                align_linked_edge(
373                    axis,
374                    metrics,
375                    group,
376                    scale,
377                    edge_ix,
378                    edge2_ix,
379                    recorder.as_deref_mut(),
380                );
381                anchor_ix = Some(edge_ix);
382            }
383        } else {
384            // More CJK divergence
385            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1937>
386            if edge2_ix < edge_ix {
387                last_stem_pos = Some(edge_pos);
388                edges[edge_ix].flags |= TopoFlags::DONE;
389                align_linked_edge(
390                    axis,
391                    metrics,
392                    group,
393                    scale,
394                    edge2_ix,
395                    edge_ix,
396                    recorder.as_deref_mut(),
397                );
398                continue;
399            }
400            if axis.dim != Dimension::Vertical && anchor_ix.is_none() {
401                delta = hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta);
402            } else {
403                hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta);
404            }
405            anchor_ix = Some(edge_ix);
406            axis.edges[edge_ix].flags |= TopoFlags::DONE;
407            let edge2 = &mut axis.edges[edge2_ix];
408            edge2.flags |= TopoFlags::DONE;
409            last_stem_pos = Some(edge2.pos);
410        }
411    }
412    (serif_count, anchor_ix)
413}
414
415/// Make sure that lowercase m's maintain symmetry.
416///
417/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3365>
418fn hint_lowercase_m(edges: &mut [Edge], group: ScriptGroup) {
419    let (edge1_ix, edge2_ix, edge3_ix) = if edges.len() == 6 {
420        (0, 2, 4)
421    } else {
422        (1, 5, 9)
423    };
424    let edge1 = &edges[edge1_ix];
425    let edge2 = &edges[edge2_ix];
426    let edge3 = &edges[edge3_ix];
427    let dist1 = edge2.opos - edge1.opos;
428    let dist2 = edge3.opos - edge2.opos;
429    let span = (dist1 - dist2).abs();
430    if group != ScriptGroup::Default {
431        // CJK has additional conditions on the following...
432        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L2090>
433        for (edge, ix) in [(edge1, edge1_ix), (edge2, edge2_ix), (edge3, edge3_ix)] {
434            if edge.link_ix != Some((ix + 1) as u16) {
435                return;
436            }
437        }
438    }
439    if span < 8 {
440        let delta = edge3.pos - (2 * edge2.pos - edge1.pos);
441        let link_ix = edge3.link_ix.map(|ix| ix as usize);
442        let edge3 = &mut edges[edge3_ix];
443        edge3.pos -= delta;
444        edge3.flags |= TopoFlags::DONE;
445        if let Some(link_ix) = link_ix {
446            let link = &mut edges[link_ix];
447            link.pos -= delta;
448            link.flags |= TopoFlags::DONE;
449        }
450        // Move serifs along with the stem
451        if edges.len() == 12 {
452            edges[8].pos -= delta;
453            edges[11].pos -= delta;
454        }
455    }
456}
457
458/// Align serif and single segment edges.
459fn align_remaining_edges(
460    axis: &mut Axis,
461    group: ScriptGroup,
462    top_to_bottom_hinting: bool,
463    mut serif_count: usize,
464    mut anchor_ix: Option<usize>,
465    mut recorder: Option<&mut HintsRecorder>,
466) {
467    if group == ScriptGroup::Default {
468        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3418>
469        for edge_ix in 0..axis.edges.len() {
470            let (edge_flags, edge_opos, edge_serif_ix, delta) = {
471                let edges = axis.edges.as_slice();
472                let edge = &edges[edge_ix];
473                let delta = edge
474                    .serif(edges)
475                    .map(|serif| (serif.opos - edge.opos).abs())
476                    .unwrap_or(1000);
477                (edge.flags, edge.opos, edge.serif_ix, delta)
478            };
479            if edge_flags.contains(TopoFlags::DONE) {
480                continue;
481            }
482            if delta < 64 + 16 {
483                // delta is only < 1000 if edge.serif_ix is Some(_)
484                let serif_ix = edge_serif_ix.unwrap() as usize;
485                align_serif_edge(axis, serif_ix, edge_ix);
486                if let Some(recorder) = recorder.as_mut() {
487                    let edges = axis.edges.as_slice();
488                    let [lower_bound_ix, upper_bound_ix] = latin_remaining_bounds(edges, edge_ix);
489                    recorder.record_edge(
490                        axis.dim,
491                        EdgeAction::Serif,
492                        edge_ix,
493                        Some(serif_ix),
494                        None,
495                        lower_bound_ix,
496                        upper_bound_ix,
497                        None,
498                    );
499                }
500            } else if let Some(anchor_ix) = anchor_ix {
501                let edges = axis.edges.as_mut_slice();
502                let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix);
503                if let Some((before_ix, after_ix)) = before_ix.zip(after_ix) {
504                    let before = &edges[before_ix];
505                    let after = &edges[after_ix];
506                    let new_pos = if after.opos == before.opos {
507                        before.pos
508                    } else {
509                        before.pos
510                            + fixed_mul_div(
511                                edge_opos - before.opos,
512                                after.pos - before.pos,
513                                after.opos - before.opos,
514                            )
515                    };
516                    edges[edge_ix].pos = new_pos;
517                    if let Some(recorder) = recorder.as_mut() {
518                        let [lower_bound_ix, upper_bound_ix] =
519                            latin_remaining_bounds(edges, edge_ix);
520                        recorder.record_edge(
521                            axis.dim,
522                            EdgeAction::SerifLink1,
523                            edge_ix,
524                            Some(before_ix),
525                            Some(after_ix),
526                            lower_bound_ix,
527                            upper_bound_ix,
528                            None,
529                        );
530                    }
531                } else {
532                    let anchor = &edges[anchor_ix];
533                    let new_pos = anchor.pos + ((edge_opos - anchor.opos + 16) & !31);
534                    edges[edge_ix].pos = new_pos;
535                    if let Some(recorder) = recorder.as_mut() {
536                        let [lower_bound_ix, upper_bound_ix] =
537                            latin_remaining_bounds(edges, edge_ix);
538                        recorder.record_edge(
539                            axis.dim,
540                            EdgeAction::SerifLink2,
541                            edge_ix,
542                            None,
543                            None,
544                            lower_bound_ix,
545                            upper_bound_ix,
546                            None,
547                        );
548                    }
549                }
550            } else {
551                anchor_ix = Some(edge_ix);
552                let edges = axis.edges.as_mut_slice();
553                let new_pos = pix_round(edge_opos);
554                edges[edge_ix].pos = new_pos;
555                if let Some(recorder) = recorder.as_mut() {
556                    let [lower_bound_ix, upper_bound_ix] = latin_remaining_bounds(edges, edge_ix);
557                    recorder.record_edge(
558                        axis.dim,
559                        EdgeAction::SerifAnchor,
560                        edge_ix,
561                        None,
562                        None,
563                        lower_bound_ix,
564                        upper_bound_ix,
565                        None,
566                    );
567                }
568            }
569            let edges = &mut axis.edges;
570            edges[edge_ix].flags |= TopoFlags::DONE;
571            adjust_link(
572                edges,
573                axis.dim,
574                edge_ix,
575                LinkDir::Prev,
576                top_to_bottom_hinting,
577                recorder.as_deref_mut(),
578            );
579            adjust_link(
580                edges,
581                axis.dim,
582                edge_ix,
583                LinkDir::Next,
584                top_to_bottom_hinting,
585                recorder.as_deref_mut(),
586            );
587        }
588    } else {
589        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L2119>
590        for edge_ix in 0..axis.edges.len() {
591            let edge = &mut axis.edges[edge_ix];
592            if edge.flags.contains(TopoFlags::DONE) {
593                continue;
594            }
595            if let Some(serif_ix) = edge.serif_ix.map(|ix| ix as usize) {
596                edge.flags |= TopoFlags::DONE;
597                align_serif_edge(axis, serif_ix, edge_ix);
598                serif_count = serif_count.saturating_sub(1);
599            }
600        }
601        if serif_count == 0 {
602            return;
603        }
604        for edge_ix in 0..axis.edges.len() {
605            let edges = axis.edges.as_mut_slice();
606            let edge = &edges[edge_ix];
607            if edge.flags.contains(TopoFlags::DONE) {
608                continue;
609            }
610            let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix);
611            match (before_ix, after_ix) {
612                (Some(before_ix), None) => {
613                    align_serif_edge(axis, before_ix, edge_ix);
614                }
615                (None, Some(after_ix)) => {
616                    align_serif_edge(axis, after_ix, edge_ix);
617                }
618                (Some(before_ix), Some(after_ix)) => {
619                    let before = edges[before_ix];
620                    let after = edges[after_ix];
621                    if after.fpos == before.fpos {
622                        edges[edge_ix].pos = before.pos;
623                    } else {
624                        edges[edge_ix].pos = before.pos
625                            + fixed_mul_div(
626                                edge.fpos as i32 - before.fpos as i32,
627                                after.pos - before.pos,
628                                after.fpos as i32 - before.fpos as i32,
629                            );
630                    }
631                }
632                _ => {}
633            }
634        }
635    }
636}
637
638#[derive(Copy, Clone, PartialEq)]
639enum LinkDir {
640    Prev,
641    Next,
642}
643
644/// Helper to adjust links based on hinting direction.
645///
646/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3499>
647fn adjust_link(
648    edges: &mut [Edge],
649    dim: Dimension,
650    edge_ix: usize,
651    link_dir: LinkDir,
652    top_to_bottom_hinting: bool,
653    mut recorder: Option<&mut HintsRecorder>,
654) -> Option<()> {
655    let edge = &edges[edge_ix];
656    let (edge2, prev_edge) = if link_dir == LinkDir::Next {
657        let edge2 = edges.get(edge_ix + 1)?;
658        // Don't adjust next edge if it's not done yet
659        if !edge2.flags.contains(TopoFlags::DONE) {
660            return None;
661        }
662        (edge2, edges.get(edge_ix.checked_sub(1)?)?)
663    } else {
664        let edge = edges.get(edge_ix.checked_sub(1)?)?;
665        (edge, edge)
666    };
667    let pos1 = edge.pos;
668    let pos2 = edge2.pos;
669    let order_check = match (link_dir, top_to_bottom_hinting) {
670        (LinkDir::Prev, true) | (LinkDir::Next, false) => pos1 > pos2,
671        (LinkDir::Prev, false) | (LinkDir::Next, true) => pos1 < pos2,
672    };
673    if !order_check {
674        return None;
675    }
676    let link = edge.link(edges)?;
677    if (link.pos - prev_edge.pos).abs() > 16 {
678        let new_pos = edge2.pos;
679        edges[edge_ix].pos = new_pos;
680        if let Some(recorder) = recorder.as_mut() {
681            recorder.record_edge(
682                dim,
683                EdgeAction::Bound,
684                edge_ix,
685                None,
686                None,
687                None,
688                None,
689                None,
690            );
691        }
692    }
693    Some(())
694}
695
696fn latin_remaining_bounds(edges: &[Edge], edge_ix: usize) -> [Option<usize>; 2] {
697    let lower_bound_ix = edge_ix.checked_sub(1);
698    let upper_bound_ix = (edge_ix + 1 < edges.len()
699        && edges[edge_ix + 1].flags.contains(TopoFlags::DONE))
700    .then_some(edge_ix + 1);
701    [lower_bound_ix, upper_bound_ix]
702}
703
704/// Returns the indices of the "completed" edges before and after the given
705/// edge index.
706fn find_bounding_completed_edges(edges: &[Edge], ix: usize) -> [Option<usize>; 2] {
707    let before_ix = edges
708        .get(..ix)
709        .unwrap_or_default()
710        .iter()
711        .enumerate()
712        .rev()
713        .filter_map(|(ix, edge)| edge.flags.contains(TopoFlags::DONE).then_some(ix))
714        .next();
715    let after_ix = edges
716        .iter()
717        .enumerate()
718        .skip(ix + 1)
719        .filter_map(|(ix, edge)| edge.flags.contains(TopoFlags::DONE).then_some(ix))
720        .next();
721    [before_ix, after_ix]
722}
723
724/// Snap a scaled width to one of the standard widths.
725///
726/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2697>
727fn snap_width(widths: &[ScaledWidth], width: i32) -> i32 {
728    let (_, ref_width) =
729        widths
730            .iter()
731            .fold((64 + 32 + 2, width), |(best_dist, ref_width), candidate| {
732                let dist = (width - candidate.scaled).abs();
733                if dist < best_dist {
734                    (dist, candidate.scaled)
735                } else {
736                    (best_dist, ref_width)
737                }
738            });
739    let scaled = pix_round(ref_width);
740    if width >= ref_width {
741        if width < scaled + 48 {
742            ref_width
743        } else {
744            width
745        }
746    } else if width > scaled - 48 {
747        ref_width
748    } else {
749        width
750    }
751}
752
753/// Compute the snapped width of a given stem.
754///
755/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2746>
756fn stem_width(
757    metrics: &ScaledAxisMetrics,
758    group: ScriptGroup,
759    scale: &Scale,
760    width: i32,
761    base_delta: i32,
762    base_flags: TopoFlags,
763    stem_flags: TopoFlags,
764) -> i32 {
765    if !scale.flags.contains(ScaleFlags::STEM_ADJUST)
766        || (group == ScriptGroup::Default && metrics.width_metrics.is_extra_light)
767    {
768        return width;
769    }
770    let is_vertical = metrics.dim == Dimension::Vertical;
771    let sign = if width < 0 { -1 } else { 1 };
772    let mut dist = width.abs();
773    if (is_vertical && !scale.flags.contains(ScaleFlags::VERTICAL_SNAP))
774        || (!is_vertical && !scale.flags.contains(ScaleFlags::HORIZONTAL_SNAP))
775    {
776        // Do smooth hinting
777        if group == ScriptGroup::Default {
778            if stem_flags.contains(TopoFlags::SERIF) && is_vertical && (dist < 3 * 64) {
779                // Don't touch widths of serifs
780                return dist * sign;
781            } else if base_flags.contains(TopoFlags::ROUND) {
782                if dist < 80 {
783                    dist = 64;
784                }
785            } else if dist < 56 {
786                dist = 56;
787            }
788        }
789        if !metrics.widths.is_empty() {
790            // Compare to standard width
791            let min_width = metrics.widths[0].scaled;
792            let delta = (dist - min_width).abs();
793            if delta < 40 {
794                dist = min_width.max(48);
795                return dist * sign;
796            }
797            if group == ScriptGroup::Default {
798                // Default/Latin behavior
799                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2809>
800                if dist < 3 * 64 {
801                    let delta = dist & 63;
802                    dist &= -64;
803                    if delta < 10 {
804                        dist += delta;
805                    } else if delta < 32 {
806                        dist += 10;
807                    } else if delta < 54 {
808                        dist += 54;
809                    } else {
810                        dist += delta;
811                    }
812                } else {
813                    let mut new_base_delta = 0;
814                    if (width > 0 && base_delta > 0) || (width < 0 && base_delta < 0) {
815                        if scale.size < 10.0 {
816                            new_base_delta = base_delta;
817                        } else if scale.size < 30.0 {
818                            new_base_delta = (base_delta * (30.0 - scale.size) as i32) / 20;
819                        }
820                    }
821                    dist = (dist - new_base_delta.abs() + 32) & !63;
822                }
823            }
824        }
825        if group != ScriptGroup::Default {
826            // Divergent CJK behavior
827            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1544>
828            if dist < 54 {
829                dist += (54 - dist) / 2;
830            } else if dist < 3 * 64 {
831                let delta = dist & 63;
832                dist &= -64;
833                if delta < 10 {
834                    dist += delta;
835                } else if delta < 22 {
836                    dist += 10;
837                } else if delta < 42 {
838                    dist += delta;
839                } else if delta < 54 {
840                    dist += 54;
841                } else {
842                    dist += delta;
843                }
844            }
845        }
846    } else {
847        // Do strong hinting: snap to integer pixels
848        let original_dist = dist;
849        dist = snap_width(&metrics.widths, dist);
850        if is_vertical {
851            // Always round to integers in the vertical case
852            if dist >= 64 {
853                dist = (dist + 16) & !63;
854            } else {
855                dist = 64;
856            }
857        } else if scale.flags.contains(ScaleFlags::MONO) {
858            // Mono horizontal hinting: snap to integer with different
859            // threshold
860            if dist < 64 {
861                dist = 64;
862            } else {
863                dist = (dist + 32) & !63;
864            }
865        } else {
866            // Smooth horizontal hinting: strengthen small stems, round
867            // stems whose size is between 1 and 2 pixels
868            if dist < 48 {
869                dist = (dist + 64) >> 1;
870            } else if dist < 128 {
871                // Only round to integer if distortion is less than
872                // 1/4 pixel
873                dist = (dist + 22) & !63;
874                if group == ScriptGroup::Default {
875                    // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2914>
876                    let delta = (dist - original_dist).abs();
877                    if delta >= 16 {
878                        dist = original_dist;
879                        if dist < 48 {
880                            dist = (dist + 64) >> 1;
881                        }
882                    }
883                }
884            } else {
885                // Round otherwise to prevent color fringes in LCD mode
886                dist = (dist + 32) & !63;
887            }
888        }
889    }
890    dist * sign
891}
892
893/// Align one stem edge relative to previous stem edge.
894///
895/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2943>
896fn align_linked_edge(
897    axis: &mut Axis,
898    metrics: &ScaledAxisMetrics,
899    group: ScriptGroup,
900    scale: &Scale,
901    base_edge_ix: usize,
902    stem_edge_ix: usize,
903    mut recorder: Option<&mut HintsRecorder>,
904) {
905    let edges = axis.edges.as_mut_slice();
906    let base_edge = &edges[base_edge_ix];
907    let stem_edge = &edges[stem_edge_ix];
908    let width = stem_edge.opos - base_edge.opos;
909    let base_delta = base_edge.pos - base_edge.opos;
910    let fitted_width = stem_width(
911        metrics,
912        group,
913        scale,
914        width,
915        base_delta,
916        base_edge.flags,
917        stem_edge.flags,
918    );
919    edges[stem_edge_ix].pos = base_edge.pos + fitted_width;
920    if let Some(recorder) = recorder.as_mut() {
921        recorder.record_edge(
922            axis.dim,
923            EdgeAction::Link,
924            base_edge_ix,
925            Some(stem_edge_ix),
926            None,
927            None,
928            None,
929            None,
930        );
931    }
932}
933
934/// Shift the serif edge by the adjustment made to base edge.
935///
936/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2975>
937fn align_serif_edge(axis: &mut Axis, base_edge_ix: usize, serif_edge_ix: usize) {
938    let edges = axis.edges.as_mut_slice();
939    let base_edge = &edges[base_edge_ix];
940    let serif_edge = &edges[serif_edge_ix];
941    edges[serif_edge_ix].pos = base_edge.pos + (serif_edge.opos - base_edge.opos);
942}
943
944/// Adjusts both edges of a stem and returns the delta.
945///
946/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1678>
947fn hint_normal_stem_cjk(
948    axis: &mut Axis,
949    metrics: &ScaledAxisMetrics,
950    group: ScriptGroup,
951    scale: &Scale,
952    edge_ix: usize,
953    edge2_ix: usize,
954    anchor: i32,
955) -> i32 {
956    const MAX_HORIZONTAL_GAP: i32 = 9;
957    const MAX_VERTICAL_GAP: i32 = 15;
958    const MAX_DELTA_ABS: i32 = 14;
959    let edge = axis.edges[edge_ix];
960    let edge2 = axis.edges[edge2_ix];
961    let do_stem_adjust = scale.flags.contains(ScaleFlags::STEM_ADJUST);
962    let threshold_delta = if do_stem_adjust {
963        0
964    } else {
965        let delta = if axis.dim == Dimension::Vertical {
966            MAX_HORIZONTAL_GAP
967        } else {
968            MAX_VERTICAL_GAP
969        };
970        if edge.flags.contains(TopoFlags::ROUND) && edge2.flags.contains(TopoFlags::ROUND) {
971            delta
972        } else {
973            delta / 3
974        }
975    };
976    let threshold = 64 - threshold_delta;
977    let original_len = edge2.opos - edge.opos;
978    let cur_len = stem_width(
979        metrics,
980        group,
981        scale,
982        original_len,
983        0,
984        edge.flags,
985        edge2.flags,
986    );
987    let original_center = (edge.opos + edge2.opos) / 2 + anchor;
988    let cur_pos1 = original_center - cur_len / 2;
989    let cur_pos2 = cur_pos1 + cur_len;
990    let mut finish = |mut delta: i32| {
991        if !do_stem_adjust {
992            delta = delta.clamp(-MAX_DELTA_ABS, MAX_DELTA_ABS);
993        }
994        let adjustment = cur_pos1 + delta;
995        if edge.opos < edge2.opos {
996            axis.edges[edge_ix].pos = adjustment;
997            axis.edges[edge2_ix].pos = adjustment + cur_len;
998        } else {
999            axis.edges[edge2_ix].pos = adjustment;
1000            axis.edges[edge_ix].pos = adjustment + cur_len;
1001        }
1002        delta
1003    };
1004    let mut d_off1 = cur_pos1 - pix_floor(cur_pos1);
1005    let mut d_off2 = cur_pos2 - pix_floor(cur_pos2);
1006    let mut delta = 0;
1007    if d_off1 == 0 || d_off2 == 0 {
1008        return finish(delta);
1009    }
1010    let mut u_off1 = 64 - d_off1;
1011    let mut u_off2 = 64 - d_off2;
1012    if cur_len <= threshold {
1013        if d_off2 < cur_len {
1014            delta = if u_off1 <= d_off2 { u_off1 } else { -d_off2 };
1015        }
1016        return finish(delta);
1017    }
1018    if threshold < 64
1019        && (d_off1 >= threshold
1020            || u_off1 >= threshold
1021            || d_off2 >= threshold
1022            || u_off2 >= threshold)
1023    {
1024        return finish(delta);
1025    }
1026    let mut offset = cur_len & 63;
1027    if offset < 32 {
1028        if u_off1 <= offset || d_off2 <= offset {
1029            return finish(delta);
1030        }
1031    } else {
1032        offset = 64 - threshold;
1033    }
1034    d_off1 = threshold - u_off1;
1035    u_off1 -= offset;
1036    u_off2 = threshold - d_off2;
1037    d_off2 -= offset;
1038    if d_off1 <= u_off1 {
1039        u_off1 = -d_off1;
1040    }
1041    if d_off2 <= u_off2 {
1042        u_off2 = -d_off2;
1043    }
1044    if u_off1.abs() <= u_off2.abs() {
1045        delta = u_off1;
1046    } else {
1047        delta = u_off2;
1048    }
1049    finish(delta)
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::{
1055        super::super::{
1056            metrics,
1057            outline::Outline,
1058            shape::{Shaper, ShaperMode},
1059            style, topo,
1060        },
1061        *,
1062    };
1063    use crate::{attribute::Style, MetadataProvider};
1064    use raw::{types::GlyphId, FontRef, TableProvider};
1065
1066    #[test]
1067    fn edge_hinting_default() {
1068        let expected_h_edges = [
1069            (0, TopoFlags::DONE | TopoFlags::ROUND),
1070            (133, TopoFlags::DONE),
1071            (187, TopoFlags::DONE),
1072            (192, TopoFlags::DONE | TopoFlags::ROUND),
1073        ];
1074        let expected_v_edges = [
1075            (-256, TopoFlags::DONE),
1076            (463, TopoFlags::DONE),
1077            (576, TopoFlags::DONE | TopoFlags::ROUND | TopoFlags::SERIF),
1078            (633, TopoFlags::DONE),
1079        ];
1080        check_edges(
1081            font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
1082            GlyphId::new(9),
1083            style::StyleClass::HEBR,
1084            &expected_h_edges,
1085            &expected_v_edges,
1086        );
1087    }
1088
1089    #[test]
1090    fn edge_hinting_cjk() {
1091        let expected_h_edges = [
1092            (128, TopoFlags::DONE),
1093            (193, TopoFlags::DONE),
1094            (473, TopoFlags::NORMAL),
1095            (594, TopoFlags::NORMAL),
1096            (704, TopoFlags::DONE),
1097            (673, TopoFlags::DONE),
1098            (767, TopoFlags::DONE),
1099            (832, TopoFlags::DONE),
1100            (896, TopoFlags::DONE),
1101        ];
1102        let expected_v_edges = [
1103            (-64, TopoFlags::DONE | TopoFlags::ROUND),
1104            (15, TopoFlags::ROUND),
1105            (142, TopoFlags::ROUND),
1106            (546, TopoFlags::DONE),
1107            (624, TopoFlags::DONE),
1108            (576, TopoFlags::DONE),
1109            (720, TopoFlags::DONE),
1110            (768, TopoFlags::DONE),
1111            (799, TopoFlags::ROUND),
1112        ];
1113        check_edges(
1114            font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
1115            GlyphId::new(9),
1116            style::StyleClass::HANI,
1117            &expected_h_edges,
1118            &expected_v_edges,
1119        );
1120    }
1121
1122    fn check_edges(
1123        font_data: &[u8],
1124        glyph_id: GlyphId,
1125        class: usize,
1126        expected_h_edges: &[(i32, TopoFlags)],
1127        expected_v_edges: &[(i32, TopoFlags)],
1128    ) {
1129        let font = FontRef::new(font_data).unwrap();
1130        let shaper = Shaper::new(&font, ShaperMode::Nominal);
1131        let class = &style::STYLE_CLASSES[class];
1132        let unscaled_metrics =
1133            metrics::compute_unscaled_style_metrics(&shaper, &[], class, Default::default());
1134        let scale = metrics::Scale::new(
1135            16.0,
1136            font.head().unwrap().units_per_em() as i32,
1137            Style::Normal,
1138            Default::default(),
1139            class.script.group,
1140        );
1141        let scaled_metrics =
1142            metrics::scale_style_metrics(&unscaled_metrics, scale, Default::default());
1143        let glyphs = font.outline_glyphs();
1144        let glyph = glyphs.get(glyph_id).unwrap();
1145        let mut outline = Outline::default();
1146        outline.fill(&glyph, &[], Default::default()).unwrap();
1147        let mut axes = [
1148            Axis::new(Dimension::Horizontal, outline.orientation),
1149            Axis::new(Dimension::Vertical, outline.orientation),
1150        ];
1151        for axis in axes.iter_mut() {
1152            topo::compute_segments(&mut outline, axis, class.script.group);
1153            let dim = axis.dim;
1154            topo::link_segments(
1155                &outline,
1156                axis,
1157                scaled_metrics.axes[dim].scale,
1158                class.script.group,
1159                unscaled_metrics.axes[dim].max_width(),
1160            );
1161            topo::compute_edges(
1162                axis,
1163                &scaled_metrics.axes[0],
1164                class.script.hint_top_to_bottom,
1165                scaled_metrics.axes[1].scale,
1166                class.script.group,
1167            );
1168            if dim == Dimension::Vertical {
1169                topo::compute_blue_edges(
1170                    axis,
1171                    &scale,
1172                    &unscaled_metrics.axes[dim].blues,
1173                    &scaled_metrics.axes[dim].blues,
1174                    class.script.group,
1175                );
1176            }
1177            hint_edges(
1178                axis,
1179                &scaled_metrics.axes[dim],
1180                class.script.group,
1181                &scale,
1182                class.script.hint_top_to_bottom,
1183                None,
1184            );
1185        }
1186        // Only pos and flags fields are modified by edge hinting
1187        let h_edges = axes[Dimension::Horizontal]
1188            .edges
1189            .iter()
1190            .map(|edge| (edge.pos, edge.flags))
1191            .collect::<Vec<_>>();
1192        let v_edges = axes[Dimension::Vertical]
1193            .edges
1194            .iter()
1195            .map(|edge| (edge.pos, edge.flags))
1196            .collect::<Vec<_>>();
1197        assert_eq!(h_edges, expected_h_edges);
1198        assert_eq!(v_edges, expected_v_edges);
1199    }
1200}