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