skrifa/outline/autohint/metrics/
mod.rs

1//! Autohinting specific metrics.
2
3mod blues;
4mod scale;
5mod widths;
6
7use super::{
8    super::Target,
9    shape::{Shaper, ShaperMode},
10    style::{GlyphStyleMap, ScriptGroup, StyleClass},
11    topo::Dimension,
12};
13use crate::{attribute::Style, collections::SmallVec, FontRef};
14use alloc::vec::Vec;
15use raw::types::{F2Dot14, Fixed, GlyphId};
16#[cfg(feature = "std")]
17use std::sync::{Arc, RwLock};
18
19pub(crate) use blues::{BlueZones, ScaledBlue, ScaledBlues, UnscaledBlue, UnscaledBlues};
20pub(crate) use scale::{compute_unscaled_style_metrics, scale_style_metrics};
21
22/// Maximum number of widths, same for Latin and CJK.
23///
24/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L65>
25/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L55>
26pub(crate) const MAX_WIDTHS: usize = 16;
27
28/// Unscaled metrics for a single axis.
29///
30/// This is the union of the Latin and CJK axis records.
31///
32/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L88>
33/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L73>
34#[derive(Clone, Default, Debug)]
35pub(crate) struct UnscaledAxisMetrics {
36    pub dim: Dimension,
37    pub widths: UnscaledWidths,
38    pub width_metrics: WidthMetrics,
39    pub blues: UnscaledBlues,
40}
41
42impl UnscaledAxisMetrics {
43    pub fn max_width(&self) -> Option<i32> {
44        self.widths.last().copied()
45    }
46}
47
48/// Scaled metrics for a single axis.
49#[derive(Clone, Default, Debug)]
50pub(crate) struct ScaledAxisMetrics {
51    pub dim: Dimension,
52    /// Font unit to 26.6 scale in the axis direction.
53    pub scale: i32,
54    /// 1/64 pixel delta in the axis direction.
55    pub delta: i32,
56    pub widths: ScaledWidths,
57    pub width_metrics: WidthMetrics,
58    pub blues: ScaledBlues,
59}
60
61/// Unscaled metrics for a single style and script.
62///
63/// This is the union of the root, Latin and CJK style metrics but
64/// the latter two are actually identical.
65///
66/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aftypes.h#L413>
67/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L109>
68/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L95>
69#[derive(Clone, Default, Debug)]
70pub(crate) struct UnscaledStyleMetrics {
71    /// Index of style class.
72    pub class_ix: u16,
73    /// Monospaced digits?
74    pub digits_have_same_width: bool,
75    /// Per-dimension unscaled metrics.
76    pub axes: [UnscaledAxisMetrics; 2],
77}
78
79impl UnscaledStyleMetrics {
80    pub fn style_class(&self) -> &'static StyleClass {
81        &super::style::STYLE_CLASSES[self.class_ix as usize]
82    }
83}
84
85/// The set of unscaled style metrics for a single font.
86///
87/// For a variable font, this is dependent on the location in variation space.
88#[derive(Clone, Debug)]
89pub(crate) enum UnscaledStyleMetricsSet {
90    Precomputed(Vec<UnscaledStyleMetrics>),
91    #[cfg(feature = "std")]
92    Lazy(Arc<RwLock<Vec<Option<UnscaledStyleMetrics>>>>),
93}
94
95impl UnscaledStyleMetricsSet {
96    /// Creates a precomputed style metrics set containing all metrics
97    /// required by the glyph map.
98    pub fn precomputed(
99        font: &FontRef,
100        coords: &[F2Dot14],
101        shaper_mode: ShaperMode,
102        style_map: &GlyphStyleMap,
103    ) -> Self {
104        // The metrics_styles() iterator does not report exact size so we
105        // preallocate and extend here rather than collect to avoid
106        // over allocating memory.
107        let shaper = Shaper::new(font, shaper_mode);
108        let mut vec = Vec::with_capacity(style_map.metrics_count());
109        vec.extend(
110            style_map
111                .metrics_styles()
112                .map(|style| compute_unscaled_style_metrics(&shaper, coords, style)),
113        );
114        Self::Precomputed(vec)
115    }
116
117    /// Creates an unscaled style metrics set where each entry will be
118    /// initialized as needed.
119    #[cfg(feature = "std")]
120    pub fn lazy(style_map: &GlyphStyleMap) -> Self {
121        let vec = vec![None; style_map.metrics_count()];
122        Self::Lazy(Arc::new(RwLock::new(vec)))
123    }
124
125    /// Returns the unscaled style metrics for the given style map and glyph
126    /// identifier.
127    pub fn get(
128        &self,
129        font: &FontRef,
130        coords: &[F2Dot14],
131        shaper_mode: ShaperMode,
132        style_map: &GlyphStyleMap,
133        glyph_id: GlyphId,
134    ) -> Option<UnscaledStyleMetrics> {
135        let style = style_map.style(glyph_id)?;
136        let index = style_map.metrics_index(style)?;
137        match self {
138            Self::Precomputed(metrics) => metrics.get(index).cloned(),
139            #[cfg(feature = "std")]
140            Self::Lazy(lazy) => {
141                let read = lazy.read().unwrap();
142                let entry = read.get(index)?;
143                if let Some(metrics) = &entry {
144                    return Some(metrics.clone());
145                }
146                core::mem::drop(read);
147                // The std RwLock doesn't support upgrading and contention is
148                // expected to be low, so let's just race to compute the new
149                // metrics.
150                let shaper = Shaper::new(font, shaper_mode);
151                let style_class = style.style_class()?;
152                let metrics = compute_unscaled_style_metrics(&shaper, coords, style_class);
153                let mut entry = lazy.write().unwrap();
154                *entry.get_mut(index)? = Some(metrics.clone());
155                Some(metrics)
156            }
157        }
158    }
159}
160
161/// Scaled metrics for a single style and script.
162#[derive(Clone, Default, Debug)]
163pub(crate) struct ScaledStyleMetrics {
164    /// Multidimensional scaling factors and deltas.
165    pub scale: Scale,
166    /// Per-dimension scaled metrics.
167    pub axes: [ScaledAxisMetrics; 2],
168}
169
170#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
171pub(crate) struct WidthMetrics {
172    /// Used for creating edges.
173    pub edge_distance_threshold: i32,
174    /// Default stem thickness.
175    pub standard_width: i32,
176    /// Is standard width very light?
177    pub is_extra_light: bool,
178}
179
180pub(crate) type UnscaledWidths = SmallVec<i32, MAX_WIDTHS>;
181
182#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
183pub(crate) struct ScaledWidth {
184    /// Width after applying scale.
185    pub scaled: i32,
186    /// Grid-fitted width.
187    pub fitted: i32,
188}
189
190pub(crate) type ScaledWidths = SmallVec<ScaledWidth, MAX_WIDTHS>;
191
192/// Captures scaling parameters which may be modified during metrics
193/// computation.
194#[derive(Copy, Clone, Default, Debug)]
195pub(crate) struct Scale {
196    /// Font unit to 26.6 scale in the X direction.
197    pub x_scale: i32,
198    /// Font unit to 26.6 scale in the Y direction.
199    pub y_scale: i32,
200    /// In 1/64 device pixels.
201    pub x_delta: i32,
202    /// In 1/64 device pixels.
203    pub y_delta: i32,
204    /// Font size in pixels per em.
205    pub size: f32,
206    /// From the source font.
207    pub units_per_em: i32,
208    /// Flags that determine hinting functionality.
209    pub flags: u32,
210}
211
212impl Scale {
213    /// Create initial scaling parameters from metrics and hinting target.
214    pub fn new(
215        size: f32,
216        units_per_em: i32,
217        font_style: Style,
218        target: Target,
219        group: ScriptGroup,
220    ) -> Self {
221        let scale =
222            (Fixed::from_bits((size * 64.0) as i32) / Fixed::from_bits(units_per_em)).to_bits();
223        let mut flags = 0;
224        let is_italic = font_style != Style::Normal;
225        let is_mono = target == Target::Mono;
226        let is_light = target.is_light() || target.preserve_linear_metrics();
227        // Snap vertical stems for monochrome and horizontal LCD rendering.
228        if is_mono || target.is_lcd() {
229            flags |= Self::HORIZONTAL_SNAP;
230        }
231        // Snap horizontal stems for monochrome and vertical LCD rendering.
232        if is_mono || target.is_vertical_lcd() {
233            flags |= Self::VERTICAL_SNAP;
234        }
235        // Adjust stems to full pixels unless in LCD or light modes.
236        if !(target.is_lcd() || is_light) {
237            flags |= Self::STEM_ADJUST;
238        }
239        if is_mono {
240            flags |= Self::MONO;
241        }
242        if group == ScriptGroup::Default {
243            // Disable horizontal hinting completely for LCD, light hinting
244            // and italic fonts
245            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2674>
246            if target.is_lcd() || is_light || is_italic {
247                flags |= Self::NO_HORIZONTAL;
248            }
249        } else {
250            // CJK doesn't hint advances
251            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1432>
252            flags |= Self::NO_ADVANCE;
253        }
254        // CJK doesn't hint advances
255        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1432>
256        if group != ScriptGroup::Default {
257            flags |= Self::NO_ADVANCE;
258        }
259        Self {
260            x_scale: scale,
261            y_scale: scale,
262            x_delta: 0,
263            y_delta: 0,
264            size,
265            units_per_em,
266            flags,
267        }
268    }
269}
270
271/// Scaler flags that determine hinting settings.
272///
273/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aftypes.h#L115>
274/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L143>
275impl Scale {
276    /// Stem width snapping.
277    pub const HORIZONTAL_SNAP: u32 = 1 << 0;
278    /// Stem height snapping.
279    pub const VERTICAL_SNAP: u32 = 1 << 1;
280    /// Stem width/height adjustment.
281    pub const STEM_ADJUST: u32 = 1 << 2;
282    /// Monochrome rendering.
283    pub const MONO: u32 = 1 << 3;
284    /// Disable horizontal hinting.
285    pub const NO_HORIZONTAL: u32 = 1 << 4;
286    /// Disable vertical hinting.
287    pub const NO_VERTICAL: u32 = 1 << 5;
288    /// Disable advance hinting.
289    pub const NO_ADVANCE: u32 = 1 << 6;
290}
291
292// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L59>
293pub(crate) fn sort_and_quantize_widths(widths: &mut UnscaledWidths, threshold: i32) {
294    if widths.len() <= 1 {
295        return;
296    }
297    widths.sort_unstable();
298    let table = widths.as_mut_slice();
299    let mut cur_ix = 0;
300    let mut cur_val = table[cur_ix];
301    let last_ix = table.len() - 1;
302    let mut ix = 1;
303    // Compute and use mean values for clusters not larger than
304    // `threshold`.
305    while ix < table.len() {
306        if (table[ix] - cur_val) > threshold || ix == last_ix {
307            let mut sum = 0;
308            // Fix loop for end of array?
309            if (table[ix] - cur_val <= threshold) && ix == last_ix {
310                ix += 1;
311            }
312            for val in &mut table[cur_ix..ix] {
313                sum += *val;
314                *val = 0;
315            }
316            table[cur_ix] = sum / ix as i32;
317            if ix < last_ix {
318                cur_ix = ix + 1;
319                cur_val = table[cur_ix];
320            }
321        }
322        ix += 1;
323    }
324    cur_ix = 1;
325    // Compress array to remove zero values
326    for ix in 1..table.len() {
327        if table[ix] != 0 {
328            table[cur_ix] = table[ix];
329            cur_ix += 1;
330        }
331    }
332    widths.truncate(cur_ix);
333}
334
335// Fixed point helpers
336//
337// Note: lots of bit fiddling based fixed point math in the autohinter
338// so we're opting out of using the strongly typed variants because they
339// just add noise and reduce clarity.
340
341pub(crate) fn fixed_mul(a: i32, b: i32) -> i32 {
342    (Fixed::from_bits(a) * Fixed::from_bits(b)).to_bits()
343}
344
345pub(crate) fn fixed_div(a: i32, b: i32) -> i32 {
346    (Fixed::from_bits(a) / Fixed::from_bits(b)).to_bits()
347}
348
349pub(crate) fn fixed_mul_div(a: i32, b: i32, c: i32) -> i32 {
350    Fixed::from_bits(a)
351        .mul_div(Fixed::from_bits(b), Fixed::from_bits(c))
352        .to_bits()
353}
354
355pub(crate) fn pix_round(a: i32) -> i32 {
356    (a + 32) & !63
357}
358
359pub(crate) fn pix_floor(a: i32) -> i32 {
360    a & !63
361}
362
363#[cfg(test)]
364mod tests {
365    use super::{
366        super::{
367            shape::{Shaper, ShaperMode},
368            style::STYLE_CLASSES,
369        },
370        *,
371    };
372    use raw::TableProvider;
373
374    #[test]
375    fn sort_widths() {
376        // We use 10 and 20 as thresholds because the computation used
377        // is units_per_em / 100
378        assert_eq!(sort_widths_helper(&[1], 10), &[1]);
379        assert_eq!(sort_widths_helper(&[1], 20), &[1]);
380        assert_eq!(sort_widths_helper(&[60, 20, 40, 35], 10), &[20, 35, 13, 60]);
381        assert_eq!(sort_widths_helper(&[60, 20, 40, 35], 20), &[31, 60]);
382    }
383
384    fn sort_widths_helper(widths: &[i32], threshold: i32) -> Vec<i32> {
385        let mut widths2 = UnscaledWidths::new();
386        for width in widths {
387            widths2.push(*width);
388        }
389        sort_and_quantize_widths(&mut widths2, threshold);
390        widths2.into_iter().collect()
391    }
392
393    #[test]
394    fn precomputed_style_set() {
395        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
396        let coords = &[];
397        let shaper = Shaper::new(&font, ShaperMode::Nominal);
398        let glyph_count = font.maxp().unwrap().num_glyphs() as u32;
399        let style_map = GlyphStyleMap::new(glyph_count, &shaper);
400        let style_set =
401            UnscaledStyleMetricsSet::precomputed(&font, coords, ShaperMode::Nominal, &style_map);
402        let UnscaledStyleMetricsSet::Precomputed(set) = &style_set else {
403            panic!("we definitely made a precomputed style set");
404        };
405        // This font has Latin, Hebrew and CJK (for unassigned chars) styles
406        assert_eq!(STYLE_CLASSES[set[0].class_ix as usize].name, "Latin");
407        assert_eq!(STYLE_CLASSES[set[1].class_ix as usize].name, "Hebrew");
408        assert_eq!(
409            STYLE_CLASSES[set[2].class_ix as usize].name,
410            "CJKV ideographs"
411        );
412        assert_eq!(set.len(), 3);
413    }
414
415    #[test]
416    fn lazy_style_set() {
417        let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
418        let coords = &[];
419        let shaper = Shaper::new(&font, ShaperMode::Nominal);
420        let glyph_count = font.maxp().unwrap().num_glyphs() as u32;
421        let style_map = GlyphStyleMap::new(glyph_count, &shaper);
422        let style_set = UnscaledStyleMetricsSet::lazy(&style_map);
423        let all_empty = lazy_set_presence(&style_set);
424        // Set starts out all empty
425        assert_eq!(all_empty, [false; 3]);
426        // First load a CJK glyph
427        let metrics2 = style_set
428            .get(
429                &font,
430                coords,
431                ShaperMode::Nominal,
432                &style_map,
433                GlyphId::new(0),
434            )
435            .unwrap();
436        assert_eq!(
437            STYLE_CLASSES[metrics2.class_ix as usize].name,
438            "CJKV ideographs"
439        );
440        let only_cjk = lazy_set_presence(&style_set);
441        assert_eq!(only_cjk, [false, false, true]);
442        // Then a Hebrew glyph
443        let metrics1 = style_set
444            .get(
445                &font,
446                coords,
447                ShaperMode::Nominal,
448                &style_map,
449                GlyphId::new(1),
450            )
451            .unwrap();
452        assert_eq!(STYLE_CLASSES[metrics1.class_ix as usize].name, "Hebrew");
453        let hebrew_and_cjk = lazy_set_presence(&style_set);
454        assert_eq!(hebrew_and_cjk, [false, true, true]);
455        // And finally a Latin glyph
456        let metrics0 = style_set
457            .get(
458                &font,
459                coords,
460                ShaperMode::Nominal,
461                &style_map,
462                GlyphId::new(15),
463            )
464            .unwrap();
465        assert_eq!(STYLE_CLASSES[metrics0.class_ix as usize].name, "Latin");
466        let all_present = lazy_set_presence(&style_set);
467        assert_eq!(all_present, [true; 3]);
468    }
469
470    fn lazy_set_presence(style_set: &UnscaledStyleMetricsSet) -> Vec<bool> {
471        let UnscaledStyleMetricsSet::Lazy(set) = &style_set else {
472            panic!("we definitely made a lazy style set");
473        };
474        set.read()
475            .unwrap()
476            .iter()
477            .map(|opt| opt.is_some())
478            .collect()
479    }
480}