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.try_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 read_fonts::{types::GlyphId, FontRef, TableProvider};
780
781    use pretty_assertions::assert_eq;
782
783    const PERIOD: u32 = 0x2E_u32;
784    const COMMA: u32 = 0x2C_u32;
785
786    #[test]
787    fn outline_glyph_formats() {
788        let font_format_pairs = [
789            (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
790            (
791                font_test_data::CANTARELL_VF_TRIMMED,
792                OutlineGlyphFormat::Cff2,
793            ),
794            (
795                font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
796                OutlineGlyphFormat::Cff,
797            ),
798            (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
799        ];
800        for (font_data, format) in font_format_pairs {
801            assert_eq!(
802                FontRef::new(font_data).unwrap().outline_glyphs().format(),
803                Some(format)
804            );
805        }
806    }
807
808    #[test]
809    fn vazirmatin_var() {
810        compare_glyphs(
811            font_test_data::VAZIRMATN_VAR,
812            font_test_data::VAZIRMATN_VAR_GLYPHS,
813        );
814    }
815
816    #[test]
817    fn cantarell_vf() {
818        compare_glyphs(
819            font_test_data::CANTARELL_VF_TRIMMED,
820            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
821        );
822    }
823
824    #[test]
825    fn noto_serif_display() {
826        compare_glyphs(
827            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
828            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
829        );
830    }
831
832    #[test]
833    fn overlap_flags() {
834        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
835        let outlines = font.outline_glyphs();
836        let glyph_count = font.maxp().unwrap().num_glyphs();
837        // GID 2 is a composite glyph with the overlap bit on a component
838        // GID 3 is a simple glyph with the overlap bit on the first flag
839        let expected_gids_with_overlap = vec![2, 3];
840        assert_eq!(
841            expected_gids_with_overlap,
842            (0..glyph_count)
843                .filter(
844                    |gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
845                )
846                .collect::<Vec<_>>()
847        );
848    }
849
850    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
851        let font = FontRef::new(font_data).unwrap();
852        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
853        let mut path = testing::Path::default();
854        for expected_outline in &expected_outlines {
855            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
856                continue;
857            }
858            let size = if expected_outline.size != 0.0 {
859                Size::new(expected_outline.size)
860            } else {
861                Size::unscaled()
862            };
863            path.elements.clear();
864            font.outline_glyphs()
865                .get(expected_outline.glyph_id)
866                .unwrap()
867                .draw(
868                    DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
869                    &mut path,
870                )
871                .unwrap();
872            assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
873                    expected_outline.glyph_id,
874                    expected_outline.size,
875                    expected_outline.coords,
876                    &path.elements,
877                    &expected_outline.path
878                );
879        }
880    }
881
882    #[derive(Copy, Clone, Debug, PartialEq)]
883    enum GlyphPoint {
884        On { x: f32, y: f32 },
885        Off { x: f32, y: f32 },
886    }
887
888    impl GlyphPoint {
889        fn implied_oncurve(&self, other: Self) -> Self {
890            let (x1, y1) = self.xy();
891            let (x2, y2) = other.xy();
892            Self::On {
893                x: (x1 + x2) / 2.0,
894                y: (y1 + y2) / 2.0,
895            }
896        }
897
898        fn xy(&self) -> (f32, f32) {
899            match self {
900                GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
901            }
902        }
903    }
904
905    #[derive(Debug)]
906    struct PointPen {
907        points: Vec<GlyphPoint>,
908    }
909
910    impl PointPen {
911        fn new() -> Self {
912            Self { points: Vec::new() }
913        }
914
915        fn into_points(self) -> Vec<GlyphPoint> {
916            self.points
917        }
918    }
919
920    impl OutlinePen for PointPen {
921        fn move_to(&mut self, x: f32, y: f32) {
922            self.points.push(GlyphPoint::On { x, y });
923        }
924
925        fn line_to(&mut self, x: f32, y: f32) {
926            self.points.push(GlyphPoint::On { x, y });
927        }
928
929        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
930            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
931            self.points.push(GlyphPoint::On { x, y });
932        }
933
934        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
935            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
936            self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
937            self.points.push(GlyphPoint::On { x, y });
938        }
939
940        fn close(&mut self) {
941            // We can't drop a 0-length closing line for fear of breaking interpolation compatibility
942            //     - some other instance might have it not 0-length
943            // However, if the last command isn't a line and ends at the subpath start we can drop the endpoint
944            //     - if any instance had it other than at the start there would be a closing line
945            //     - and it wouldn't be interpolation compatible
946            // See <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
947            let np = self.points.len();
948            // We need at least 3 points to satisfy subsequent conditions
949            if np > 2
950                && self.points[0] == self.points[np - 1]
951                && matches!(
952                    (self.points[0], self.points[np - 2]),
953                    (GlyphPoint::On { .. }, GlyphPoint::Off { .. })
954                )
955            {
956                self.points.pop();
957            }
958        }
959    }
960
961    const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
962        GlyphPoint::Off { x: 278.0, y: 710.0 },
963        GlyphPoint::On { x: 278.0, y: 470.0 },
964        GlyphPoint::On { x: 998.0, y: 470.0 },
965        GlyphPoint::On { x: 998.0, y: 710.0 },
966    ];
967
968    const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
969        GlyphPoint::Off { x: 278.0, y: 710.0 },
970        GlyphPoint::Off { x: 278.0, y: 470.0 },
971        GlyphPoint::On { x: 998.0, y: 470.0 },
972        GlyphPoint::Off { x: 998.0, y: 710.0 },
973        GlyphPoint::Off { x: 750.0, y: 500.0 },
974    ];
975
976    /// Captures the svg drawing command sequence, e.g. MLLZ.
977    ///
978    /// Intended use is to confirm the command sequence pushed to the pen is interpolation compatible.
979    #[derive(Default, Debug)]
980    struct CommandPen {
981        commands: String,
982    }
983
984    impl OutlinePen for CommandPen {
985        fn move_to(&mut self, _x: f32, _y: f32) {
986            self.commands.push('M');
987        }
988
989        fn line_to(&mut self, _x: f32, _y: f32) {
990            self.commands.push('L');
991        }
992
993        fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
994            self.commands.push('Q');
995        }
996
997        fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
998            self.commands.push('C');
999        }
1000
1001        fn close(&mut self) {
1002            self.commands.push('Z');
1003        }
1004    }
1005
1006    fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
1007        let font = FontRef::new(font).unwrap();
1008        let gid = font
1009            .cmap()
1010            .unwrap()
1011            .map_codepoint(codepoint)
1012            .unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
1013        let outlines = font.outline_glyphs();
1014        let outline = outlines.get(gid).unwrap_or_else(|| {
1015            panic!(
1016                "No outline for {gid:?} in collection of {:?}",
1017                outlines.format()
1018            )
1019        });
1020
1021        outline.draw(settings, pen).unwrap();
1022    }
1023
1024    fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
1025        let mut pen = CommandPen::default();
1026        draw_to_pen(font, codepoint, settings, &mut pen);
1027        pen.commands
1028    }
1029
1030    fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
1031        let mut pen = PointPen::new();
1032        draw_to_pen(font, codepoint, settings, &mut pen);
1033        pen.into_points()
1034    }
1035
1036    fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
1037        let mut expanded_points = Vec::new();
1038
1039        for i in 0..pointstream.len() - 1 {
1040            expanded_points.push(pointstream[i]);
1041            if matches!(
1042                (pointstream[i], pointstream[i + 1]),
1043                (GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
1044            ) {
1045                expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
1046            }
1047        }
1048
1049        expanded_points.push(*pointstream.last().unwrap());
1050
1051        expanded_points
1052    }
1053
1054    fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
1055        points
1056            .iter()
1057            .map(|p| match p {
1058                GlyphPoint::On { .. } => "On",
1059                GlyphPoint::Off { .. } => "Off",
1060            })
1061            .collect()
1062    }
1063
1064    #[test]
1065    fn always_get_closing_lines() {
1066        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
1067        let period = draw_commands(
1068            font_test_data::INTERPOLATE_THIS,
1069            PERIOD,
1070            Size::unscaled().into(),
1071        );
1072        let comma = draw_commands(
1073            font_test_data::INTERPOLATE_THIS,
1074            COMMA,
1075            Size::unscaled().into(),
1076        );
1077
1078        assert_eq!(
1079            period, comma,
1080            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1081        );
1082        assert_eq!(
1083            "MLLLZ", period,
1084            "We should get an explicit L for close even when it's a nop"
1085        );
1086    }
1087
1088    #[test]
1089    fn triangle_and_square_retain_compatibility() {
1090        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
1091        let period = drawn_points(
1092            font_test_data::INTERPOLATE_THIS,
1093            PERIOD,
1094            Size::unscaled().into(),
1095        );
1096        let comma = drawn_points(
1097            font_test_data::INTERPOLATE_THIS,
1098            COMMA,
1099            Size::unscaled().into(),
1100        );
1101
1102        assert_ne!(period, comma);
1103        assert_eq!(
1104            as_on_off_sequence(&period),
1105            as_on_off_sequence(&comma),
1106            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1107        );
1108        assert_eq!(
1109            4,
1110            period.len(),
1111            "we should have the same # of points we started with"
1112        );
1113    }
1114
1115    fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
1116        assert!(
1117            matches!(pointstream[0], GlyphPoint::Off { .. }),
1118            "Bad testdata, should start off curve"
1119        );
1120
1121        // The default: look for an oncurve at the back, as freetype would do
1122        let mut expected_points = pointstream.to_vec();
1123        let last = *expected_points.last().unwrap();
1124        let first_move = if matches!(last, GlyphPoint::Off { .. }) {
1125            expected_points[0].implied_oncurve(last)
1126        } else {
1127            expected_points.pop().unwrap()
1128        };
1129        expected_points.insert(0, first_move);
1130
1131        expected_points = insert_implicit_oncurve(&expected_points);
1132        let actual = drawn_points(font, PERIOD, Size::unscaled().into());
1133        assert_eq!(
1134            expected_points, actual,
1135            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1136        );
1137    }
1138
1139    fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
1140        assert!(
1141            matches!(pointstream[0], GlyphPoint::Off { .. }),
1142            "Bad testdata, should start off curve"
1143        );
1144
1145        // look for an oncurve at the front, as harfbuzz would do
1146        let mut expected_points = pointstream.to_vec();
1147        let first = expected_points.remove(0);
1148        expected_points.push(first);
1149        if matches!(expected_points[0], GlyphPoint::Off { .. }) {
1150            expected_points.insert(0, first.implied_oncurve(expected_points[0]))
1151        };
1152
1153        expected_points = insert_implicit_oncurve(&expected_points);
1154
1155        let settings: DrawSettings = Size::unscaled().into();
1156        let settings = settings.with_path_style(PathStyle::HarfBuzz);
1157        let actual = drawn_points(font, PERIOD, settings);
1158        assert_eq!(
1159            expected_points, actual,
1160            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1161        );
1162    }
1163
1164    #[test]
1165    fn starting_off_curve_walk_backwards_like_freetype() {
1166        assert_walked_backwards_like_freetype(
1167            &STARTING_OFF_CURVE_POINTS,
1168            font_test_data::STARTING_OFF_CURVE,
1169        );
1170    }
1171
1172    #[test]
1173    fn mostly_off_curve_walk_backwards_like_freetype() {
1174        assert_walked_backwards_like_freetype(
1175            &MOSTLY_OFF_CURVE_POINTS,
1176            font_test_data::MOSTLY_OFF_CURVE,
1177        );
1178    }
1179
1180    #[test]
1181    fn starting_off_curve_walk_forwards_like_hbdraw() {
1182        assert_walked_forwards_like_harfbuzz(
1183            &STARTING_OFF_CURVE_POINTS,
1184            font_test_data::STARTING_OFF_CURVE,
1185        );
1186    }
1187
1188    #[test]
1189    fn mostly_off_curve_walk_forwards_like_hbdraw() {
1190        assert_walked_forwards_like_harfbuzz(
1191            &MOSTLY_OFF_CURVE_POINTS,
1192            font_test_data::MOSTLY_OFF_CURVE,
1193        );
1194    }
1195
1196    // A location noted for making FreeType and HarfBuzz results differ
1197    // See https://github.com/googlefonts/sleipnir/pull/15
1198    fn icon_loc_off_default(font: &FontRef) -> Location {
1199        font.axes().location(&[
1200            ("wght", 700.0),
1201            ("opsz", 48.0),
1202            ("GRAD", 200.0),
1203            ("FILL", 1.0),
1204        ])
1205    }
1206
1207    fn pt(x: f32, y: f32) -> Point {
1208        (x as f64, y as f64).into()
1209    }
1210
1211    // String command rounded to two decimal places, suitable for assert comparison
1212    fn svg_commands(elements: &[PathEl]) -> Vec<String> {
1213        elements
1214            .iter()
1215            .map(|e| match e {
1216                PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
1217                PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
1218                PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
1219                PathEl::CurveTo(c0, c1, p) => format!(
1220                    "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1221                    c0.x, c0.y, c1.x, c1.y, p.x, p.y
1222                ),
1223                PathEl::ClosePath => "Z".to_string(),
1224            })
1225            .collect()
1226    }
1227
1228    // Declared here to avoid a write-fonts dependency that is awkward for google3 at time of writing
1229    #[derive(Default)]
1230    struct BezPen {
1231        path: BezPath,
1232    }
1233
1234    impl OutlinePen for BezPen {
1235        fn move_to(&mut self, x: f32, y: f32) {
1236            self.path.move_to(pt(x, y));
1237        }
1238
1239        fn line_to(&mut self, x: f32, y: f32) {
1240            self.path.line_to(pt(x, y));
1241        }
1242
1243        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1244            self.path.quad_to(pt(cx0, cy0), pt(x, y));
1245        }
1246
1247        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1248            self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
1249        }
1250
1251        fn close(&mut self) {
1252            self.path.close_path();
1253        }
1254    }
1255
1256    // We take glyph id here to bypass the need to resolve codepoint:gid and apply substitutions
1257    fn assert_glyph_path_start_with(
1258        font: &FontRef,
1259        gid: GlyphId,
1260        loc: Location,
1261        path_style: PathStyle,
1262        expected_path_start: &[PathEl],
1263    ) {
1264        let glyph = font
1265            .outline_glyphs()
1266            .get(gid)
1267            .unwrap_or_else(|| panic!("No glyph for {gid}"));
1268
1269        let mut pen = BezPen::default();
1270        glyph
1271            .draw(
1272                DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
1273                &mut pen,
1274            )
1275            .unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
1276        let bez = Affine::FLIP_Y * pen.path; // like an icon svg
1277        let actual_path_start = &bez.elements()[..expected_path_start.len()];
1278        // round2 can still leave very small differences from the typed 2-decimal value
1279        // and the diff isn't pleasant so just compare as svg string fragments
1280        assert_eq!(
1281            svg_commands(expected_path_start),
1282            svg_commands(actual_path_start)
1283        );
1284    }
1285
1286    const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
1287    const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
1288
1289    #[test]
1290    fn draw_icon_freetype_style_at_default() {
1291        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1292        assert_glyph_path_start_with(
1293            &font,
1294            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1295            Location::default(),
1296            PathStyle::FreeType,
1297            &[
1298                PathEl::MoveTo((160.0, -160.0).into()),
1299                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1300                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1301            ],
1302        );
1303    }
1304
1305    #[test]
1306    fn draw_icon_harfbuzz_style_at_default() {
1307        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1308        assert_glyph_path_start_with(
1309            &font,
1310            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1311            Location::default(),
1312            PathStyle::HarfBuzz,
1313            &[
1314                PathEl::MoveTo((160.0, -160.0).into()),
1315                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1316                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1317            ],
1318        );
1319    }
1320
1321    #[test]
1322    fn draw_icon_freetype_style_off_default() {
1323        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1324        assert_glyph_path_start_with(
1325            &font,
1326            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1327            icon_loc_off_default(&font),
1328            PathStyle::FreeType,
1329            &[
1330                PathEl::MoveTo((150.0, -138.0).into()),
1331                PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
1332                PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
1333            ],
1334        );
1335    }
1336
1337    #[test]
1338    fn draw_icon_harfbuzz_style_off_default() {
1339        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1340        assert_glyph_path_start_with(
1341            &font,
1342            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1343            icon_loc_off_default(&font),
1344            PathStyle::HarfBuzz,
1345            &[
1346                PathEl::MoveTo((150.0, -138.0).into()),
1347                PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
1348                PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
1349            ],
1350        );
1351    }
1352
1353    const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
1354    const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
1355    const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
1356
1357    #[test]
1358    fn draw_nonuniform_scale_component_freetype() {
1359        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1360        assert_glyph_path_start_with(
1361            &font,
1362            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1363            Location::default(),
1364            PathStyle::FreeType,
1365            &[
1366                PathEl::MoveTo((-138.0, -185.0).into()),
1367                PathEl::LineTo((-32.0, -259.0).into()),
1368                PathEl::LineTo((26.0, -175.0).into()),
1369                PathEl::LineTo((-80.0, -101.0).into()),
1370                PathEl::ClosePath,
1371            ],
1372        );
1373    }
1374
1375    #[test]
1376    fn draw_nonuniform_scale_component_harfbuzz() {
1377        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1378        assert_glyph_path_start_with(
1379            &font,
1380            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1381            Location::default(),
1382            PathStyle::HarfBuzz,
1383            &[
1384                PathEl::MoveTo((-137.8, -184.86).into()),
1385                PathEl::LineTo((-32.15, -258.52).into()),
1386                PathEl::LineTo((25.9, -175.24).into()),
1387                PathEl::LineTo((-79.75, -101.58).into()),
1388                PathEl::ClosePath,
1389            ],
1390        );
1391    }
1392
1393    #[test]
1394    fn draw_scaled_component_offset_freetype() {
1395        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1396        assert_glyph_path_start_with(
1397            &font,
1398            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1399            Location::default(),
1400            PathStyle::FreeType,
1401            &[
1402                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1403                PathEl::MoveTo((715.0, -360.0).into()),
1404            ],
1405        );
1406    }
1407
1408    #[test]
1409    fn draw_no_scaled_component_offset_freetype() {
1410        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1411        assert_glyph_path_start_with(
1412            &font,
1413            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1414            Location::default(),
1415            PathStyle::FreeType,
1416            &[PathEl::MoveTo((705.0, -340.0).into())],
1417        );
1418    }
1419
1420    #[test]
1421    fn draw_scaled_component_offset_harfbuzz() {
1422        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1423        assert_glyph_path_start_with(
1424            &font,
1425            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1426            Location::default(),
1427            PathStyle::HarfBuzz,
1428            &[
1429                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1430                PathEl::MoveTo((714.97, -360.0).into()),
1431            ],
1432        );
1433    }
1434
1435    #[test]
1436    fn draw_no_scaled_component_offset_harfbuzz() {
1437        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1438        assert_glyph_path_start_with(
1439            &font,
1440            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1441            Location::default(),
1442            PathStyle::HarfBuzz,
1443            &[PathEl::MoveTo((704.97, -340.0).into())],
1444        );
1445    }
1446
1447    #[cfg(feature = "spec_next")]
1448    const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
1449
1450    #[test]
1451    #[cfg(feature = "spec_next")]
1452    fn draw_cubic() {
1453        let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
1454        assert_glyph_path_start_with(
1455            &font,
1456            CUBIC_GLYPH,
1457            Location::default(),
1458            PathStyle::FreeType,
1459            &[
1460                PathEl::MoveTo((278.0, -710.0).into()),
1461                PathEl::LineTo((278.0, -470.0).into()),
1462                PathEl::CurveTo(
1463                    (300.0, -500.0).into(),
1464                    (800.0, -500.0).into(),
1465                    (998.0, -470.0).into(),
1466                ),
1467                PathEl::LineTo((998.0, -710.0).into()),
1468            ],
1469        );
1470    }
1471
1472    /// Case where a font subset caused hinting to fail because execution
1473    /// budget was derived from glyph count.
1474    /// <https://github.com/googlefonts/fontations/issues/936>
1475    #[test]
1476    fn tthint_with_subset() {
1477        let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
1478        let glyphs = font.outline_glyphs();
1479        let hinting = HintingInstance::new(
1480            &glyphs,
1481            Size::new(16.0),
1482            LocationRef::default(),
1483            HintingOptions::default(),
1484        )
1485        .unwrap();
1486        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1487        // Shouldn't fail in pedantic mode
1488        glyph
1489            .draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
1490            .unwrap();
1491    }
1492
1493    #[test]
1494    fn empty_glyph_advance_unhinted() {
1495        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1496        let outlines = font.outline_glyphs();
1497        let coords = [NormalizedCoord::from_f32(0.5)];
1498        let gid = font.charmap().map(' ').unwrap();
1499        let outline = outlines.get(gid).unwrap();
1500        let advance = outline
1501            .draw(
1502                (Size::new(24.0), LocationRef::new(&coords)),
1503                &mut super::pen::NullPen,
1504            )
1505            .unwrap()
1506            .advance_width
1507            .unwrap();
1508        assert_eq!(advance, 10.796875);
1509    }
1510
1511    #[test]
1512    fn empty_glyph_advance_hinted() {
1513        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1514        let outlines = font.outline_glyphs();
1515        let coords = [NormalizedCoord::from_f32(0.5)];
1516        let hinter = HintingInstance::new(
1517            &outlines,
1518            Size::new(24.0),
1519            LocationRef::new(&coords),
1520            HintingOptions::default(),
1521        )
1522        .unwrap();
1523        let gid = font.charmap().map(' ').unwrap();
1524        let outline = outlines.get(gid).unwrap();
1525        let advance = outline
1526            .draw(&hinter, &mut super::pen::NullPen)
1527            .unwrap()
1528            .advance_width
1529            .unwrap();
1530        assert_eq!(advance, 11.0);
1531    }
1532
1533    /// Ensure we produce different TrueType outlines based on the
1534    /// fractional_size_hinting flag
1535    #[test]
1536    fn fractional_size_hinting_matters() {
1537        let font = FontRef::from_index(font_test_data::TINOS_SUBSET, 0).unwrap();
1538        let mut outlines = font.outline_glyphs();
1539        let instance = HintingInstance::new(
1540            &outlines,
1541            Size::new(24.8),
1542            LocationRef::default(),
1543            HintingOptions::default(),
1544        )
1545        .unwrap();
1546        let gid = GlyphId::new(2);
1547        let outline_with_fractional = {
1548            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1549                panic!("this is definitely a TrueType font");
1550            };
1551            glyf.fractional_size_hinting = true;
1552            let mut pen = SvgPen::new();
1553            let outline = outlines.get(gid).unwrap();
1554            outline.draw(&instance, &mut pen).unwrap();
1555            pen.to_string()
1556        };
1557        let outline_without_fractional = {
1558            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1559                panic!("this is definitely a TrueType font");
1560            };
1561            glyf.fractional_size_hinting = false;
1562            let mut pen = SvgPen::new();
1563            let outline = outlines.get(gid).unwrap();
1564            outline.draw(&instance, &mut pen).unwrap();
1565            pen.to_string()
1566        };
1567        assert_ne!(outline_with_fractional, outline_without_fractional);
1568    }
1569
1570    #[test]
1571    fn cff2_advance_widths() {
1572        check_cff_hinted_and_unhinted_advance_widths(font_test_data::CANTARELL_VF_TRIMMED, true);
1573    }
1574
1575    #[test]
1576    fn cff_advance_widths() {
1577        check_cff_hinted_and_unhinted_advance_widths(
1578            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
1579            false,
1580        );
1581    }
1582
1583    #[track_caller]
1584    fn check_cff_hinted_and_unhinted_advance_widths(font_data: &[u8], is_cff2: bool) {
1585        let font = FontRef::new(font_data).unwrap();
1586        let outlines = font.outline_glyphs();
1587        let OutlineCollectionKind::Cff(cff_outlines) = &outlines.kind else {
1588            panic!("this should be a CFF outline collection");
1589        };
1590        assert_eq!(cff_outlines.is_cff2(), is_cff2);
1591        let size = Size::new(16.0);
1592        let hinter = HintingInstance::new(
1593            &outlines,
1594            size,
1595            LocationRef::default(),
1596            HintingOptions::default(),
1597        )
1598        .unwrap();
1599        let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
1600        assert!(num_glyphs > 0);
1601        for gid in 0..num_glyphs {
1602            let outline = outlines.get(gid.into()).unwrap();
1603            let hinted = outline
1604                .draw(&hinter, &mut super::pen::NullPen)
1605                .unwrap()
1606                .advance_width
1607                .unwrap();
1608            let unhinted = outline
1609                .draw(
1610                    DrawSettings::unhinted(size, LocationRef::default()),
1611                    &mut super::pen::NullPen,
1612                )
1613                .unwrap()
1614                .advance_width
1615                .unwrap();
1616            assert_eq!(hinted.round(), hinted);
1617            assert!(
1618                (hinted - unhinted).abs() <= 0.5,
1619                "advance widths should be close, got hinted={hinted}, unhinted={unhinted} for gid {gid}"
1620            );
1621        }
1622    }
1623}