Skip to main content

skrifa/outline/
mod.rs

1//! Loading, scaling and hinting of glyph outlines.
2//!
3//! This module provides support for retrieving (optionally scaled and hinted)
4//! glyph outlines in the form of vector paths.
5//!
6//! # Drawing a glyph
7//!
8//! Generating SVG [path commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands)
9//! for a character (this assumes a local variable `font` of type [`FontRef`]):
10//!
11//! ```rust
12//! use skrifa::{
13//!     instance::{LocationRef, Size},
14//!     outline::{DrawSettings, OutlinePen},
15//!     FontRef, MetadataProvider,
16//! };
17//!
18//! # fn wrapper(font: FontRef) {
19//! // First, grab the set of outline glyphs from the font.
20//! let outlines = font.outline_glyphs();
21//!
22//! // Find the glyph identifier for our character.
23//! let glyph_id = font.charmap().map('Q').unwrap();
24//!
25//! // Grab the outline glyph.
26//! let glyph = outlines.get(glyph_id).unwrap();
27//!
28//! // Define how we want the glyph to be drawn. This creates
29//! // settings for an instance without hinting at a size of
30//! // 16px with no variations applied.
31//! let settings = DrawSettings::unhinted(Size::new(16.0), LocationRef::default());
32//!
33//! // Alternatively, we can apply variations like so:
34//! let var_location = font.axes().location(&[("wght", 650.0), ("wdth", 100.0)]);
35//! let settings = DrawSettings::unhinted(Size::new(16.0), &var_location);
36//!
37//! // At this point, we need a "sink" to receive the resulting path. This
38//! // is done by creating an implementation of the OutlinePen trait.
39//!
40//! // Let's make one that generates SVG path data.
41//! #[derive(Default)]
42//! struct SvgPath(String);
43//!
44//! // Implement the OutlinePen trait for this type. This emits the appropriate
45//! // SVG path commands for each element type.
46//! impl OutlinePen for SvgPath {
47//!     fn move_to(&mut self, x: f32, y: f32) {
48//!         self.0.push_str(&format!("M{x:.1},{y:.1} "));
49//!     }
50//!
51//!     fn line_to(&mut self, x: f32, y: f32) {
52//!         self.0.push_str(&format!("L{x:.1},{y:.1} "));
53//!     }
54//!
55//!     fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
56//!         self.0
57//!             .push_str(&format!("Q{cx0:.1},{cy0:.1} {x:.1},{y:.1} "));
58//!     }
59//!
60//!     fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
61//!         self.0.push_str(&format!(
62//!             "C{cx0:.1},{cy0:.1} {cx1:.1},{cy1:.1} {x:.1},{y:.1} "
63//!         ));
64//!     }
65//!
66//!     fn close(&mut self) {
67//!         self.0.push_str("Z ");
68//!     }
69//! }
70//! // Now, construct an instance of our pen.
71//! let mut svg_path = SvgPath::default();
72//!
73//! // And draw the glyph!
74//! glyph.draw(settings, &mut svg_path).unwrap();
75//!
76//! // See what we've drawn.
77//! println!("{}", svg_path.0);
78//! # }
79//! ```
80
81pub mod autohint;
82pub(crate) mod cff;
83pub(crate) mod glyf;
84mod hint;
85mod metrics;
86mod unscaled;
87mod varc;
88
89#[cfg(test)]
90mod testing;
91
92pub mod error;
93pub mod pen;
94
95pub use autohint::GlyphStyles;
96pub use hint::{
97    Engine, HintingInstance, HintingMode, HintingOptions, LcdLayout, SmoothMode, Target,
98};
99use metrics::GlyphHMetrics;
100use raw::FontRef;
101#[doc(inline)]
102pub use {error::DrawError, pen::OutlinePen};
103
104// A sized adapter lets the private drawing APIs share one pen type.
105struct ErasedPen<'a>(&'a mut dyn OutlinePen);
106
107impl OutlinePen for ErasedPen<'_> {
108    fn move_to(&mut self, x: f32, y: f32) {
109        self.0.move_to(x, y);
110    }
111    fn line_to(&mut self, x: f32, y: f32) {
112        self.0.line_to(x, y);
113    }
114    fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
115        self.0.quad_to(cx, cy, x, y);
116    }
117    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
118        self.0.curve_to(cx0, cy0, cx1, cy1, x, y);
119    }
120    fn close(&mut self) {
121        self.0.close();
122    }
123}
124
125use self::glyf::{FreeTypeScaler, HarfBuzzScaler};
126use super::instance::{LocationRef, NormalizedCoord, Size};
127use core::fmt::Debug;
128use pen::PathStyle;
129use read_fonts::{types::GlyphId, TableProvider};
130
131#[cfg(feature = "libm")]
132#[allow(unused_imports)]
133use core_maths::CoreFloat;
134
135/// Source format for an outline glyph.
136#[derive(Copy, Clone, PartialEq, Eq, Debug)]
137pub enum OutlineGlyphFormat {
138    /// TrueType outlines sourced from the `glyf` table.
139    Glyf,
140    /// PostScript outlines sourced from the `CFF` table.
141    Cff,
142    /// PostScript outlines sourced from the `CFF2` table.
143    Cff2,
144    /// Variable composites sourced from the `VARC` table.
145    Varc,
146}
147
148/// Specifies the hinting strategy for memory size calculations.
149#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
150pub enum Hinting {
151    /// Hinting is disabled.
152    #[default]
153    None,
154    /// Application of hints that are embedded in the font.
155    ///
156    /// For TrueType, these are bytecode instructions associated with each
157    /// glyph outline. For PostScript (CFF/CFF2), these are stem hints
158    /// encoded in the character string.
159    Embedded,
160}
161
162/// Information and adjusted metrics generated while drawing an outline glyph.
163///
164/// When applying hints to a TrueType glyph, the outline may be adjusted in
165/// the horizontal direction, affecting the left side bearing and advance width
166/// of the glyph. This captures those metrics.
167#[derive(Copy, Clone, Default, Debug)]
168pub struct AdjustedMetrics {
169    /// True if the underlying glyph contains flags indicating the
170    /// presence of overlapping contours or components.
171    pub has_overlaps: bool,
172    /// If present, an adjusted left side bearing value generated by the
173    /// scaler.
174    ///
175    /// This is equivalent to the `horiBearingX` value in
176    /// [`FT_Glyph_Metrics`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyph_metrics).
177    pub lsb: Option<f32>,
178    /// If present, an adjusted advance width value generated by the
179    /// scaler.
180    ///
181    /// Note that, like FreeType, this value is always rounded when hinting
182    /// is requested even if the font does not contain any hinting
183    /// instructions. This might be relevant for CFF where the hinter never
184    /// adjusts outlines in the horizontal direction, in which case,
185    /// using the linear advance width from [`GlyphMetrics`](crate::metrics::GlyphMetrics)
186    /// may be desirable.
187    ///
188    /// This is equivalent to the `advance.x` value in
189    /// [`FT_GlyphSlotRec`](https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_glyphslotrec).
190    pub advance_width: Option<f32>,
191}
192
193/// Options that define how a [glyph](OutlineGlyph) is drawn to a
194/// [pen](OutlinePen).
195pub struct DrawSettings<'a> {
196    instance: DrawInstance<'a>,
197    memory: Option<&'a mut [u8]>,
198    path_style: PathStyle,
199}
200
201impl<'a> DrawSettings<'a> {
202    /// Creates settings for an unhinted draw operation with the given size and
203    /// location in variation space.
204    pub fn unhinted(size: Size, location: impl Into<LocationRef<'a>>) -> Self {
205        Self {
206            instance: DrawInstance::Unhinted(size, location.into()),
207            memory: None,
208            path_style: PathStyle::default(),
209        }
210    }
211
212    /// Creates settings for a hinted draw operation using hinting data
213    /// contained in the font.
214    ///
215    /// If `is_pedantic` is true then any error that occurs during hinting will
216    /// cause drawing to fail. This is equivalent to the `FT_LOAD_PEDANTIC` flag
217    /// in FreeType.
218    ///
219    /// The font size, location in variation space and hinting mode are
220    /// defined by the current configuration of the given hinting instance.
221    pub fn hinted(instance: &'a HintingInstance, is_pedantic: bool) -> Self {
222        Self {
223            instance: DrawInstance::Hinted {
224                instance,
225                is_pedantic,
226            },
227            memory: None,
228            path_style: PathStyle::default(),
229        }
230    }
231
232    /// Builder method to associate a user memory buffer to be used for
233    /// temporary allocations during drawing.
234    ///
235    /// The required size of this buffer can be computed using the
236    /// [`OutlineGlyph::draw_memory_size`] method.
237    ///
238    /// If not provided, any necessary memory will be allocated internally.
239    pub fn with_memory(mut self, memory: Option<&'a mut [u8]>) -> Self {
240        self.memory = memory;
241        self
242    }
243
244    /// Builder method to control nuances of [`glyf`](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf) pointstream interpretation.
245    ///
246    /// Meant for use when trying to match legacy code behavior in Rust.
247    pub fn with_path_style(mut self, path_style: PathStyle) -> Self {
248        self.path_style = path_style;
249        self
250    }
251}
252
253enum DrawInstance<'a> {
254    Unhinted(Size, LocationRef<'a>),
255    Hinted {
256        instance: &'a HintingInstance,
257        is_pedantic: bool,
258    },
259}
260
261impl<'a, L> From<(Size, L)> for DrawSettings<'a>
262where
263    L: Into<LocationRef<'a>>,
264{
265    fn from(value: (Size, L)) -> Self {
266        DrawSettings::unhinted(value.0, value.1.into())
267    }
268}
269
270impl From<Size> for DrawSettings<'_> {
271    fn from(value: Size) -> Self {
272        DrawSettings::unhinted(value, LocationRef::default())
273    }
274}
275
276impl<'a> From<&'a HintingInstance> for DrawSettings<'a> {
277    fn from(value: &'a HintingInstance) -> Self {
278        DrawSettings::hinted(value, false)
279    }
280}
281
282/// A scalable glyph outline.
283///
284/// This can be sourced from the [`glyf`](https://learn.microsoft.com/en-us/typography/opentype/spec/glyf),
285/// [`CFF`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff) or
286/// [`CFF2`](https://learn.microsoft.com/en-us/typography/opentype/spec/cff2)
287/// tables. Use the [`format`](OutlineGlyph::format) method to determine which
288/// was chosen for this glyph.
289#[derive(Clone)]
290pub struct OutlineGlyph<'a> {
291    kind: OutlineKind<'a>,
292}
293
294impl<'a> OutlineGlyph<'a> {
295    /// Returns the underlying source format for this outline.
296    pub fn format(&self) -> OutlineGlyphFormat {
297        match &self.kind {
298            OutlineKind::Glyf(..) => OutlineGlyphFormat::Glyf,
299            OutlineKind::Cff(cff, ..) => {
300                if cff.is_cff2() {
301                    OutlineGlyphFormat::Cff2
302                } else {
303                    OutlineGlyphFormat::Cff
304                }
305            }
306            OutlineKind::Varc(..) => OutlineGlyphFormat::Varc,
307        }
308    }
309
310    /// Returns the glyph identifier for this outline.
311    pub fn glyph_id(&self) -> GlyphId {
312        match &self.kind {
313            OutlineKind::Glyf(_, glyph) => glyph.glyph_id,
314            OutlineKind::Cff(_, gid, _) => *gid,
315            OutlineKind::Varc(_, outline) => outline.glyph_id,
316        }
317    }
318
319    /// Returns a value indicating if the outline may contain overlapping
320    /// contours or components.
321    ///
322    /// For CFF outlines, returns `None` since this information is unavailable.
323    pub fn has_overlaps(&self) -> Option<bool> {
324        match &self.kind {
325            OutlineKind::Glyf(_, outline) => Some(outline.has_overlaps),
326            _ => None,
327        }
328    }
329
330    /// Returns a value indicating whether the outline has hinting
331    /// instructions.
332    ///
333    /// For CFF outlines, returns `None` since this is unknown prior
334    /// to loading the outline.
335    pub fn has_hinting(&self) -> Option<bool> {
336        match &self.kind {
337            OutlineKind::Glyf(_, outline) => Some(outline.has_hinting),
338            OutlineKind::Varc(..) => Some(false),
339            _ => None,
340        }
341    }
342
343    /// Returns the size (in bytes) of the temporary memory required to draw
344    /// this outline.
345    ///
346    /// This is used to compute the size of the memory buffer required for the
347    /// [`DrawSettings::with_memory`] method.
348    ///
349    /// The `hinting` parameter determines which hinting method, if any, will
350    /// be used for drawing which has an effect on memory requirements.
351    ///
352    /// The appropriate hinting types are as follows:
353    ///
354    /// | For draw settings                  | Use hinting           |
355    /// |------------------------------------|-----------------------|
356    /// | [`DrawSettings::unhinted`]         | [`Hinting::None`]     |
357    /// | [`DrawSettings::hinted`]           | [`Hinting::Embedded`] |
358    pub fn draw_memory_size(&self, hinting: Hinting) -> usize {
359        match &self.kind {
360            OutlineKind::Glyf(_, outline) => outline.required_buffer_size(hinting),
361            OutlineKind::Varc(_, outline) => outline.required_buffer_size(),
362            _ => 0,
363        }
364    }
365
366    /// Draws the outline glyph with the given settings and emits the resulting
367    /// path commands to the specified pen.
368    pub fn draw<'s>(
369        &self,
370        settings: impl Into<DrawSettings<'a>>,
371        pen: &mut impl OutlinePen,
372    ) -> Result<AdjustedMetrics, DrawError> {
373        let mut pen = ErasedPen(pen);
374        self.draw_dyn(settings.into(), &mut pen)
375    }
376
377    // Compile the drawing dispatcher once instead of once per caller's pen.
378    #[inline(never)]
379    fn draw_dyn(
380        &self,
381        settings: DrawSettings<'a>,
382        pen: &mut ErasedPen<'_>,
383    ) -> Result<AdjustedMetrics, DrawError> {
384        match (settings.instance, settings.path_style) {
385            (DrawInstance::Unhinted(size, location), PathStyle::FreeType) => {
386                self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
387            }
388            (DrawInstance::Unhinted(size, location), PathStyle::HarfBuzz) => {
389                self.draw_unhinted(size, location, settings.memory, settings.path_style, pen)
390            }
391            (
392                DrawInstance::Hinted {
393                    instance: hinting_instance,
394                    is_pedantic,
395                },
396                PathStyle::FreeType,
397            ) => {
398                if hinting_instance.is_enabled() {
399                    hinting_instance.draw(
400                        self,
401                        settings.memory,
402                        settings.path_style,
403                        pen,
404                        is_pedantic,
405                    )
406                } else {
407                    let mut metrics = self.draw_unhinted(
408                        hinting_instance.size(),
409                        hinting_instance.location(),
410                        settings.memory,
411                        settings.path_style,
412                        pen,
413                    )?;
414                    // Round advance width when hinting is requested, even if
415                    // the instance is disabled.
416                    if let Some(advance) = metrics.advance_width.as_mut() {
417                        *advance = advance.round();
418                    }
419                    Ok(metrics)
420                }
421            }
422            (DrawInstance::Hinted { .. }, PathStyle::HarfBuzz) => {
423                Err(DrawError::HarfBuzzHintingUnsupported)
424            }
425        }
426    }
427
428    fn draw_unhinted(
429        &self,
430        size: Size,
431        location: impl Into<LocationRef<'a>>,
432        user_memory: Option<&mut [u8]>,
433        path_style: PathStyle,
434        pen: &mut impl OutlinePen,
435    ) -> Result<AdjustedMetrics, DrawError> {
436        let ppem = size.ppem();
437        let coords = location.into().effective_coords();
438        match &self.kind {
439            OutlineKind::Glyf(glyf, outline) => {
440                with_temporary_memory(self, Hinting::None, user_memory, |buf| {
441                    let (lsb, advance_width) = match path_style {
442                        PathStyle::FreeType => {
443                            let scaled_outline =
444                                FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
445                                    .scale(&outline.glyph, outline.glyph_id)?;
446                            scaled_outline.to_path(path_style, pen)?;
447                            (
448                                scaled_outline.adjusted_lsb().to_f32(),
449                                scaled_outline.adjusted_advance_width().to_f32(),
450                            )
451                        }
452                        PathStyle::HarfBuzz => {
453                            let scaled_outline =
454                                HarfBuzzScaler::unhinted(glyf, outline, buf, ppem, coords)?
455                                    .scale(&outline.glyph, outline.glyph_id)?;
456                            scaled_outline.to_path(path_style, pen)?;
457                            (
458                                scaled_outline.adjusted_lsb(),
459                                scaled_outline.adjusted_advance_width(),
460                            )
461                        }
462                    };
463
464                    Ok(AdjustedMetrics {
465                        has_overlaps: outline.has_overlaps,
466                        lsb: Some(lsb),
467                        advance_width: Some(advance_width),
468                    })
469                })
470            }
471            OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
472                let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
473                let advance_width = cff.draw(&subfont, *glyph_id, coords, false, pen)?;
474                Ok(AdjustedMetrics {
475                    has_overlaps: false,
476                    lsb: None,
477                    advance_width,
478                })
479            }
480            OutlineKind::Varc(varc, outline) => {
481                with_temporary_memory(self, Hinting::None, user_memory, |buf| {
482                    varc.draw(outline, buf, size, coords, path_style, pen)?;
483                    Ok(AdjustedMetrics::default())
484                })
485            }
486        }
487    }
488
489    /// Internal drawing API for autohinting that offers unified compact
490    /// storage for unscaled outlines.
491    #[allow(dead_code)]
492    fn draw_unscaled(
493        &self,
494        location: impl Into<LocationRef<'a>>,
495        user_memory: Option<&mut [u8]>,
496        sink: &mut impl unscaled::UnscaledOutlineSink,
497    ) -> Result<i32, DrawError> {
498        let coords = location.into().effective_coords();
499        let ppem = None;
500        match &self.kind {
501            OutlineKind::Glyf(glyf, outline) => {
502                with_temporary_memory(self, Hinting::None, user_memory, |buf| {
503                    let outline = FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
504                        .scale(&outline.glyph, outline.glyph_id)?;
505                    let mut contour_start = 0;
506                    for contour_end in outline.contours.iter().map(|contour| *contour as usize) {
507                        if contour_end >= contour_start {
508                            if let Some(points) = outline.points.get(contour_start..=contour_end) {
509                                let flags = &outline.flags[contour_start..=contour_end];
510                                sink.start_contour();
511                                for (point, flags) in points.iter().zip(flags) {
512                                    sink.point(
513                                        point.x.to_bits() >> 6,
514                                        point.y.to_bits() >> 6,
515                                        flags.without_markers(),
516                                    );
517                                }
518                                sink.end_contour();
519                            }
520                        }
521                        contour_start = contour_end + 1;
522                    }
523                    Ok(outline.adjusted_advance_width().to_bits() >> 6)
524                })
525            }
526            OutlineKind::Cff(cff, glyph_id, subfont_ix) => {
527                let subfont = cff.subfont(*subfont_ix, ppem, coords)?;
528                let mut adapter = unscaled::PointsFromPath::new(sink);
529                cff.draw(&subfont, *glyph_id, coords, false, &mut adapter)?;
530                adapter.finish();
531                let advance = cff.glyph_metrics.advance_width(*glyph_id, coords);
532                Ok(advance)
533            }
534            OutlineKind::Varc(varc, outline) => {
535                with_temporary_memory(self, Hinting::None, user_memory, |buf| {
536                    let mut adapter = unscaled::PointsFromPath::new(sink);
537                    let advance = varc.draw_unscaled(outline, buf, coords, &mut adapter)?;
538                    adapter.finish();
539                    Ok(advance)
540                })
541            }
542        }
543    }
544
545    /// Pass a raw scaled outline to a callback with direct point access.
546    pub fn with_scaled_glyf_outline<R>(
547        &self,
548        size: Size,
549        location: impl Into<LocationRef<'a>>,
550        user_memory: Option<&mut [u8]>,
551        mut callback: impl FnMut(&glyf::ScaledOutline<'_, raw::types::F26Dot6>) -> Result<R, DrawError>,
552    ) -> Result<R, DrawError> {
553        let ppem = size.ppem();
554        let coords = location.into().effective_coords();
555
556        match &self.kind {
557            OutlineKind::Glyf(glyf, outline) => {
558                with_temporary_memory(self, Hinting::None, user_memory, |buf| {
559                    let scaled = FreeTypeScaler::unhinted(glyf, outline, buf, ppem, coords)?
560                        .scale(&outline.glyph, outline.glyph_id)?;
561                    callback(&scaled)
562                })
563            }
564            _ => Err(DrawError::NoSources),
565        }
566    }
567
568    pub(crate) fn font(&self) -> &FontRef<'a> {
569        match &self.kind {
570            OutlineKind::Glyf(glyf, ..) => &glyf.font,
571            OutlineKind::Cff(cff, ..) => &cff.font,
572            OutlineKind::Varc(varc, ..) => varc.font(),
573        }
574    }
575
576    fn units_per_em(&self) -> u16 {
577        match &self.kind {
578            OutlineKind::Cff(cff, ..) => cff.units_per_em(),
579            OutlineKind::Glyf(glyf, ..) => glyf.units_per_em(),
580            OutlineKind::Varc(varc, ..) => varc.units_per_em(),
581        }
582    }
583}
584
585#[derive(Clone)]
586enum OutlineKind<'a> {
587    Glyf(glyf::Outlines<'a>, glyf::Outline<'a>),
588    // Third field is subfont index
589    Cff(cff::Outlines<'a>, GlyphId, u32),
590    Varc(varc::Outlines<'a>, varc::Outline),
591}
592
593impl Debug for OutlineKind<'_> {
594    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
595        match self {
596            Self::Glyf(_, outline) => f.debug_tuple("Glyf").field(&outline.glyph_id).finish(),
597            Self::Cff(_, gid, subfont_index) => f
598                .debug_tuple("Cff")
599                .field(gid)
600                .field(subfont_index)
601                .finish(),
602            Self::Varc(_, outline) => f.debug_tuple("Varc").field(&outline.glyph_id).finish(),
603        }
604    }
605}
606
607/// Collection of scalable glyph outlines.
608#[derive(Debug, Clone)]
609pub struct OutlineGlyphCollection<'a> {
610    kind: OutlineCollectionKind<'a>,
611}
612
613impl<'a> OutlineGlyphCollection<'a> {
614    /// Creates a new outline collection for the given font.
615    pub fn new(font: &FontRef<'a>) -> Self {
616        let kind = if let Some(varc) = varc::Outlines::new(font) {
617            OutlineCollectionKind::Varc(varc)
618        } else if let Some(glyf) = glyf::Outlines::new(font) {
619            OutlineCollectionKind::Glyf(glyf)
620        } else if let Some(cff) = cff::Outlines::new(font) {
621            OutlineCollectionKind::Cff(cff)
622        } else {
623            OutlineCollectionKind::None
624        };
625        Self { kind }
626    }
627
628    /// Creates a new outline collection for the given font and outline
629    /// format.
630    ///
631    /// Returns `None` if the font does not contain outlines in the requested
632    /// format.
633    pub fn with_format(font: &FontRef<'a>, format: OutlineGlyphFormat) -> Option<Self> {
634        let kind = match format {
635            OutlineGlyphFormat::Glyf => OutlineCollectionKind::Glyf(glyf::Outlines::new(font)?),
636            OutlineGlyphFormat::Cff => {
637                let upem = font.head().ok()?.units_per_em();
638                OutlineCollectionKind::Cff(cff::Outlines::from_cff(font, upem)?)
639            }
640            OutlineGlyphFormat::Cff2 => {
641                let upem = font.head().ok()?.units_per_em();
642                OutlineCollectionKind::Cff(cff::Outlines::from_cff2(font, upem)?)
643            }
644            OutlineGlyphFormat::Varc => OutlineCollectionKind::Varc(varc::Outlines::new(font)?),
645        };
646        Some(Self { kind })
647    }
648
649    /// Returns the underlying format of the source outline tables.
650    pub fn format(&self) -> Option<OutlineGlyphFormat> {
651        match &self.kind {
652            OutlineCollectionKind::Glyf(..) => Some(OutlineGlyphFormat::Glyf),
653            OutlineCollectionKind::Cff(cff) => cff
654                .is_cff2()
655                .then_some(OutlineGlyphFormat::Cff2)
656                .or(Some(OutlineGlyphFormat::Cff)),
657            OutlineCollectionKind::Varc(..) => Some(OutlineGlyphFormat::Varc),
658            OutlineCollectionKind::None => None,
659        }
660    }
661
662    /// Returns the outline for the given glyph identifier.
663    pub fn get(&self, glyph_id: GlyphId) -> Option<OutlineGlyph<'a>> {
664        match &self.kind {
665            OutlineCollectionKind::None => None,
666            OutlineCollectionKind::Glyf(glyf) => Some(OutlineGlyph {
667                kind: OutlineKind::Glyf(glyf.clone(), glyf.outline(glyph_id).ok()?),
668            }),
669            OutlineCollectionKind::Cff(cff) => Some(OutlineGlyph {
670                kind: OutlineKind::Cff(cff.clone(), glyph_id, cff.subfont_index(glyph_id)),
671            }),
672            OutlineCollectionKind::Varc(varc) => Some(OutlineGlyph {
673                kind: if let Some(outline) = varc.outline(glyph_id).ok()? {
674                    OutlineKind::Varc(varc.clone(), outline)
675                } else {
676                    varc.fallback_outline_kind(glyph_id)?
677                },
678            }),
679        }
680    }
681
682    /// Returns an iterator over all of the outline glyphs in the collection.
683    pub fn iter(&self) -> impl Iterator<Item = (GlyphId, OutlineGlyph<'a>)> + 'a + Clone {
684        let len = match &self.kind {
685            OutlineCollectionKind::Glyf(glyf) => glyf.glyph_count() as u32,
686            OutlineCollectionKind::Cff(cff) => cff.glyph_count() as u32,
687            OutlineCollectionKind::Varc(varc) => varc.glyph_count(),
688            OutlineCollectionKind::None => 0,
689        };
690        let copy = self.clone();
691        (0..len).filter_map(move |gid| {
692            let gid = GlyphId::from(gid);
693            let glyph = copy.get(gid)?;
694            Some((gid, glyph))
695        })
696    }
697
698    /// Returns true if the interpreter engine should be used for hinting this
699    /// set of outlines.
700    ///
701    /// When this returns false, you likely want to use the automatic hinter
702    /// instead.
703    ///
704    /// This matches the logic used in FreeType when neither of the
705    /// `FT_LOAD_FORCE_AUTOHINT` or `FT_LOAD_NO_AUTOHINT` load flags are
706    /// specified.
707    ///
708    /// When setting [`HintingOptions::engine`] to [`Engine::AutoFallback`],
709    /// this is used to determine whether to use the interpreter or automatic
710    /// hinter.
711    pub fn prefer_interpreter(&self) -> bool {
712        match &self.kind {
713            OutlineCollectionKind::Glyf(glyf) => glyf.prefer_interpreter(),
714            OutlineCollectionKind::Varc(varc) => varc.prefer_interpreter(),
715            _ => true,
716        }
717    }
718
719    /// Returns true when the interpreter engine _must_ be used for hinting
720    /// this set of outlines to produce correct results.
721    ///
722    /// This corresponds so FreeType's `FT_FACE_FLAG_TRICKY` face flag. See
723    /// the documentation for that [flag](https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face_flag_xxx)
724    /// for more detail.
725    ///
726    /// When this returns `true`, you should construct a [`HintingInstance`]
727    /// with [`HintingOptions::engine`] set to [`Engine::Interpreter`] and
728    /// [`HintingOptions::target`] set to [`Target::Mono`].
729    ///
730    /// # Performance
731    /// This digs through the name table and potentially computes checksums
732    /// so it may be slow. You should cache the result of this function if
733    /// possible.
734    pub fn require_interpreter(&self) -> bool {
735        self.font()
736            .map(raw::tables::glyf::is_hint_reliant)
737            .unwrap_or_default()
738    }
739
740    /// Returns true when the font supports hinting at fractional sizes.
741    ///
742    /// When this returns false, the requested size will be rounded before
743    /// computing the scale factor for hinted glyphs.
744    pub fn fractional_size_hinting(&self) -> bool {
745        match &self.kind {
746            OutlineCollectionKind::Glyf(glyf) => glyf.fractional_size_hinting,
747            OutlineCollectionKind::Varc(varc) => varc.fractional_size_hinting(),
748            _ => true,
749        }
750    }
751
752    /// The number of glyphs the outline data can describe, which may be
753    /// fewer than the count in MAXP or maxp.
754    pub(crate) fn stored_glyph_count(&self) -> u32 {
755        match &self.kind {
756            OutlineCollectionKind::Glyf(glyf) => glyf.stored_glyph_count(),
757            OutlineCollectionKind::Cff(cff) => cff.glyph_count() as u32,
758            OutlineCollectionKind::Varc(varc) => varc.glyph_count(),
759            OutlineCollectionKind::None => 0,
760        }
761    }
762
763    pub(crate) fn font(&self) -> Option<&FontRef<'a>> {
764        match &self.kind {
765            OutlineCollectionKind::Glyf(glyf) => Some(&glyf.font),
766            OutlineCollectionKind::Cff(cff) => Some(&cff.font),
767            OutlineCollectionKind::Varc(varc) => Some(varc.font()),
768            OutlineCollectionKind::None => None,
769        }
770    }
771}
772
773#[derive(Clone)]
774enum OutlineCollectionKind<'a> {
775    None,
776    Glyf(glyf::Outlines<'a>),
777    Cff(cff::Outlines<'a>),
778    Varc(varc::Outlines<'a>),
779}
780
781impl Debug for OutlineCollectionKind<'_> {
782    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
783        match self {
784            Self::None => write!(f, "None"),
785            Self::Glyf(..) => f.debug_tuple("Glyf").finish(),
786            Self::Cff(..) => f.debug_tuple("Cff").finish(),
787            Self::Varc(..) => f.debug_tuple("Varc").finish(),
788        }
789    }
790}
791
792/// Invokes the callback with a memory buffer suitable for drawing
793/// the given outline.
794pub(super) fn with_temporary_memory<R>(
795    outline: &OutlineGlyph<'_>,
796    hinting: Hinting,
797    memory: Option<&mut [u8]>,
798    mut f: impl FnMut(&mut [u8]) -> R,
799) -> R {
800    match memory {
801        Some(buf) => f(buf),
802        None => {
803            let buf_size = outline.draw_memory_size(hinting);
804            read_fonts::mem::with_scratch(buf_size, f)
805        }
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812    use crate::outline::pen::NullPen;
813    use crate::{instance::Location, outline::pen::SvgPen, MetadataProvider};
814    use kurbo::{Affine, BezPath, PathEl, Point};
815    use read_fonts::{types::GlyphId, FontRef, TableProvider};
816
817    use pretty_assertions::assert_eq;
818
819    const PERIOD: u32 = 0x2E_u32;
820    const COMMA: u32 = 0x2C_u32;
821
822    #[test]
823    fn outline_glyph_formats() {
824        let font_format_pairs = [
825            (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
826            (
827                font_test_data::CANTARELL_VF_TRIMMED,
828                OutlineGlyphFormat::Cff2,
829            ),
830            (
831                font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
832                OutlineGlyphFormat::Cff,
833            ),
834            (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
835        ];
836        for (font_data, format) in font_format_pairs {
837            assert_eq!(
838                FontRef::new(font_data).unwrap().outline_glyphs().format(),
839                Some(format)
840            );
841        }
842    }
843
844    #[test]
845    fn vazirmatin_var() {
846        compare_glyphs(
847            font_test_data::VAZIRMATN_VAR,
848            font_test_data::VAZIRMATN_VAR_GLYPHS,
849        );
850    }
851
852    #[test]
853    fn cantarell_vf() {
854        compare_glyphs(
855            font_test_data::CANTARELL_VF_TRIMMED,
856            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
857        );
858    }
859
860    #[test]
861    fn noto_serif_display() {
862        compare_glyphs(
863            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
864            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
865        );
866    }
867
868    #[test]
869    fn overlap_flags() {
870        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
871        let outlines = font.outline_glyphs();
872        let glyph_count = font.maxp().unwrap().num_glyphs();
873        // GID 2 is a composite glyph with the overlap bit on a component
874        // GID 3 is a simple glyph with the overlap bit on the first flag
875        let expected_gids_with_overlap = vec![2, 3];
876        assert_eq!(
877            expected_gids_with_overlap,
878            (0..glyph_count)
879                .filter(
880                    |gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
881                )
882                .collect::<Vec<_>>()
883        );
884    }
885
886    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
887        let font = FontRef::new(font_data).unwrap();
888        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
889        let mut path = testing::Path::default();
890        for expected_outline in &expected_outlines {
891            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
892                continue;
893            }
894            let size = if expected_outline.size != 0.0 {
895                Size::new(expected_outline.size)
896            } else {
897                Size::unscaled()
898            };
899            path.elements.clear();
900            font.outline_glyphs()
901                .get(expected_outline.glyph_id)
902                .unwrap()
903                .draw(
904                    DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
905                    &mut path,
906                )
907                .unwrap();
908            assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
909                    expected_outline.glyph_id,
910                    expected_outline.size,
911                    expected_outline.coords,
912                    &path.elements,
913                    &expected_outline.path
914                );
915        }
916    }
917
918    #[derive(Copy, Clone, Debug, PartialEq)]
919    enum GlyphPoint {
920        On { x: f32, y: f32 },
921        Off { x: f32, y: f32 },
922    }
923
924    impl GlyphPoint {
925        fn implied_oncurve(&self, other: Self) -> Self {
926            let (x1, y1) = self.xy();
927            let (x2, y2) = other.xy();
928            Self::On {
929                x: (x1 + x2) / 2.0,
930                y: (y1 + y2) / 2.0,
931            }
932        }
933
934        fn xy(&self) -> (f32, f32) {
935            match self {
936                GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
937            }
938        }
939    }
940
941    #[derive(Debug)]
942    struct PointPen {
943        points: Vec<GlyphPoint>,
944    }
945
946    impl PointPen {
947        fn new() -> Self {
948            Self { points: Vec::new() }
949        }
950
951        fn into_points(self) -> Vec<GlyphPoint> {
952            self.points
953        }
954    }
955
956    impl OutlinePen for PointPen {
957        fn move_to(&mut self, x: f32, y: f32) {
958            self.points.push(GlyphPoint::On { x, y });
959        }
960
961        fn line_to(&mut self, x: f32, y: f32) {
962            self.points.push(GlyphPoint::On { x, y });
963        }
964
965        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
966            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
967            self.points.push(GlyphPoint::On { x, y });
968        }
969
970        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
971            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
972            self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
973            self.points.push(GlyphPoint::On { x, y });
974        }
975
976        fn close(&mut self) {
977            // We can't drop a 0-length closing line for fear of breaking interpolation compatibility
978            //     - some other instance might have it not 0-length
979            // However, if the last command isn't a line and ends at the subpath start we can drop the endpoint
980            //     - if any instance had it other than at the start there would be a closing line
981            //     - and it wouldn't be interpolation compatible
982            // See <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
983            let np = self.points.len();
984            // We need at least 3 points to satisfy subsequent conditions
985            if np > 2
986                && self.points[0] == self.points[np - 1]
987                && matches!(
988                    (self.points[0], self.points[np - 2]),
989                    (GlyphPoint::On { .. }, GlyphPoint::Off { .. })
990                )
991            {
992                self.points.pop();
993            }
994        }
995    }
996
997    const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
998        GlyphPoint::Off { x: 278.0, y: 710.0 },
999        GlyphPoint::On { x: 278.0, y: 470.0 },
1000        GlyphPoint::On { x: 998.0, y: 470.0 },
1001        GlyphPoint::On { x: 998.0, y: 710.0 },
1002    ];
1003
1004    const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
1005        GlyphPoint::Off { x: 278.0, y: 710.0 },
1006        GlyphPoint::Off { x: 278.0, y: 470.0 },
1007        GlyphPoint::On { x: 998.0, y: 470.0 },
1008        GlyphPoint::Off { x: 998.0, y: 710.0 },
1009        GlyphPoint::Off { x: 750.0, y: 500.0 },
1010    ];
1011
1012    /// Captures the svg drawing command sequence, e.g. MLLZ.
1013    ///
1014    /// Intended use is to confirm the command sequence pushed to the pen is interpolation compatible.
1015    #[derive(Default, Debug)]
1016    struct CommandPen {
1017        commands: String,
1018    }
1019
1020    impl OutlinePen for CommandPen {
1021        fn move_to(&mut self, _x: f32, _y: f32) {
1022            self.commands.push('M');
1023        }
1024
1025        fn line_to(&mut self, _x: f32, _y: f32) {
1026            self.commands.push('L');
1027        }
1028
1029        fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
1030            self.commands.push('Q');
1031        }
1032
1033        fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
1034            self.commands.push('C');
1035        }
1036
1037        fn close(&mut self) {
1038            self.commands.push('Z');
1039        }
1040    }
1041
1042    fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
1043        let font = FontRef::new(font).unwrap();
1044        let gid = font
1045            .cmap()
1046            .unwrap()
1047            .map_codepoint(codepoint)
1048            .unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
1049        let outlines = font.outline_glyphs();
1050        let outline = outlines.get(gid).unwrap_or_else(|| {
1051            panic!(
1052                "No outline for {gid:?} in collection of {:?}",
1053                outlines.format()
1054            )
1055        });
1056
1057        outline.draw(settings, pen).unwrap();
1058    }
1059
1060    fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
1061        let mut pen = CommandPen::default();
1062        draw_to_pen(font, codepoint, settings, &mut pen);
1063        pen.commands
1064    }
1065
1066    fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
1067        let mut pen = PointPen::new();
1068        draw_to_pen(font, codepoint, settings, &mut pen);
1069        pen.into_points()
1070    }
1071
1072    fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
1073        let mut expanded_points = Vec::new();
1074
1075        for i in 0..pointstream.len() - 1 {
1076            expanded_points.push(pointstream[i]);
1077            if matches!(
1078                (pointstream[i], pointstream[i + 1]),
1079                (GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
1080            ) {
1081                expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
1082            }
1083        }
1084
1085        expanded_points.push(*pointstream.last().unwrap());
1086
1087        expanded_points
1088    }
1089
1090    fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
1091        points
1092            .iter()
1093            .map(|p| match p {
1094                GlyphPoint::On { .. } => "On",
1095                GlyphPoint::Off { .. } => "Off",
1096            })
1097            .collect()
1098    }
1099
1100    #[test]
1101    fn always_get_closing_lines() {
1102        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
1103        let period = draw_commands(
1104            font_test_data::INTERPOLATE_THIS,
1105            PERIOD,
1106            Size::unscaled().into(),
1107        );
1108        let comma = draw_commands(
1109            font_test_data::INTERPOLATE_THIS,
1110            COMMA,
1111            Size::unscaled().into(),
1112        );
1113
1114        assert_eq!(
1115            period, comma,
1116            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1117        );
1118        assert_eq!(
1119            "MLLLZ", period,
1120            "We should get an explicit L for close even when it's a nop"
1121        );
1122    }
1123
1124    #[test]
1125    fn triangle_and_square_retain_compatibility() {
1126        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
1127        let period = drawn_points(
1128            font_test_data::INTERPOLATE_THIS,
1129            PERIOD,
1130            Size::unscaled().into(),
1131        );
1132        let comma = drawn_points(
1133            font_test_data::INTERPOLATE_THIS,
1134            COMMA,
1135            Size::unscaled().into(),
1136        );
1137
1138        assert_ne!(period, comma);
1139        assert_eq!(
1140            as_on_off_sequence(&period),
1141            as_on_off_sequence(&comma),
1142            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1143        );
1144        assert_eq!(
1145            4,
1146            period.len(),
1147            "we should have the same # of points we started with"
1148        );
1149    }
1150
1151    fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
1152        assert!(
1153            matches!(pointstream[0], GlyphPoint::Off { .. }),
1154            "Bad testdata, should start off curve"
1155        );
1156
1157        // The default: look for an oncurve at the back, as freetype would do
1158        let mut expected_points = pointstream.to_vec();
1159        let last = *expected_points.last().unwrap();
1160        let first_move = if matches!(last, GlyphPoint::Off { .. }) {
1161            expected_points[0].implied_oncurve(last)
1162        } else {
1163            expected_points.pop().unwrap()
1164        };
1165        expected_points.insert(0, first_move);
1166
1167        expected_points = insert_implicit_oncurve(&expected_points);
1168        let actual = drawn_points(font, PERIOD, Size::unscaled().into());
1169        assert_eq!(
1170            expected_points, actual,
1171            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1172        );
1173    }
1174
1175    fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
1176        assert!(
1177            matches!(pointstream[0], GlyphPoint::Off { .. }),
1178            "Bad testdata, should start off curve"
1179        );
1180
1181        // look for an oncurve at the front, as harfbuzz would do
1182        let mut expected_points = pointstream.to_vec();
1183        let first = expected_points.remove(0);
1184        expected_points.push(first);
1185        if matches!(expected_points[0], GlyphPoint::Off { .. }) {
1186            expected_points.insert(0, first.implied_oncurve(expected_points[0]))
1187        };
1188
1189        expected_points = insert_implicit_oncurve(&expected_points);
1190
1191        let settings: DrawSettings = Size::unscaled().into();
1192        let settings = settings.with_path_style(PathStyle::HarfBuzz);
1193        let actual = drawn_points(font, PERIOD, settings);
1194        assert_eq!(
1195            expected_points, actual,
1196            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1197        );
1198    }
1199
1200    #[test]
1201    fn starting_off_curve_walk_backwards_like_freetype() {
1202        assert_walked_backwards_like_freetype(
1203            &STARTING_OFF_CURVE_POINTS,
1204            font_test_data::STARTING_OFF_CURVE,
1205        );
1206    }
1207
1208    #[test]
1209    fn mostly_off_curve_walk_backwards_like_freetype() {
1210        assert_walked_backwards_like_freetype(
1211            &MOSTLY_OFF_CURVE_POINTS,
1212            font_test_data::MOSTLY_OFF_CURVE,
1213        );
1214    }
1215
1216    #[test]
1217    fn starting_off_curve_walk_forwards_like_hbdraw() {
1218        assert_walked_forwards_like_harfbuzz(
1219            &STARTING_OFF_CURVE_POINTS,
1220            font_test_data::STARTING_OFF_CURVE,
1221        );
1222    }
1223
1224    #[test]
1225    fn mostly_off_curve_walk_forwards_like_hbdraw() {
1226        assert_walked_forwards_like_harfbuzz(
1227            &MOSTLY_OFF_CURVE_POINTS,
1228            font_test_data::MOSTLY_OFF_CURVE,
1229        );
1230    }
1231
1232    // A location noted for making FreeType and HarfBuzz results differ
1233    // See https://github.com/googlefonts/sleipnir/pull/15
1234    fn icon_loc_off_default(font: &FontRef) -> Location {
1235        font.axes().location(&[
1236            ("wght", 700.0),
1237            ("opsz", 48.0),
1238            ("GRAD", 200.0),
1239            ("FILL", 1.0),
1240        ])
1241    }
1242
1243    fn pt(x: f32, y: f32) -> Point {
1244        (x as f64, y as f64).into()
1245    }
1246
1247    // String command rounded to two decimal places, suitable for assert comparison
1248    fn svg_commands(elements: &[PathEl]) -> Vec<String> {
1249        elements
1250            .iter()
1251            .map(|e| match e {
1252                PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
1253                PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
1254                PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
1255                PathEl::CurveTo(c0, c1, p) => format!(
1256                    "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1257                    c0.x, c0.y, c1.x, c1.y, p.x, p.y
1258                ),
1259                PathEl::ClosePath => "Z".to_string(),
1260            })
1261            .collect()
1262    }
1263
1264    // Declared here to avoid a write-fonts dependency that is awkward for google3 at time of writing
1265    #[derive(Default)]
1266    struct BezPen {
1267        path: BezPath,
1268    }
1269
1270    impl OutlinePen for BezPen {
1271        fn move_to(&mut self, x: f32, y: f32) {
1272            self.path.move_to(pt(x, y));
1273        }
1274
1275        fn line_to(&mut self, x: f32, y: f32) {
1276            self.path.line_to(pt(x, y));
1277        }
1278
1279        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1280            self.path.quad_to(pt(cx0, cy0), pt(x, y));
1281        }
1282
1283        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1284            self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
1285        }
1286
1287        fn close(&mut self) {
1288            self.path.close_path();
1289        }
1290    }
1291
1292    // We take glyph id here to bypass the need to resolve codepoint:gid and apply substitutions
1293    fn assert_glyph_path_start_with(
1294        font: &FontRef,
1295        gid: GlyphId,
1296        loc: Location,
1297        path_style: PathStyle,
1298        expected_path_start: &[PathEl],
1299    ) {
1300        let glyph = font
1301            .outline_glyphs()
1302            .get(gid)
1303            .unwrap_or_else(|| panic!("No glyph for {gid}"));
1304
1305        let mut pen = BezPen::default();
1306        glyph
1307            .draw(
1308                DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
1309                &mut pen,
1310            )
1311            .unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
1312        let bez = Affine::FLIP_Y * pen.path; // like an icon svg
1313        let actual_path_start = &bez.elements()[..expected_path_start.len()];
1314        // round2 can still leave very small differences from the typed 2-decimal value
1315        // and the diff isn't pleasant so just compare as svg string fragments
1316        assert_eq!(
1317            svg_commands(expected_path_start),
1318            svg_commands(actual_path_start)
1319        );
1320    }
1321
1322    const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
1323    const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
1324
1325    #[test]
1326    fn draw_icon_freetype_style_at_default() {
1327        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1328        assert_glyph_path_start_with(
1329            &font,
1330            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1331            Location::default(),
1332            PathStyle::FreeType,
1333            &[
1334                PathEl::MoveTo((160.0, -160.0).into()),
1335                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1336                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1337            ],
1338        );
1339    }
1340
1341    #[test]
1342    fn draw_icon_harfbuzz_style_at_default() {
1343        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1344        assert_glyph_path_start_with(
1345            &font,
1346            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1347            Location::default(),
1348            PathStyle::HarfBuzz,
1349            &[
1350                PathEl::MoveTo((160.0, -160.0).into()),
1351                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1352                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1353            ],
1354        );
1355    }
1356
1357    #[test]
1358    fn draw_icon_freetype_style_off_default() {
1359        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1360        assert_glyph_path_start_with(
1361            &font,
1362            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1363            icon_loc_off_default(&font),
1364            PathStyle::FreeType,
1365            &[
1366                PathEl::MoveTo((150.0, -138.0).into()),
1367                PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
1368                PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
1369            ],
1370        );
1371    }
1372
1373    #[test]
1374    fn draw_icon_harfbuzz_style_off_default() {
1375        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1376        assert_glyph_path_start_with(
1377            &font,
1378            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1379            icon_loc_off_default(&font),
1380            PathStyle::HarfBuzz,
1381            &[
1382                PathEl::MoveTo((150.0, -138.0).into()),
1383                PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
1384                PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
1385            ],
1386        );
1387    }
1388
1389    const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
1390    const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
1391    const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
1392
1393    #[test]
1394    fn draw_nonuniform_scale_component_freetype() {
1395        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1396        assert_glyph_path_start_with(
1397            &font,
1398            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1399            Location::default(),
1400            PathStyle::FreeType,
1401            &[
1402                PathEl::MoveTo((-138.0, -185.0).into()),
1403                PathEl::LineTo((-32.0, -259.0).into()),
1404                PathEl::LineTo((26.0, -175.0).into()),
1405                PathEl::LineTo((-80.0, -101.0).into()),
1406                PathEl::ClosePath,
1407            ],
1408        );
1409    }
1410
1411    #[test]
1412    fn draw_nonuniform_scale_component_harfbuzz() {
1413        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1414        assert_glyph_path_start_with(
1415            &font,
1416            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1417            Location::default(),
1418            PathStyle::HarfBuzz,
1419            &[
1420                PathEl::MoveTo((-137.8, -184.86).into()),
1421                PathEl::LineTo((-32.15, -258.52).into()),
1422                PathEl::LineTo((25.9, -175.24).into()),
1423                PathEl::LineTo((-79.75, -101.58).into()),
1424                PathEl::ClosePath,
1425            ],
1426        );
1427    }
1428
1429    #[test]
1430    fn draw_scaled_component_offset_freetype() {
1431        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1432        assert_glyph_path_start_with(
1433            &font,
1434            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1435            Location::default(),
1436            PathStyle::FreeType,
1437            &[
1438                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1439                PathEl::MoveTo((715.0, -360.0).into()),
1440            ],
1441        );
1442    }
1443
1444    #[test]
1445    fn draw_no_scaled_component_offset_freetype() {
1446        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1447        assert_glyph_path_start_with(
1448            &font,
1449            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1450            Location::default(),
1451            PathStyle::FreeType,
1452            &[PathEl::MoveTo((705.0, -340.0).into())],
1453        );
1454    }
1455
1456    #[test]
1457    fn draw_scaled_component_offset_harfbuzz() {
1458        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1459        assert_glyph_path_start_with(
1460            &font,
1461            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1462            Location::default(),
1463            PathStyle::HarfBuzz,
1464            &[
1465                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1466                PathEl::MoveTo((714.97, -360.0).into()),
1467            ],
1468        );
1469    }
1470
1471    #[test]
1472    fn draw_no_scaled_component_offset_harfbuzz() {
1473        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1474        assert_glyph_path_start_with(
1475            &font,
1476            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1477            Location::default(),
1478            PathStyle::HarfBuzz,
1479            &[PathEl::MoveTo((704.97, -340.0).into())],
1480        );
1481    }
1482
1483    const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
1484
1485    #[test]
1486    fn draw_cubic() {
1487        let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
1488        assert_glyph_path_start_with(
1489            &font,
1490            CUBIC_GLYPH,
1491            Location::default(),
1492            PathStyle::FreeType,
1493            &[
1494                PathEl::MoveTo((278.0, -710.0).into()),
1495                PathEl::LineTo((278.0, -470.0).into()),
1496                PathEl::CurveTo(
1497                    (300.0, -500.0).into(),
1498                    (800.0, -500.0).into(),
1499                    (998.0, -470.0).into(),
1500                ),
1501                PathEl::LineTo((998.0, -710.0).into()),
1502            ],
1503        );
1504    }
1505
1506    #[test]
1507    fn draw_extended_glyf_with_wide_components_and_independent_metrics() {
1508        use font_test_data::extended::outlines_font;
1509        let expected = "M0.0,40.0 C0.0,0.0 80.0,0.0 80.0,40.0 C80.0,80.0 0.0,80.0 0.0,40.0 Z";
1510        for long_loca in [false, true] {
1511            for legacy in [false, true] {
1512                for extended_metrics in [false, true] {
1513                    let data = outlines_font(true, legacy, extended_metrics, long_loca);
1514                    let font = FontRef::new(&data).unwrap();
1515                    let glyphs = font.outline_glyphs();
1516                    for gid in [1, 65536] {
1517                        let glyph = glyphs.get(GlyphId::new(gid)).unwrap();
1518                        for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1519                            let mut pen = SvgPen::with_precision(1);
1520                            let metrics = glyph
1521                                .draw(
1522                                    DrawSettings::unhinted(
1523                                        Size::unscaled(),
1524                                        LocationRef::default(),
1525                                    )
1526                                    .with_path_style(style),
1527                                    &mut pen,
1528                                )
1529                                .unwrap();
1530                            assert_eq!(pen.as_ref(), expected);
1531                            assert_eq!(metrics.advance_width, Some(1000.0));
1532                        }
1533                    }
1534                    assert!(glyphs.get(GlyphId::new(65537)).is_none());
1535                }
1536            }
1537        }
1538        // Uppercase metrics do not require uppercase outlines.
1539        let data = outlines_font(false, true, true, false);
1540        let font = FontRef::new(&data).unwrap();
1541        let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
1542        let mut pen = SvgPen::with_precision(1);
1543        glyph
1544            .draw((Size::unscaled(), LocationRef::default()), &mut pen)
1545            .unwrap();
1546        assert_eq!(pen.as_ref(), "M0.0,0.0 L10.0,0.0 L10.0,10.0 Z");
1547    }
1548
1549    #[test]
1550    fn scaled_extended_cubic_outlines_match_cached_metrics() {
1551        use font_test_data::extended::outlines_font;
1552        use read_fonts::model::Font;
1553
1554        for long_loca in [false, true] {
1555            let bytes = outlines_font(true, true, true, long_loca);
1556            let font = FontRef::new(&bytes).unwrap();
1557            let cached = Font::new(bytes.clone(), 0).unwrap();
1558            let outlines = font.outline_glyphs();
1559            for gid in [1, 65536] {
1560                let id = GlyphId::new(gid);
1561                let extents = cached.glyph_metrics().extents(id).unwrap();
1562                assert_eq!(
1563                    (extents.x_bearing, extents.width, extents.height),
1564                    (0.0, 80.0, 80.0)
1565                );
1566                for size in [125.0, 1000.0] {
1567                    let edge = size * 0.08;
1568                    let mid = edge / 2.0;
1569                    let expected = format!(
1570                        "M0.0,{mid:.1} C0.0,0.0 {edge:.1},0.0 {edge:.1},{mid:.1} C{edge:.1},{edge:.1} 0.0,{edge:.1} 0.0,{mid:.1} Z"
1571                    );
1572                    for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1573                        let mut pen = SvgPen::with_precision(1);
1574                        let adjusted = outlines
1575                            .get(id)
1576                            .unwrap()
1577                            .draw(
1578                                DrawSettings::unhinted(Size::new(size), LocationRef::default())
1579                                    .with_path_style(style),
1580                                &mut pen,
1581                            )
1582                            .unwrap();
1583                        assert_eq!(pen.as_ref(), expected, "gid {gid}, size {size}, {style:?}");
1584                        assert_eq!(adjusted.advance_width, Some(size));
1585                        assert_eq!(
1586                            font.glyph_metrics(Size::new(size), LocationRef::default())
1587                                .advance_width(id),
1588                            Some(size)
1589                        );
1590                    }
1591                }
1592            }
1593        }
1594    }
1595
1596    #[test]
1597    fn extended_gvar_draws_high_glyphs_and_varies_metrics() {
1598        use font_test_data::extended::variable_outlines_font;
1599        for long_offsets in [false, true] {
1600            for (extended, legacy) in [(true, false), (true, true), (false, true), (false, false)] {
1601                let bytes = variable_outlines_font(extended, legacy, long_offsets);
1602                let font = FontRef::new(&bytes).unwrap();
1603                let glyphs = font.outline_glyphs();
1604                for coord in [0.0, 0.5, 1.0] {
1605                    let coords = [NormalizedCoord::from_f32(coord)];
1606                    let location = LocationRef::new(&coords);
1607                    let metrics = font.glyph_metrics(Size::unscaled(), location);
1608                    for gid in [65536, 1] {
1609                        let shift = if gid == 1 {
1610                            (if extended { 40.0 } else { 0.0 })
1611                                + if extended || legacy { 20.0 } else { 0.0 }
1612                        } else if extended {
1613                            40.0
1614                        } else {
1615                            0.0
1616                        } * coord;
1617                        let advance_delta = if gid == 1 && (extended || legacy) {
1618                            60.0
1619                        } else if gid == 65536 && extended {
1620                            100.0
1621                        } else {
1622                            0.0
1623                        } * coord;
1624                        let right = shift + 80.0;
1625                        let expected = format!(
1626                            "M{shift:.1},40.0 C{shift:.1},0.0 {right:.1},0.0 {right:.1},40.0 C{right:.1},80.0 {shift:.1},80.0 {shift:.1},40.0 Z"
1627                        );
1628                        let id = GlyphId::new(gid);
1629                        assert_eq!(metrics.advance_width(id), Some(1000.0 + advance_delta));
1630                        for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1631                            let mut pen = SvgPen::with_precision(1);
1632                            let adjusted = glyphs
1633                                .get(id)
1634                                .unwrap()
1635                                .draw(
1636                                    DrawSettings::unhinted(Size::unscaled(), location)
1637                                        .with_path_style(style),
1638                                    &mut pen,
1639                                )
1640                                .unwrap();
1641                            assert_eq!(
1642                                pen.as_ref(),
1643                                expected,
1644                                "gid {gid}, coord {coord}, {style:?}"
1645                            );
1646                            assert_eq!(adjusted.advance_width, Some(1000.0 + advance_delta));
1647                        }
1648                    }
1649                }
1650            }
1651        }
1652    }
1653
1654    #[test]
1655    fn extended_gvar_instances_keep_metrics_and_outlines_in_sync() {
1656        use font_test_data::extended::variable_outlines_font;
1657        use read_fonts::model::Font;
1658
1659        for long_offsets in [false, true] {
1660            let bytes = variable_outlines_font(true, true, long_offsets);
1661            let font = FontRef::new(&bytes).unwrap();
1662            let cached = Font::new(bytes.clone(), 0).unwrap();
1663            let coords = [0.0, 1.0, 0.5, -0.5];
1664            let instances: Vec<_> = coords
1665                .iter()
1666                .map(|&coord| {
1667                    cached
1668                        .instance_builder()
1669                        .normalized_coords([NormalizedCoord::from_f32(coord)])
1670                        .build()
1671                })
1672                .collect();
1673            let outlines = font.outline_glyphs();
1674            for gid in [1, 65536] {
1675                let id = GlyphId::new(gid);
1676                let glyph = outlines.get(id).unwrap();
1677                // Revisit retained instances after other locations have populated caches.
1678                for index in [1, 0, 2, 3, 1, 2, 0] {
1679                    let location_coords = [NormalizedCoord::from_f32(coords[index])];
1680                    let location = LocationRef::new(&location_coords);
1681                    let metrics = instances[index].glyph_metrics();
1682                    let scalar = coords[index].max(0.0);
1683                    let shift = if gid == 1 { 60.0 } else { 40.0 } * scalar;
1684                    let advance = 1000.0 + if gid == 1 { 60.0 } else { 100.0 } * scalar;
1685                    assert_eq!(metrics.h_advance(id), advance);
1686                    assert_eq!(metrics.extents(id).unwrap().x_bearing, shift);
1687                    assert_eq!(
1688                        font.glyph_metrics(Size::unscaled(), location)
1689                            .advance_width(id),
1690                        Some(advance)
1691                    );
1692                    for style in [PathStyle::FreeType, PathStyle::HarfBuzz] {
1693                        let mut pen = SvgPen::with_precision(1);
1694                        let adjusted = glyph
1695                            .draw(
1696                                DrawSettings::unhinted(Size::new(125.0), location)
1697                                    .with_path_style(style),
1698                                &mut pen,
1699                            )
1700                            .unwrap();
1701                        let left = shift / 8.0;
1702                        let right = left + 10.0;
1703                        assert_eq!(pen.as_ref(), format!(
1704                            "M{left:.1},5.0 C{left:.1},0.0 {right:.1},0.0 {right:.1},5.0 C{right:.1},10.0 {left:.1},10.0 {left:.1},5.0 Z"
1705                        ), "gid {gid}, coord {}, {style:?}", coords[index]);
1706                        assert_eq!(adjusted.advance_width, Some(advance / 8.0));
1707                    }
1708                }
1709            }
1710        }
1711    }
1712
1713    /// Case where a font subset caused hinting to fail because execution
1714    /// budget was derived from glyph count.
1715    /// <https://github.com/googlefonts/fontations/issues/936>
1716    #[test]
1717    fn tthint_with_subset() {
1718        let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
1719        let glyphs = font.outline_glyphs();
1720        let hinting = HintingInstance::new(
1721            &glyphs,
1722            Size::new(16.0),
1723            LocationRef::default(),
1724            HintingOptions::default(),
1725        )
1726        .unwrap();
1727        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1728        // Shouldn't fail in pedantic mode
1729        glyph
1730            .draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
1731            .unwrap();
1732    }
1733
1734    #[test]
1735    fn empty_glyph_advance_unhinted() {
1736        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1737        let outlines = font.outline_glyphs();
1738        let coords = [NormalizedCoord::from_f32(0.5)];
1739        let gid = font.charmap().map(' ').unwrap();
1740        let outline = outlines.get(gid).unwrap();
1741        let advance = outline
1742            .draw(
1743                (Size::new(24.0), LocationRef::new(&coords)),
1744                &mut super::pen::NullPen,
1745            )
1746            .unwrap()
1747            .advance_width
1748            .unwrap();
1749        assert_eq!(advance, 10.796875);
1750    }
1751
1752    #[test]
1753    fn empty_glyph_advance_hinted() {
1754        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1755        let outlines = font.outline_glyphs();
1756        let coords = [NormalizedCoord::from_f32(0.5)];
1757        let hinter = HintingInstance::new(
1758            &outlines,
1759            Size::new(24.0),
1760            LocationRef::new(&coords),
1761            HintingOptions::default(),
1762        )
1763        .unwrap();
1764        let gid = font.charmap().map(' ').unwrap();
1765        let outline = outlines.get(gid).unwrap();
1766        let advance = outline
1767            .draw(&hinter, &mut super::pen::NullPen)
1768            .unwrap()
1769            .advance_width
1770            .unwrap();
1771        assert_eq!(advance, 11.0);
1772    }
1773
1774    /// Ensure we produce different TrueType outlines based on the
1775    /// fractional_size_hinting flag
1776    #[test]
1777    fn fractional_size_hinting_matters() {
1778        let font = FontRef::from_index(font_test_data::TINOS_SUBSET, 0).unwrap();
1779        let mut outlines = font.outline_glyphs();
1780        let instance = HintingInstance::new(
1781            &outlines,
1782            Size::new(24.8),
1783            LocationRef::default(),
1784            HintingOptions::default(),
1785        )
1786        .unwrap();
1787        let gid = GlyphId::new(2);
1788        let outline_with_fractional = {
1789            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1790                panic!("this is definitely a TrueType font");
1791            };
1792            glyf.fractional_size_hinting = true;
1793            let mut pen = SvgPen::new();
1794            let outline = outlines.get(gid).unwrap();
1795            outline.draw(&instance, &mut pen).unwrap();
1796            pen.to_string()
1797        };
1798        let outline_without_fractional = {
1799            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1800                panic!("this is definitely a TrueType font");
1801            };
1802            glyf.fractional_size_hinting = false;
1803            let mut pen = SvgPen::new();
1804            let outline = outlines.get(gid).unwrap();
1805            outline.draw(&instance, &mut pen).unwrap();
1806            pen.to_string()
1807        };
1808        assert_ne!(outline_with_fractional, outline_without_fractional);
1809    }
1810
1811    #[test]
1812    fn cff2_advance_widths() {
1813        check_cff_hinted_and_unhinted_advance_widths(font_test_data::CANTARELL_VF_TRIMMED, true);
1814    }
1815
1816    #[test]
1817    fn cff_advance_widths() {
1818        check_cff_hinted_and_unhinted_advance_widths(
1819            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
1820            false,
1821        );
1822    }
1823
1824    #[track_caller]
1825    fn check_cff_hinted_and_unhinted_advance_widths(font_data: &[u8], is_cff2: bool) {
1826        let font = FontRef::new(font_data).unwrap();
1827        let outlines = font.outline_glyphs();
1828        let OutlineCollectionKind::Cff(cff_outlines) = &outlines.kind else {
1829            panic!("this should be a CFF outline collection");
1830        };
1831        assert_eq!(cff_outlines.is_cff2(), is_cff2);
1832        let size = Size::new(16.0);
1833        let hinter = HintingInstance::new(
1834            &outlines,
1835            size,
1836            LocationRef::default(),
1837            HintingOptions::default(),
1838        )
1839        .unwrap();
1840        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
1841        assert!(num_glyphs > 0);
1842        for gid in 0..num_glyphs {
1843            let outline = outlines.get(gid.into()).unwrap();
1844            let hinted = outline
1845                .draw(&hinter, &mut super::pen::NullPen)
1846                .unwrap()
1847                .advance_width
1848                .unwrap();
1849            let unhinted = outline
1850                .draw(
1851                    DrawSettings::unhinted(size, LocationRef::default()),
1852                    &mut super::pen::NullPen,
1853                )
1854                .unwrap()
1855                .advance_width
1856                .unwrap();
1857            assert_eq!(hinted.round(), hinted);
1858            assert!(
1859                (hinted - unhinted).abs() <= 0.5,
1860                "advance widths should be close, got hinted={hinted}, unhinted={unhinted} for gid {gid}"
1861            );
1862        }
1863    }
1864
1865    #[test]
1866    fn autohint_completes_with_long_blues() {
1867        // This tiny font caused an infinite loop in the long blues code path
1868        // in the autohinter.
1869        static FONT_DATA: [u8; 352] = [
1870            0, 1, 0, 0, 0, 7, 0, 64, 0, 2, 0, 48, 99, 109, 97, 112, 0, 12, 6, 4, 0, 0, 0, 124, 0,
1871            0, 0, 44, 103, 108, 121, 102, 51, 203, 248, 103, 0, 0, 0, 168, 0, 0, 0, 40, 104, 101,
1872            97, 100, 95, 16, 64, 222, 0, 0, 0, 208, 0, 0, 0, 54, 104, 104, 101, 97, 0, 1, 0, 1, 0,
1873            0, 1, 8, 0, 0, 0, 36, 104, 109, 116, 120, 0, 0, 0, 0, 0, 0, 1, 44, 0, 0, 0, 6, 108,
1874            111, 99, 97, 0, 0, 0, 40, 0, 0, 1, 52, 0, 0, 0, 12, 109, 97, 120, 112, 0, 4, 0, 5, 0,
1875            0, 1, 64, 0, 0, 0, 32, 0, 0, 0, 1, 0, 3, 0, 1, 0, 0, 0, 12, 0, 4, 0, 32, 0, 0, 0, 4, 0,
1876            4, 0, 1, 0, 0, 5, 209, 255, 255, 0, 0, 5, 209, 255, 255, 250, 48, 0, 1, 0, 0, 0, 0, 0,
1877            1, 0, 0, 254, 212, 0, 100, 1, 244, 0, 4, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 100, 0, 0,
1878            255, 206, 0, 0, 254, 212, 0, 0, 1, 44, 1, 244, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1879            95, 15, 60, 245, 0, 0, 3, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1880            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1881            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
1882            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 1, 0, 0, 0, 2, 0, 5, 0, 1, 0, 0, 0, 0,
1883            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1884        ];
1885        let font = FontRef::new(&FONT_DATA).unwrap();
1886        let outlines = font.outline_glyphs();
1887        let hinter = HintingInstance::new(
1888            &outlines,
1889            Size::new(16.0),
1890            LocationRef::default(),
1891            Engine::Auto(None),
1892        )
1893        .unwrap();
1894        let gid = font.charmap().map('\u{05D1}').unwrap();
1895        let outline = outlines.get(gid).unwrap();
1896        let (tx, rx) = std::sync::mpsc::channel();
1897        std::thread::spawn(move || {
1898            outline.draw(&hinter, &mut NullPen).unwrap();
1899            let _ = tx.send(());
1900        });
1901        let budget = std::time::Duration::from_secs(5);
1902        match rx.recv_timeout(budget) {
1903            Ok(_) => {}
1904            Err(_) => {
1905                panic!("Autohinting did not complete within {budget:?}");
1906            }
1907        }
1908    }
1909}