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