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    /// Returns true when the font supports hinting at fractional sizes.
645    ///
646    /// When this returns false, the requested size will be rounded before
647    /// computing the scale factor for hinted glyphs.
648    pub fn fractional_size_hinting(&self) -> bool {
649        match &self.kind {
650            OutlineCollectionKind::Glyf(glyf) => glyf.fractional_size_hinting,
651            _ => true,
652        }
653    }
654
655    pub(crate) fn font(&self) -> Option<&FontRef<'a>> {
656        match &self.kind {
657            OutlineCollectionKind::Glyf(glyf) => Some(&glyf.font),
658            OutlineCollectionKind::Cff(cff) => Some(&cff.font),
659            _ => None,
660        }
661    }
662}
663
664#[derive(Clone)]
665enum OutlineCollectionKind<'a> {
666    None,
667    Glyf(glyf::Outlines<'a>),
668    Cff(cff::Outlines<'a>),
669}
670
671impl Debug for OutlineCollectionKind<'_> {
672    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
673        match self {
674            Self::None => write!(f, "None"),
675            Self::Glyf(..) => f.debug_tuple("Glyf").finish(),
676            Self::Cff(..) => f.debug_tuple("Cff").finish(),
677        }
678    }
679}
680
681/// Invokes the callback with a memory buffer suitable for drawing
682/// the given TrueType outline.
683pub(super) fn with_glyf_memory<R>(
684    outline: &glyf::Outline,
685    hinting: Hinting,
686    memory: Option<&mut [u8]>,
687    mut f: impl FnMut(&mut [u8]) -> R,
688) -> R {
689    match memory {
690        Some(buf) => f(buf),
691        None => {
692            let buf_size = outline.required_buffer_size(hinting);
693            memory::with_temporary_memory(buf_size, f)
694        }
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::{instance::Location, outline::pen::SvgPen, MetadataProvider};
702    use kurbo::{Affine, BezPath, PathEl, Point};
703    use read_fonts::{types::GlyphId, FontRef, TableProvider};
704
705    use pretty_assertions::assert_eq;
706
707    const PERIOD: u32 = 0x2E_u32;
708    const COMMA: u32 = 0x2C_u32;
709
710    #[test]
711    fn outline_glyph_formats() {
712        let font_format_pairs = [
713            (font_test_data::VAZIRMATN_VAR, OutlineGlyphFormat::Glyf),
714            (
715                font_test_data::CANTARELL_VF_TRIMMED,
716                OutlineGlyphFormat::Cff2,
717            ),
718            (
719                font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
720                OutlineGlyphFormat::Cff,
721            ),
722            (font_test_data::COLRV0V1_VARIABLE, OutlineGlyphFormat::Glyf),
723        ];
724        for (font_data, format) in font_format_pairs {
725            assert_eq!(
726                FontRef::new(font_data).unwrap().outline_glyphs().format(),
727                Some(format)
728            );
729        }
730    }
731
732    #[test]
733    fn vazirmatin_var() {
734        compare_glyphs(
735            font_test_data::VAZIRMATN_VAR,
736            font_test_data::VAZIRMATN_VAR_GLYPHS,
737        );
738    }
739
740    #[test]
741    fn cantarell_vf() {
742        compare_glyphs(
743            font_test_data::CANTARELL_VF_TRIMMED,
744            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
745        );
746    }
747
748    #[test]
749    fn noto_serif_display() {
750        compare_glyphs(
751            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
752            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
753        );
754    }
755
756    #[test]
757    fn overlap_flags() {
758        let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
759        let outlines = font.outline_glyphs();
760        let glyph_count = font.maxp().unwrap().num_glyphs();
761        // GID 2 is a composite glyph with the overlap bit on a component
762        // GID 3 is a simple glyph with the overlap bit on the first flag
763        let expected_gids_with_overlap = vec![2, 3];
764        assert_eq!(
765            expected_gids_with_overlap,
766            (0..glyph_count)
767                .filter(
768                    |gid| outlines.get(GlyphId::from(*gid)).unwrap().has_overlaps() == Some(true)
769                )
770                .collect::<Vec<_>>()
771        );
772    }
773
774    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
775        let font = FontRef::new(font_data).unwrap();
776        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
777        let mut path = testing::Path::default();
778        for expected_outline in &expected_outlines {
779            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
780                continue;
781            }
782            let size = if expected_outline.size != 0.0 {
783                Size::new(expected_outline.size)
784            } else {
785                Size::unscaled()
786            };
787            path.elements.clear();
788            font.outline_glyphs()
789                .get(expected_outline.glyph_id)
790                .unwrap()
791                .draw(
792                    DrawSettings::unhinted(size, expected_outline.coords.as_slice()),
793                    &mut path,
794                )
795                .unwrap();
796            assert_eq!(path.elements, expected_outline.path, "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
797                    expected_outline.glyph_id,
798                    expected_outline.size,
799                    expected_outline.coords,
800                    &path.elements,
801                    &expected_outline.path
802                );
803        }
804    }
805
806    #[derive(Copy, Clone, Debug, PartialEq)]
807    enum GlyphPoint {
808        On { x: f32, y: f32 },
809        Off { x: f32, y: f32 },
810    }
811
812    impl GlyphPoint {
813        fn implied_oncurve(&self, other: Self) -> Self {
814            let (x1, y1) = self.xy();
815            let (x2, y2) = other.xy();
816            Self::On {
817                x: (x1 + x2) / 2.0,
818                y: (y1 + y2) / 2.0,
819            }
820        }
821
822        fn xy(&self) -> (f32, f32) {
823            match self {
824                GlyphPoint::On { x, y } | GlyphPoint::Off { x, y } => (*x, *y),
825            }
826        }
827    }
828
829    #[derive(Debug)]
830    struct PointPen {
831        points: Vec<GlyphPoint>,
832    }
833
834    impl PointPen {
835        fn new() -> Self {
836            Self { points: Vec::new() }
837        }
838
839        fn into_points(self) -> Vec<GlyphPoint> {
840            self.points
841        }
842    }
843
844    impl OutlinePen for PointPen {
845        fn move_to(&mut self, x: f32, y: f32) {
846            self.points.push(GlyphPoint::On { x, y });
847        }
848
849        fn line_to(&mut self, x: f32, y: f32) {
850            self.points.push(GlyphPoint::On { x, y });
851        }
852
853        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
854            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
855            self.points.push(GlyphPoint::On { x, y });
856        }
857
858        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
859            self.points.push(GlyphPoint::Off { x: cx0, y: cy0 });
860            self.points.push(GlyphPoint::Off { x: cx1, y: cy1 });
861            self.points.push(GlyphPoint::On { x, y });
862        }
863
864        fn close(&mut self) {
865            // We can't drop a 0-length closing line for fear of breaking interpolation compatibility
866            //     - some other instance might have it not 0-length
867            // However, if the last command isn't a line and ends at the subpath start we can drop the endpoint
868            //     - if any instance had it other than at the start there would be a closing line
869            //     - and it wouldn't be interpolation compatible
870            // See <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
871            let np = self.points.len();
872            // We need at least 3 points to satisfy subsequent conditions
873            if np > 2
874                && self.points[0] == self.points[np - 1]
875                && matches!(
876                    (self.points[0], self.points[np - 2]),
877                    (GlyphPoint::On { .. }, GlyphPoint::Off { .. })
878                )
879            {
880                self.points.pop();
881            }
882        }
883    }
884
885    const STARTING_OFF_CURVE_POINTS: [GlyphPoint; 4] = [
886        GlyphPoint::Off { x: 278.0, y: 710.0 },
887        GlyphPoint::On { x: 278.0, y: 470.0 },
888        GlyphPoint::On { x: 998.0, y: 470.0 },
889        GlyphPoint::On { x: 998.0, y: 710.0 },
890    ];
891
892    const MOSTLY_OFF_CURVE_POINTS: [GlyphPoint; 5] = [
893        GlyphPoint::Off { x: 278.0, y: 710.0 },
894        GlyphPoint::Off { x: 278.0, y: 470.0 },
895        GlyphPoint::On { x: 998.0, y: 470.0 },
896        GlyphPoint::Off { x: 998.0, y: 710.0 },
897        GlyphPoint::Off { x: 750.0, y: 500.0 },
898    ];
899
900    /// Captures the svg drawing command sequence, e.g. MLLZ.
901    ///
902    /// Intended use is to confirm the command sequence pushed to the pen is interpolation compatible.
903    #[derive(Default, Debug)]
904    struct CommandPen {
905        commands: String,
906    }
907
908    impl OutlinePen for CommandPen {
909        fn move_to(&mut self, _x: f32, _y: f32) {
910            self.commands.push('M');
911        }
912
913        fn line_to(&mut self, _x: f32, _y: f32) {
914            self.commands.push('L');
915        }
916
917        fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {
918            self.commands.push('Q');
919        }
920
921        fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {
922            self.commands.push('C');
923        }
924
925        fn close(&mut self) {
926            self.commands.push('Z');
927        }
928    }
929
930    fn draw_to_pen(font: &[u8], codepoint: u32, settings: DrawSettings, pen: &mut impl OutlinePen) {
931        let font = FontRef::new(font).unwrap();
932        let gid = font
933            .cmap()
934            .unwrap()
935            .map_codepoint(codepoint)
936            .unwrap_or_else(|| panic!("No gid for 0x{codepoint:04x}"));
937        let outlines = font.outline_glyphs();
938        let outline = outlines.get(gid).unwrap_or_else(|| {
939            panic!(
940                "No outline for {gid:?} in collection of {:?}",
941                outlines.format()
942            )
943        });
944
945        outline.draw(settings, pen).unwrap();
946    }
947
948    fn draw_commands(font: &[u8], codepoint: u32, settings: DrawSettings) -> String {
949        let mut pen = CommandPen::default();
950        draw_to_pen(font, codepoint, settings, &mut pen);
951        pen.commands
952    }
953
954    fn drawn_points(font: &[u8], codepoint: u32, settings: DrawSettings) -> Vec<GlyphPoint> {
955        let mut pen = PointPen::new();
956        draw_to_pen(font, codepoint, settings, &mut pen);
957        pen.into_points()
958    }
959
960    fn insert_implicit_oncurve(pointstream: &[GlyphPoint]) -> Vec<GlyphPoint> {
961        let mut expanded_points = Vec::new();
962
963        for i in 0..pointstream.len() - 1 {
964            expanded_points.push(pointstream[i]);
965            if matches!(
966                (pointstream[i], pointstream[i + 1]),
967                (GlyphPoint::Off { .. }, GlyphPoint::Off { .. })
968            ) {
969                expanded_points.push(pointstream[i].implied_oncurve(pointstream[i + 1]));
970            }
971        }
972
973        expanded_points.push(*pointstream.last().unwrap());
974
975        expanded_points
976    }
977
978    fn as_on_off_sequence(points: &[GlyphPoint]) -> Vec<&'static str> {
979        points
980            .iter()
981            .map(|p| match p {
982                GlyphPoint::On { .. } => "On",
983                GlyphPoint::Off { .. } => "Off",
984            })
985            .collect()
986    }
987
988    #[test]
989    fn always_get_closing_lines() {
990        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
991        let period = draw_commands(
992            font_test_data::INTERPOLATE_THIS,
993            PERIOD,
994            Size::unscaled().into(),
995        );
996        let comma = draw_commands(
997            font_test_data::INTERPOLATE_THIS,
998            COMMA,
999            Size::unscaled().into(),
1000        );
1001
1002        assert_eq!(
1003            period, comma,
1004            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1005        );
1006        assert_eq!(
1007            "MLLLZ", period,
1008            "We should get an explicit L for close even when it's a nop"
1009        );
1010    }
1011
1012    #[test]
1013    fn triangle_and_square_retain_compatibility() {
1014        // <https://github.com/googlefonts/fontations/pull/818/files#r1521188624>
1015        let period = drawn_points(
1016            font_test_data::INTERPOLATE_THIS,
1017            PERIOD,
1018            Size::unscaled().into(),
1019        );
1020        let comma = drawn_points(
1021            font_test_data::INTERPOLATE_THIS,
1022            COMMA,
1023            Size::unscaled().into(),
1024        );
1025
1026        assert_ne!(period, comma);
1027        assert_eq!(
1028            as_on_off_sequence(&period),
1029            as_on_off_sequence(&comma),
1030            "Incompatible\nperiod\n{period:#?}\ncomma\n{comma:#?}\n"
1031        );
1032        assert_eq!(
1033            4,
1034            period.len(),
1035            "we should have the same # of points we started with"
1036        );
1037    }
1038
1039    fn assert_walked_backwards_like_freetype(pointstream: &[GlyphPoint], font: &[u8]) {
1040        assert!(
1041            matches!(pointstream[0], GlyphPoint::Off { .. }),
1042            "Bad testdata, should start off curve"
1043        );
1044
1045        // The default: look for an oncurve at the back, as freetype would do
1046        let mut expected_points = pointstream.to_vec();
1047        let last = *expected_points.last().unwrap();
1048        let first_move = if matches!(last, GlyphPoint::Off { .. }) {
1049            expected_points[0].implied_oncurve(last)
1050        } else {
1051            expected_points.pop().unwrap()
1052        };
1053        expected_points.insert(0, first_move);
1054
1055        expected_points = insert_implicit_oncurve(&expected_points);
1056        let actual = drawn_points(font, PERIOD, Size::unscaled().into());
1057        assert_eq!(
1058            expected_points, actual,
1059            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1060        );
1061    }
1062
1063    fn assert_walked_forwards_like_harfbuzz(pointstream: &[GlyphPoint], font: &[u8]) {
1064        assert!(
1065            matches!(pointstream[0], GlyphPoint::Off { .. }),
1066            "Bad testdata, should start off curve"
1067        );
1068
1069        // look for an oncurve at the front, as harfbuzz would do
1070        let mut expected_points = pointstream.to_vec();
1071        let first = expected_points.remove(0);
1072        expected_points.push(first);
1073        if matches!(expected_points[0], GlyphPoint::Off { .. }) {
1074            expected_points.insert(0, first.implied_oncurve(expected_points[0]))
1075        };
1076
1077        expected_points = insert_implicit_oncurve(&expected_points);
1078
1079        let settings: DrawSettings = Size::unscaled().into();
1080        let settings = settings.with_path_style(PathStyle::HarfBuzz);
1081        let actual = drawn_points(font, PERIOD, settings);
1082        assert_eq!(
1083            expected_points, actual,
1084            "expected\n{expected_points:#?}\nactual\n{actual:#?}"
1085        );
1086    }
1087
1088    #[test]
1089    fn starting_off_curve_walk_backwards_like_freetype() {
1090        assert_walked_backwards_like_freetype(
1091            &STARTING_OFF_CURVE_POINTS,
1092            font_test_data::STARTING_OFF_CURVE,
1093        );
1094    }
1095
1096    #[test]
1097    fn mostly_off_curve_walk_backwards_like_freetype() {
1098        assert_walked_backwards_like_freetype(
1099            &MOSTLY_OFF_CURVE_POINTS,
1100            font_test_data::MOSTLY_OFF_CURVE,
1101        );
1102    }
1103
1104    #[test]
1105    fn starting_off_curve_walk_forwards_like_hbdraw() {
1106        assert_walked_forwards_like_harfbuzz(
1107            &STARTING_OFF_CURVE_POINTS,
1108            font_test_data::STARTING_OFF_CURVE,
1109        );
1110    }
1111
1112    #[test]
1113    fn mostly_off_curve_walk_forwards_like_hbdraw() {
1114        assert_walked_forwards_like_harfbuzz(
1115            &MOSTLY_OFF_CURVE_POINTS,
1116            font_test_data::MOSTLY_OFF_CURVE,
1117        );
1118    }
1119
1120    // A location noted for making FreeType and HarfBuzz results differ
1121    // See https://github.com/googlefonts/sleipnir/pull/15
1122    fn icon_loc_off_default(font: &FontRef) -> Location {
1123        font.axes().location(&[
1124            ("wght", 700.0),
1125            ("opsz", 48.0),
1126            ("GRAD", 200.0),
1127            ("FILL", 1.0),
1128        ])
1129    }
1130
1131    fn pt(x: f32, y: f32) -> Point {
1132        (x as f64, y as f64).into()
1133    }
1134
1135    // String command rounded to two decimal places, suitable for assert comparison
1136    fn svg_commands(elements: &[PathEl]) -> Vec<String> {
1137        elements
1138            .iter()
1139            .map(|e| match e {
1140                PathEl::MoveTo(p) => format!("M{:.2},{:.2}", p.x, p.y),
1141                PathEl::LineTo(p) => format!("L{:.2},{:.2}", p.x, p.y),
1142                PathEl::QuadTo(c0, p) => format!("Q{:.2},{:.2} {:.2},{:.2}", c0.x, c0.y, p.x, p.y),
1143                PathEl::CurveTo(c0, c1, p) => format!(
1144                    "C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
1145                    c0.x, c0.y, c1.x, c1.y, p.x, p.y
1146                ),
1147                PathEl::ClosePath => "Z".to_string(),
1148            })
1149            .collect()
1150    }
1151
1152    // Declared here to avoid a write-fonts dependency that is awkward for google3 at time of writing
1153    #[derive(Default)]
1154    struct BezPen {
1155        path: BezPath,
1156    }
1157
1158    impl OutlinePen for BezPen {
1159        fn move_to(&mut self, x: f32, y: f32) {
1160            self.path.move_to(pt(x, y));
1161        }
1162
1163        fn line_to(&mut self, x: f32, y: f32) {
1164            self.path.line_to(pt(x, y));
1165        }
1166
1167        fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1168            self.path.quad_to(pt(cx0, cy0), pt(x, y));
1169        }
1170
1171        fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1172            self.path.curve_to(pt(cx0, cy0), pt(cx1, cy1), pt(x, y));
1173        }
1174
1175        fn close(&mut self) {
1176            self.path.close_path();
1177        }
1178    }
1179
1180    // We take glyph id here to bypass the need to resolve codepoint:gid and apply substitutions
1181    fn assert_glyph_path_start_with(
1182        font: &FontRef,
1183        gid: GlyphId,
1184        loc: Location,
1185        path_style: PathStyle,
1186        expected_path_start: &[PathEl],
1187    ) {
1188        let glyph = font
1189            .outline_glyphs()
1190            .get(gid)
1191            .unwrap_or_else(|| panic!("No glyph for {gid}"));
1192
1193        let mut pen = BezPen::default();
1194        glyph
1195            .draw(
1196                DrawSettings::unhinted(Size::unscaled(), &loc).with_path_style(path_style),
1197                &mut pen,
1198            )
1199            .unwrap_or_else(|e| panic!("Unable to draw {gid}: {e}"));
1200        let bez = Affine::FLIP_Y * pen.path; // like an icon svg
1201        let actual_path_start = &bez.elements()[..expected_path_start.len()];
1202        // round2 can still leave very small differences from the typed 2-decimal value
1203        // and the diff isn't pleasant so just compare as svg string fragments
1204        assert_eq!(
1205            svg_commands(expected_path_start),
1206            svg_commands(actual_path_start)
1207        );
1208    }
1209
1210    const MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT: GlyphId = GlyphId::new(1);
1211    const MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT: GlyphId = GlyphId::new(2);
1212
1213    #[test]
1214    fn draw_icon_freetype_style_at_default() {
1215        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1216        assert_glyph_path_start_with(
1217            &font,
1218            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1219            Location::default(),
1220            PathStyle::FreeType,
1221            &[
1222                PathEl::MoveTo((160.0, -160.0).into()),
1223                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1224                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1225            ],
1226        );
1227    }
1228
1229    #[test]
1230    fn draw_icon_harfbuzz_style_at_default() {
1231        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1232        assert_glyph_path_start_with(
1233            &font,
1234            MATERIAL_SYMBOL_GID_MAIL_AT_DEFAULT,
1235            Location::default(),
1236            PathStyle::HarfBuzz,
1237            &[
1238                PathEl::MoveTo((160.0, -160.0).into()),
1239                PathEl::QuadTo((127.0, -160.0).into(), (103.5, -183.5).into()),
1240                PathEl::QuadTo((80.0, -207.0).into(), (80.0, -240.0).into()),
1241            ],
1242        );
1243    }
1244
1245    #[test]
1246    fn draw_icon_freetype_style_off_default() {
1247        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1248        assert_glyph_path_start_with(
1249            &font,
1250            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1251            icon_loc_off_default(&font),
1252            PathStyle::FreeType,
1253            &[
1254                PathEl::MoveTo((150.0, -138.0).into()),
1255                PathEl::QuadTo((113.0, -138.0).into(), (86.0, -165.5).into()),
1256                PathEl::QuadTo((59.0, -193.0).into(), (59.0, -229.0).into()),
1257            ],
1258        );
1259    }
1260
1261    #[test]
1262    fn draw_icon_harfbuzz_style_off_default() {
1263        let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
1264        assert_glyph_path_start_with(
1265            &font,
1266            MATERIAL_SYMBOL_GID_MAIL_OFF_DEFAULT,
1267            icon_loc_off_default(&font),
1268            PathStyle::HarfBuzz,
1269            &[
1270                PathEl::MoveTo((150.0, -138.0).into()),
1271                PathEl::QuadTo((113.22, -138.0).into(), (86.11, -165.61).into()),
1272                PathEl::QuadTo((59.0, -193.22).into(), (59.0, -229.0).into()),
1273            ],
1274        );
1275    }
1276
1277    const GLYF_COMPONENT_GID_NON_UNIFORM_SCALE: GlyphId = GlyphId::new(3);
1278    const GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(7);
1279    const GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET: GlyphId = GlyphId::new(8);
1280
1281    #[test]
1282    fn draw_nonuniform_scale_component_freetype() {
1283        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1284        assert_glyph_path_start_with(
1285            &font,
1286            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1287            Location::default(),
1288            PathStyle::FreeType,
1289            &[
1290                PathEl::MoveTo((-138.0, -185.0).into()),
1291                PathEl::LineTo((-32.0, -259.0).into()),
1292                PathEl::LineTo((26.0, -175.0).into()),
1293                PathEl::LineTo((-80.0, -101.0).into()),
1294                PathEl::ClosePath,
1295            ],
1296        );
1297    }
1298
1299    #[test]
1300    fn draw_nonuniform_scale_component_harfbuzz() {
1301        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1302        assert_glyph_path_start_with(
1303            &font,
1304            GLYF_COMPONENT_GID_NON_UNIFORM_SCALE,
1305            Location::default(),
1306            PathStyle::HarfBuzz,
1307            &[
1308                PathEl::MoveTo((-137.8, -184.86).into()),
1309                PathEl::LineTo((-32.15, -258.52).into()),
1310                PathEl::LineTo((25.9, -175.24).into()),
1311                PathEl::LineTo((-79.75, -101.58).into()),
1312                PathEl::ClosePath,
1313            ],
1314        );
1315    }
1316
1317    #[test]
1318    fn draw_scaled_component_offset_freetype() {
1319        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1320        assert_glyph_path_start_with(
1321            &font,
1322            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1323            Location::default(),
1324            PathStyle::FreeType,
1325            &[
1326                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1327                PathEl::MoveTo((715.0, -360.0).into()),
1328            ],
1329        );
1330    }
1331
1332    #[test]
1333    fn draw_no_scaled_component_offset_freetype() {
1334        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1335        assert_glyph_path_start_with(
1336            &font,
1337            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1338            Location::default(),
1339            PathStyle::FreeType,
1340            &[PathEl::MoveTo((705.0, -340.0).into())],
1341        );
1342    }
1343
1344    #[test]
1345    fn draw_scaled_component_offset_harfbuzz() {
1346        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1347        assert_glyph_path_start_with(
1348            &font,
1349            GLYF_COMPONENT_GID_SCALED_COMPONENT_OFFSET,
1350            Location::default(),
1351            PathStyle::HarfBuzz,
1352            &[
1353                // Adds (x-transform magnitude * x-offset, y-transform magnitude * y-offset) to x/y offset
1354                PathEl::MoveTo((714.97, -360.0).into()),
1355            ],
1356        );
1357    }
1358
1359    #[test]
1360    fn draw_no_scaled_component_offset_harfbuzz() {
1361        let font = FontRef::new(font_test_data::GLYF_COMPONENTS).unwrap();
1362        assert_glyph_path_start_with(
1363            &font,
1364            GLYF_COMPONENT_GID_NO_SCALED_COMPONENT_OFFSET,
1365            Location::default(),
1366            PathStyle::HarfBuzz,
1367            &[PathEl::MoveTo((704.97, -340.0).into())],
1368        );
1369    }
1370
1371    #[cfg(feature = "spec_next")]
1372    const CUBIC_GLYPH: GlyphId = GlyphId::new(2);
1373
1374    #[test]
1375    #[cfg(feature = "spec_next")]
1376    fn draw_cubic() {
1377        let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
1378        assert_glyph_path_start_with(
1379            &font,
1380            CUBIC_GLYPH,
1381            Location::default(),
1382            PathStyle::FreeType,
1383            &[
1384                PathEl::MoveTo((278.0, -710.0).into()),
1385                PathEl::LineTo((278.0, -470.0).into()),
1386                PathEl::CurveTo(
1387                    (300.0, -500.0).into(),
1388                    (800.0, -500.0).into(),
1389                    (998.0, -470.0).into(),
1390                ),
1391                PathEl::LineTo((998.0, -710.0).into()),
1392            ],
1393        );
1394    }
1395
1396    /// Case where a font subset caused hinting to fail because execution
1397    /// budget was derived from glyph count.
1398    /// <https://github.com/googlefonts/fontations/issues/936>
1399    #[test]
1400    fn tthint_with_subset() {
1401        let font = FontRef::new(font_test_data::TTHINT_SUBSET).unwrap();
1402        let glyphs = font.outline_glyphs();
1403        let hinting = HintingInstance::new(
1404            &glyphs,
1405            Size::new(16.0),
1406            LocationRef::default(),
1407            HintingOptions::default(),
1408        )
1409        .unwrap();
1410        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1411        // Shouldn't fail in pedantic mode
1412        glyph
1413            .draw(DrawSettings::hinted(&hinting, true), &mut BezPen::default())
1414            .unwrap();
1415    }
1416
1417    #[test]
1418    fn empty_glyph_advance_unhinted() {
1419        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1420        let outlines = font.outline_glyphs();
1421        let coords = [NormalizedCoord::from_f32(0.5)];
1422        let gid = font.charmap().map(' ').unwrap();
1423        let outline = outlines.get(gid).unwrap();
1424        let advance = outline
1425            .draw(
1426                (Size::new(24.0), LocationRef::new(&coords)),
1427                &mut super::pen::NullPen,
1428            )
1429            .unwrap()
1430            .advance_width
1431            .unwrap();
1432        assert_eq!(advance, 10.796875);
1433    }
1434
1435    #[test]
1436    fn empty_glyph_advance_hinted() {
1437        let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
1438        let outlines = font.outline_glyphs();
1439        let coords = [NormalizedCoord::from_f32(0.5)];
1440        let hinter = HintingInstance::new(
1441            &outlines,
1442            Size::new(24.0),
1443            LocationRef::new(&coords),
1444            HintingOptions::default(),
1445        )
1446        .unwrap();
1447        let gid = font.charmap().map(' ').unwrap();
1448        let outline = outlines.get(gid).unwrap();
1449        let advance = outline
1450            .draw(&hinter, &mut super::pen::NullPen)
1451            .unwrap()
1452            .advance_width
1453            .unwrap();
1454        assert_eq!(advance, 11.0);
1455    }
1456
1457    /// Ensure we produce different TrueType outlines based on the
1458    /// fractional_size_hinting flag
1459    #[test]
1460    fn fractional_size_hinting_matters() {
1461        let font = FontRef::from_index(font_test_data::TINOS_SUBSET, 0).unwrap();
1462        let mut outlines = font.outline_glyphs();
1463        let instance = HintingInstance::new(
1464            &outlines,
1465            Size::new(24.8),
1466            LocationRef::default(),
1467            HintingOptions::default(),
1468        )
1469        .unwrap();
1470        let gid = GlyphId::new(2);
1471        let outline_with_fractional = {
1472            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1473                panic!("this is definitely a TrueType font");
1474            };
1475            glyf.fractional_size_hinting = true;
1476            let mut pen = SvgPen::new();
1477            let outline = outlines.get(gid).unwrap();
1478            outline.draw(&instance, &mut pen).unwrap();
1479            pen.to_string()
1480        };
1481        let outline_without_fractional = {
1482            let OutlineCollectionKind::Glyf(glyf) = &mut outlines.kind else {
1483                panic!("this is definitely a TrueType font");
1484            };
1485            glyf.fractional_size_hinting = false;
1486            let mut pen = SvgPen::new();
1487            let outline = outlines.get(gid).unwrap();
1488            outline.draw(&instance, &mut pen).unwrap();
1489            pen.to_string()
1490        };
1491        assert_ne!(outline_with_fractional, outline_without_fractional);
1492    }
1493}