skrifa/outline/autohint/metrics/
widths.rs

1//! Latin standard stem width computation.
2
3use super::super::{
4    derived_constant,
5    metrics::{self, UnscaledWidths, WidthMetrics, MAX_WIDTHS},
6    outline::Outline,
7    shape::{ShapedCluster, Shaper},
8    style::{ScriptGroup, StyleClass},
9    topo::{compute_segments, link_segments, Axis},
10};
11use crate::MetadataProvider;
12use raw::{types::F2Dot14, TableProvider};
13
14/// Compute all stem widths and initialize standard width and height for the
15/// given script.
16///
17/// Returns width metrics and unscaled widths for each dimension.
18///
19/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L54>
20pub(super) fn compute_widths(
21    shaper: &Shaper,
22    coords: &[F2Dot14],
23    style: &StyleClass,
24) -> [(WidthMetrics, UnscaledWidths); 2] {
25    let mut result: [(WidthMetrics, UnscaledWidths); 2] = Default::default();
26    let font = shaper.font();
27    let glyphs = font.outline_glyphs();
28    let units_per_em = font
29        .head()
30        .map(|head| head.units_per_em() as i32)
31        .unwrap_or_default();
32    let mut outline = Outline::default();
33    let mut axis = Axis::default();
34    let mut cluster_shaper = shaper.cluster_shaper(style);
35    let mut shaped_cluster = ShapedCluster::default();
36    // We take the first available glyph from the standard character set.
37    let glyph = style
38        .script
39        .std_chars
40        .split(' ')
41        .filter_map(|cluster| {
42            cluster_shaper.shape(cluster, &mut shaped_cluster);
43            // Reject input that maps to more than a single glyph
44            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L128>
45            match shaped_cluster.as_slice() {
46                [glyph] if glyph.id.to_u32() != 0 => glyphs.get(glyph.id),
47                _ => None,
48            }
49        })
50        .next();
51    if let Some(glyph) = glyph {
52        if outline.fill(&glyph, coords).is_ok() && !outline.points.is_empty() {
53            // Now process each dimension
54            for (dim, (_metrics, widths)) in result.iter_mut().enumerate() {
55                axis.reset(dim, outline.orientation);
56                // Segment computation for widths always uses the default
57                // script group
58                compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
59                link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
60                let segments = axis.segments.as_slice();
61                for (segment_ix, segment) in segments.iter().enumerate() {
62                    let segment_ix = segment_ix as u16;
63                    let Some(link_ix) = segment.link_ix else {
64                        continue;
65                    };
66                    let link = &segments[link_ix as usize];
67                    if link_ix > segment_ix && link.link_ix == Some(segment_ix) {
68                        let dist = (segment.pos as i32 - link.pos as i32).abs();
69                        if widths.len() < MAX_WIDTHS {
70                            widths.push(dist);
71                        } else {
72                            break;
73                        }
74                    }
75                }
76                // FreeTypes `af_sort_and_quantize_widths()` has the side effect
77                // of always updating the width count to 1 when we don't find
78                // any...
79                // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L121>
80                if widths.is_empty() {
81                    widths.push(0);
82                }
83                // The value 100 is heuristic
84                metrics::sort_and_quantize_widths(widths, units_per_em / 100);
85            }
86        }
87    }
88    for (metrics, widths) in result.iter_mut() {
89        // Now set derived values
90        let stdw = widths
91            .first()
92            .copied()
93            .unwrap_or_else(|| derived_constant(units_per_em, 50));
94        // Heuristic value of 20% of the smallest width
95        metrics.edge_distance_threshold = stdw / 5;
96        metrics.standard_width = stdw;
97        metrics.is_extra_light = false;
98    }
99    result
100}
101
102#[cfg(test)]
103mod tests {
104    use super::{
105        super::super::{shape::ShaperMode, style},
106        *,
107    };
108    use raw::FontRef;
109
110    #[test]
111    fn computed_widths() {
112        // Expected data produced by internal routines in FreeType. Scraped
113        // from a debugger
114        check_widths(
115            font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
116            super::StyleClass::HEBR,
117            [
118                (
119                    WidthMetrics {
120                        edge_distance_threshold: 10,
121                        standard_width: 54,
122                        is_extra_light: false,
123                    },
124                    &[54],
125                ),
126                (
127                    WidthMetrics {
128                        edge_distance_threshold: 4,
129                        standard_width: 21,
130                        is_extra_light: false,
131                    },
132                    &[21, 109],
133                ),
134            ],
135        );
136    }
137
138    #[test]
139    fn fallback_widths() {
140        // Expected data produced by internal routines in FreeType. Scraped
141        // from a debugger
142        check_widths(
143            font_test_data::CANTARELL_VF_TRIMMED,
144            super::StyleClass::LATN,
145            [
146                (
147                    WidthMetrics {
148                        edge_distance_threshold: 4,
149                        standard_width: 24,
150                        is_extra_light: false,
151                    },
152                    &[],
153                ),
154                (
155                    WidthMetrics {
156                        edge_distance_threshold: 4,
157                        standard_width: 24,
158                        is_extra_light: false,
159                    },
160                    &[],
161                ),
162            ],
163        );
164    }
165
166    #[test]
167    fn cjk_computed_widths() {
168        // Expected data produced by internal routines in FreeType. Scraped
169        // from a debugger
170        check_widths(
171            font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
172            super::StyleClass::HANI,
173            [
174                (
175                    WidthMetrics {
176                        edge_distance_threshold: 13,
177                        standard_width: 65,
178                        is_extra_light: false,
179                    },
180                    &[65],
181                ),
182                (
183                    WidthMetrics {
184                        edge_distance_threshold: 5,
185                        standard_width: 29,
186                        is_extra_light: false,
187                    },
188                    &[29],
189                ),
190            ],
191        );
192    }
193
194    fn check_widths(font_data: &[u8], style_class: usize, expected: [(WidthMetrics, &[i32]); 2]) {
195        let font = FontRef::new(font_data).unwrap();
196        let shaper = Shaper::new(&font, ShaperMode::Nominal);
197        let script = &style::STYLE_CLASSES[style_class];
198        let [(hori_metrics, hori_widths), (vert_metrics, vert_widths)] =
199            compute_widths(&shaper, Default::default(), script);
200        assert_eq!(hori_metrics, expected[0].0);
201        assert_eq!(hori_widths.as_slice(), expected[0].1);
202        assert_eq!(vert_metrics, expected[1].0);
203        assert_eq!(vert_widths.as_slice(), expected[1].1);
204    }
205}