Skip to main content

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