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