Skip to main content

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