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