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