Skip to main content

skrifa/outline/cff/
mod.rs

1//! Support for scaling CFF outlines.
2
3mod hint;
4
5use super::{GlyphHMetrics, OutlinePen};
6use hint::{HintParams, HintState, HintingSink};
7use read_fonts::{
8    ps::{
9        cff::{blend::BlendState, dict, fd_select::FdSelect, index::Index},
10        cs::{self, CommandSink, NopFilterSink, TransformSink},
11        error::Error,
12        transform::{self, FontMatrix, ScaledFontMatrix, Transform},
13    },
14    tables::variations::ItemVariationStore,
15    types::{F2Dot14, Fixed, GlyphId},
16    FontData, FontRead, FontRef, ReadError, TableProvider,
17};
18use std::ops::Range;
19
20/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables.
21///
22/// The skrifa crate provides a higher level interface for this that handles
23/// caching and abstracting over the different outline formats. Consider using
24/// that if detailed control over resources is not required.
25///
26/// # Subfonts
27///
28/// CFF tables can contain multiple logical "subfonts" which determine the
29/// state required for processing some subset of glyphs. This state is
30/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28)
31/// operators to select an appropriate subfont for any given glyph identifier.
32/// This process is exposed on this type with the
33/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont
34/// index for the requested glyph followed by using the
35/// [`subfont`](Self::subfont) method to create an appropriately configured
36/// subfont for that glyph.
37#[derive(Clone)]
38pub(crate) struct Outlines<'a> {
39    pub(crate) font: FontRef<'a>,
40    pub(crate) glyph_metrics: GlyphHMetrics<'a>,
41    offset_data: FontData<'a>,
42    global_subrs: Index<'a>,
43    top_dict: TopDict<'a>,
44    version: u16,
45    units_per_em: u16,
46}
47
48impl<'a> Outlines<'a> {
49    /// Creates a new scaler for the given font.
50    ///
51    /// This will choose an underlying CFF2 or CFF table from the font, in that
52    /// order.
53    pub fn new(font: &FontRef<'a>) -> Option<Self> {
54        let units_per_em = font.head().ok()?.units_per_em();
55        Self::from_cff2(font, units_per_em).or_else(|| Self::from_cff(font, units_per_em))
56    }
57
58    pub fn from_cff(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
59        let cff1 = font.cff().ok()?;
60        let glyph_metrics = GlyphHMetrics::new(font)?;
61        // "The Name INDEX in the CFF data must contain only one entry;
62        // that is, there must be only one font in the CFF FontSet"
63        // So we always pass 0 for Top DICT index when reading from an
64        // OpenType font.
65        // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff>
66        let top_dict_data = cff1.top_dicts().get(0).ok()?;
67        let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false).ok()?;
68        Some(Self {
69            font: font.clone(),
70            glyph_metrics,
71            offset_data: cff1.offset_data(),
72            global_subrs: cff1.global_subrs().into(),
73            top_dict,
74            version: 1,
75            units_per_em,
76        })
77    }
78
79    pub fn from_cff2(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
80        let cff2 = font.cff2().ok()?;
81        let glyph_metrics = GlyphHMetrics::new(font)?;
82        let table_data = cff2.offset_data().as_bytes();
83        let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true).ok()?;
84        Some(Self {
85            font: font.clone(),
86            glyph_metrics,
87            offset_data: cff2.offset_data(),
88            global_subrs: cff2.global_subrs().into(),
89            top_dict,
90            version: 2,
91            units_per_em,
92        })
93    }
94
95    pub fn is_cff2(&self) -> bool {
96        self.version == 2
97    }
98
99    pub fn units_per_em(&self) -> u16 {
100        self.units_per_em
101    }
102
103    /// Returns the number of available glyphs.
104    pub fn glyph_count(&self) -> usize {
105        self.top_dict.charstrings.count() as usize
106    }
107
108    /// Returns the number of available subfonts.
109    pub fn subfont_count(&self) -> u32 {
110        // All CFF fonts have at least one logical subfont.
111        self.top_dict.font_dicts.count().max(1)
112    }
113
114    /// Returns the subfont (or Font DICT) index for the given glyph
115    /// identifier.
116    pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 {
117        // For CFF tables, an FDSelect index will be present for CID-keyed
118        // fonts. Otherwise, the Top DICT will contain an entry for the
119        // "global" Private DICT.
120        // See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=27>
121        //
122        // CFF2 tables always contain a Font DICT and an FDSelect is only
123        // present if the size of the DICT is greater than 1.
124        // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>
125        //
126        // In both cases, we return a subfont index of 0 when FDSelect is missing.
127        self.top_dict
128            .fd_select
129            .as_ref()
130            .and_then(|select| select.font_index(glyph_id))
131            .unwrap_or(0) as u32
132    }
133
134    /// Creates a new subfont for the given index, size, normalized
135    /// variation coordinates and hinting state.
136    ///
137    /// The index of a subfont for a particular glyph can be retrieved with
138    /// the [`subfont_index`](Self::subfont_index) method.
139    pub fn subfont(
140        &self,
141        index: u32,
142        size: Option<f32>,
143        coords: &[F2Dot14],
144    ) -> Result<Subfont, Error> {
145        let font_dict = self.parse_font_dict(index)?;
146        let blend_state = self
147            .top_dict
148            .var_store
149            .clone()
150            .map(|store| BlendState::new(store, coords, 0))
151            .transpose()?;
152        let private_dict =
153            PrivateDict::new(self.offset_data, font_dict.private_dict_range, blend_state)?;
154        let upem = self.units_per_em as i32;
155        let mut scale = match size {
156            Some(ppem) if upem > 0 => {
157                // Note: we do an intermediate scale to 26.6 to ensure we
158                // match FreeType
159                Some(Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem))
160            }
161            _ => None,
162        };
163        let scale_requested = size.is_some();
164        // Compute our font matrix and adjusted UPEM
165        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L746>
166        let font_matrix = if let Some(top_matrix) = self.top_dict.font_matrix {
167            // We have a top dict matrix. Now check for a font dict matrix.
168            if let Some(sub_matrix) = font_dict.font_matrix {
169                let scaling = if top_matrix.scale > 1 && sub_matrix.scale > 1 {
170                    top_matrix.scale.min(sub_matrix.scale)
171                } else {
172                    1
173                };
174                // Concatenate and scale
175                let matrix =
176                    transform::combine_scaled(&top_matrix.matrix, &sub_matrix.matrix, scaling);
177                let upem = Fixed::from_bits(sub_matrix.scale).mul_div(
178                    Fixed::from_bits(top_matrix.scale),
179                    Fixed::from_bits(scaling),
180                );
181                // Then normalize
182                Some(
183                    ScaledFontMatrix {
184                        matrix,
185                        scale: upem.to_bits(),
186                    }
187                    .normalize(),
188                )
189            } else {
190                // Top matrix was already normalized on load
191                Some(top_matrix)
192            }
193        } else {
194            // Just normalize if we have a subfont matrix
195            font_dict.font_matrix.map(|matrix| matrix.normalize())
196        };
197        // Now adjust our scale factor if necessary
198        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffgload.c#L450>
199        let mut font_matrix = if let Some(matrix) = font_matrix {
200            // If the scaling factor from our matrix does not equal the nominal
201            // UPEM of the font then adjust the scale.
202            if matrix.scale != upem {
203                // In this case, we need to force a scale for "unscaled"
204                // requests in order to apply the adjusted UPEM from the
205                // font matrix.
206                let original_scale = scale.unwrap_or(Fixed::from_i32(64));
207                scale = Some(
208                    original_scale.mul_div(Fixed::from_bits(upem), Fixed::from_bits(matrix.scale)),
209                );
210            }
211            Some(matrix.matrix)
212        } else {
213            None
214        };
215        if font_matrix == Some(FontMatrix::IDENTITY) {
216            // Let's not waste time applying an identity matrix. This occurs
217            // fairly often after normalization.
218            font_matrix = None;
219        }
220        let hint_scale = scale_for_hinting(scale);
221        let hint_state = HintState::new(&private_dict.hint_params, hint_scale);
222        Ok(Subfont {
223            is_cff2: self.is_cff2(),
224            scale,
225            scale_requested,
226            subrs_offset: private_dict.subrs_offset,
227            hint_state,
228            store_index: private_dict.store_index,
229            font_matrix,
230            default_width: private_dict.default_width,
231            nominal_width: private_dict.nominal_width,
232        })
233    }
234
235    /// Loads and scales an outline for the given subfont instance, glyph
236    /// identifier and normalized variation coordinates.
237    ///
238    /// Before calling this method, use [`subfont_index`](Self::subfont_index)
239    /// to retrieve the subfont index for the desired glyph and then
240    /// [`subfont`](Self::subfont) to create an instance of the subfont for a
241    /// particular size and location in variation space.
242    /// Creating subfont instances is not free, so this process is exposed in
243    /// discrete steps to allow for caching.
244    ///
245    /// The result is emitted to the specified pen.
246    pub fn draw(
247        &self,
248        subfont: &Subfont,
249        glyph_id: GlyphId,
250        coords: &[F2Dot14],
251        hint: bool,
252        pen: &mut impl OutlinePen,
253    ) -> Result<Option<f32>, Error> {
254        let cff_data = self.offset_data.as_bytes();
255        let charstrings = self.top_dict.charstrings.clone();
256        let charstring_data = charstrings.get(glyph_id.to_u32() as usize)?;
257        let subrs = subfont.subrs(self)?;
258        let blend_state = subfont.blend_state(self, coords)?;
259        let cs_eval = CharstringEvaluator {
260            cff_data,
261            charstrings,
262            global_subrs: self.global_subrs.clone(),
263            subrs,
264            blend_state,
265            charstring_data,
266        };
267        // Only apply hinting if we have a scale
268        let apply_hinting = hint && subfont.scale_requested;
269        let mut pen_sink = PenSink::new(pen);
270        let mut simplifying_adapter = NopFilterSink::new(&mut pen_sink);
271        let mut transform = Transform {
272            matrix: FontMatrix::IDENTITY,
273            scale: subfont.scale,
274        };
275        let maybe_width = if let Some(matrix) = subfont.font_matrix {
276            transform.matrix = matrix;
277            if apply_hinting {
278                let mut transform_sink =
279                    HintedTransformingSink::new(&mut simplifying_adapter, matrix);
280                let mut hinting_adapter =
281                    HintingSink::new(&subfont.hint_state, &mut transform_sink);
282                cs_eval.evaluate(&mut hinting_adapter)
283            } else {
284                let mut transform_sink = TransformSink::from_matrix_scale(
285                    &mut simplifying_adapter,
286                    matrix,
287                    subfont.scale,
288                );
289                cs_eval.evaluate(&mut transform_sink)
290            }
291        } else if apply_hinting {
292            let mut hinting_adapter =
293                HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
294            cs_eval.evaluate(&mut hinting_adapter)
295        } else {
296            let mut scaling_adapter = TransformSink::from_matrix_scale(
297                &mut simplifying_adapter,
298                FontMatrix::IDENTITY,
299                subfont.scale,
300            );
301            cs_eval.evaluate(&mut scaling_adapter)
302        }?;
303        Ok(maybe_width
304            // If charstring eval returned a width, add the nominal width
305            // from the Private DICT
306            .map(|w| w + subfont.nominal_width)
307            // Otherwise, try the default width from the Private DICT
308            .or(subfont.default_width)
309            // If all else fails, fall back to hmtx/HVAR tables
310            .or_else(|| {
311                Some(Fixed::from_i32(
312                    self.glyph_metrics.advance_width(glyph_id, coords),
313                ))
314            })
315            .map(|w| {
316                let w = transform.transform_h_metric(w);
317                if hint {
318                    w.round().to_f32()
319                } else {
320                    w.to_f32()
321                }
322            })
323            // Some fonts can generate weird negative advance widths.
324            // FreeType casts these to unsigned values resulting in
325            // large positive advances. Since this advance is optional,
326            // we can just filter these out and let the client deal
327            // with it, falling back to linear metrics.
328            .filter(|w| *w >= 0.0))
329    }
330
331    fn parse_font_dict(&self, subfont_index: u32) -> Result<FontDict, Error> {
332        if self.top_dict.font_dicts.count() != 0 {
333            // If we have a font dict array, extract the private dict range
334            // from the font dict at the given index.
335            let font_dict_data = self.top_dict.font_dicts.get(subfont_index as usize)?;
336            FontDict::new(font_dict_data)
337        } else {
338            // Use the private dict range from the top dict.
339            // Note: "A Private DICT is required but may be specified as having
340            // a length of 0 if there are no non-default values to be stored."
341            // <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=25>
342            let range = self.top_dict.private_dict_range.clone();
343            Ok(FontDict {
344                private_dict_range: range.start as usize..range.end as usize,
345                font_matrix: None,
346            })
347        }
348    }
349}
350
351/// When hinting, use a modified scale factor.
352///
353/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279>
354fn scale_for_hinting(scale: Option<Fixed>) -> Fixed {
355    Fixed::from_bits((scale.unwrap_or(Fixed::ONE).to_bits().saturating_add(32)) / 64)
356}
357
358struct CharstringEvaluator<'a> {
359    cff_data: &'a [u8],
360    charstrings: Index<'a>,
361    global_subrs: Index<'a>,
362    subrs: Option<Index<'a>>,
363    blend_state: Option<BlendState<'a>>,
364    charstring_data: &'a [u8],
365}
366
367impl CharstringEvaluator<'_> {
368    fn evaluate(self, sink: &mut impl CommandSink) -> Result<Option<Fixed>, Error> {
369        let subrs = self.subrs.unwrap_or_default();
370        let ctx = (self.cff_data, &self.charstrings, &self.global_subrs, &subrs);
371        cs::evaluate(&ctx, self.blend_state, self.charstring_data, sink)
372    }
373}
374
375/// Specifies local subroutines and hinting parameters for some subset of
376/// glyphs in a CFF or CFF2 table.
377///
378/// This type is designed to be cacheable to avoid re-evaluating the private
379/// dict every time a charstring is processed.
380///
381/// For variable fonts, this is dependent on a location in variation space.
382#[derive(Clone)]
383pub(crate) struct Subfont {
384    is_cff2: bool,
385    scale: Option<Fixed>,
386    /// When we have a font matrix, we might force a scale even if the user
387    /// requested unscaled output. In this case, we shouldn't apply hinting
388    /// and this keeps track of that.
389    scale_requested: bool,
390    subrs_offset: Option<usize>,
391    pub(crate) hint_state: HintState,
392    store_index: u16,
393    font_matrix: Option<FontMatrix>,
394    default_width: Option<Fixed>,
395    nominal_width: Fixed,
396}
397
398impl Subfont {
399    /// Returns the local subroutine index.
400    pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error> {
401        if let Some(subrs_offset) = self.subrs_offset {
402            let offset_data = scaler.offset_data.as_bytes();
403            let index_data = offset_data.get(subrs_offset..).unwrap_or_default();
404            Ok(Some(Index::new(index_data, self.is_cff2)?))
405        } else {
406            Ok(None)
407        }
408    }
409
410    /// Creates a new blend state for the given normalized variation
411    /// coordinates.
412    pub fn blend_state<'a>(
413        &self,
414        scaler: &Outlines<'a>,
415        coords: &'a [F2Dot14],
416    ) -> Result<Option<BlendState<'a>>, Error> {
417        if let Some(var_store) = scaler.top_dict.var_store.clone() {
418            Ok(Some(BlendState::new(var_store, coords, self.store_index)?))
419        } else {
420            Ok(None)
421        }
422    }
423}
424
425/// Entries that we parse from the Private DICT to support charstring
426/// evaluation.
427#[derive(Default)]
428struct PrivateDict {
429    hint_params: HintParams,
430    subrs_offset: Option<usize>,
431    store_index: u16,
432    default_width: Option<Fixed>,
433    nominal_width: Fixed,
434}
435
436impl PrivateDict {
437    fn new(
438        data: FontData,
439        range: Range<usize>,
440        blend_state: Option<BlendState<'_>>,
441    ) -> Result<Self, Error> {
442        let private_dict_data = data.read_array(range.clone())?;
443        let mut dict = Self::default();
444        for entry in dict::entries(private_dict_data, blend_state) {
445            use dict::Entry::*;
446            match entry? {
447                // FreeType truncates the width values to int on read
448                DefaultWidthX(width) => dict.default_width = Some(width.floor()),
449                NominalWidthX(width) => dict.nominal_width = width.floor(),
450                BlueValues(values) => dict.hint_params.blues = values,
451                FamilyBlues(values) => dict.hint_params.family_blues = values,
452                OtherBlues(values) => dict.hint_params.other_blues = values,
453                FamilyOtherBlues(values) => dict.hint_params.family_other_blues = values,
454                BlueScale(value) => dict.hint_params.blue_scale = value,
455                BlueShift(value) => dict.hint_params.blue_shift = value,
456                BlueFuzz(value) => dict.hint_params.blue_fuzz = value,
457                LanguageGroup(group) => dict.hint_params.language_group = group,
458                // Subrs offset is relative to the private DICT
459                SubrsOffset(offset) => {
460                    dict.subrs_offset = Some(
461                        range
462                            .start
463                            .checked_add(offset)
464                            .ok_or(ReadError::OutOfBounds)?,
465                    )
466                }
467                VariationStoreIndex(index) => dict.store_index = index,
468                _ => {}
469            }
470        }
471        Ok(dict)
472    }
473}
474
475/// Entries that we parse from a Font DICT.
476#[derive(Clone, Default)]
477struct FontDict {
478    private_dict_range: Range<usize>,
479    font_matrix: Option<ScaledFontMatrix>,
480}
481
482impl FontDict {
483    fn new(font_dict_data: &[u8]) -> Result<Self, Error> {
484        let mut range = None;
485        let mut font_matrix = None;
486        for entry in dict::entries(font_dict_data, None) {
487            match entry? {
488                dict::Entry::PrivateDictRange(r) => {
489                    range = Some(r);
490                }
491                // We store this matrix unnormalized since FreeType
492                // concatenates this with the top dict matrix (if present)
493                // before normalizing
494                dict::Entry::FontMatrix(matrix) => font_matrix = Some(matrix),
495                _ => {}
496            }
497        }
498        Ok(Self {
499            private_dict_range: range.ok_or(Error::MissingPrivateDict)?,
500            font_matrix,
501        })
502    }
503}
504
505/// Entries that we parse from the Top DICT that are required to support
506/// charstring evaluation.
507#[derive(Clone, Default)]
508struct TopDict<'a> {
509    charstrings: Index<'a>,
510    font_dicts: Index<'a>,
511    fd_select: Option<FdSelect<'a>>,
512    private_dict_range: Range<u32>,
513    font_matrix: Option<ScaledFontMatrix>,
514    var_store: Option<ItemVariationStore<'a>>,
515}
516
517impl<'a> TopDict<'a> {
518    fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> {
519        let mut items = TopDict::default();
520        for entry in dict::entries(top_dict_data, None) {
521            match entry? {
522                dict::Entry::CharstringsOffset(offset) => {
523                    items.charstrings =
524                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
525                }
526                dict::Entry::FdArrayOffset(offset) => {
527                    items.font_dicts =
528                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
529                }
530                dict::Entry::FdSelectOffset(offset) => {
531                    items.fd_select = Some(FdSelect::read(FontData::new(
532                        table_data.get(offset..).unwrap_or_default(),
533                    ))?);
534                }
535                dict::Entry::PrivateDictRange(range) => {
536                    items.private_dict_range = range.start as u32..range.end as u32;
537                }
538                dict::Entry::FontMatrix(matrix) => {
539                    // Store this matrix normalized since FT always applies normalization
540                    items.font_matrix = Some(matrix.normalize());
541                }
542                dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
543                    // IVS is preceded by a 2 byte length, but ensure that
544                    // we don't overflow
545                    // See <https://github.com/googlefonts/fontations/issues/1223>
546                    let offset = offset.checked_add(2).ok_or(ReadError::OutOfBounds)?;
547                    items.var_store = Some(ItemVariationStore::read(FontData::new(
548                        table_data.get(offset..).unwrap_or_default(),
549                    ))?);
550                }
551                _ => {}
552            }
553        }
554        Ok(items)
555    }
556}
557
558/// Command sink that sends the results of charstring evaluation to
559/// an [OutlinePen].
560struct PenSink<'a, P>(&'a mut P);
561
562impl<'a, P> PenSink<'a, P> {
563    fn new(pen: &'a mut P) -> Self {
564        Self(pen)
565    }
566}
567
568impl<P> CommandSink for PenSink<'_, P>
569where
570    P: OutlinePen,
571{
572    fn move_to(&mut self, x: Fixed, y: Fixed) {
573        self.0.move_to(x.to_f32(), y.to_f32());
574    }
575
576    fn line_to(&mut self, x: Fixed, y: Fixed) {
577        self.0.line_to(x.to_f32(), y.to_f32());
578    }
579
580    fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
581        self.0.curve_to(
582            cx0.to_f32(),
583            cy0.to_f32(),
584            cx1.to_f32(),
585            cy1.to_f32(),
586            x.to_f32(),
587            y.to_f32(),
588        );
589    }
590
591    fn close(&mut self) {
592        self.0.close();
593    }
594}
595
596/// Command sink adapter that applies a transform to hinted coordinates.
597struct HintedTransformingSink<'a, S> {
598    inner: &'a mut S,
599    matrix: FontMatrix,
600}
601
602impl<'a, S> HintedTransformingSink<'a, S> {
603    fn new(sink: &'a mut S, matrix: FontMatrix) -> Self {
604        Self {
605            inner: sink,
606            matrix,
607        }
608    }
609
610    fn transform(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
611        // FreeType applies the transform to 26.6 values but we maintain
612        // values in 16.16 so convert, transform and then convert back
613        let (x, y) = self.matrix.transform(
614            Fixed::from_bits(x.to_bits() >> 10),
615            Fixed::from_bits(y.to_bits() >> 10),
616        );
617        (
618            Fixed::from_bits(x.to_bits() << 10),
619            Fixed::from_bits(y.to_bits() << 10),
620        )
621    }
622}
623
624impl<S: CommandSink> CommandSink for HintedTransformingSink<'_, S> {
625    fn hstem(&mut self, y: Fixed, dy: Fixed) {
626        self.inner.hstem(y, dy);
627    }
628
629    fn vstem(&mut self, x: Fixed, dx: Fixed) {
630        self.inner.vstem(x, dx);
631    }
632
633    fn hint_mask(&mut self, mask: &[u8]) {
634        self.inner.hint_mask(mask);
635    }
636
637    fn counter_mask(&mut self, mask: &[u8]) {
638        self.inner.counter_mask(mask);
639    }
640
641    fn clear_hints(&mut self) {
642        self.inner.clear_hints();
643    }
644
645    fn move_to(&mut self, x: Fixed, y: Fixed) {
646        let (x, y) = self.transform(x, y);
647        self.inner.move_to(x, y);
648    }
649
650    fn line_to(&mut self, x: Fixed, y: Fixed) {
651        let (x, y) = self.transform(x, y);
652        self.inner.line_to(x, y);
653    }
654
655    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
656        let (cx1, cy1) = self.transform(cx1, cy1);
657        let (cx2, cy2) = self.transform(cx2, cy2);
658        let (x, y) = self.transform(x, y);
659        self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
660    }
661
662    fn close(&mut self) {
663        self.inner.close();
664    }
665
666    fn finish(&mut self) {
667        self.inner.finish();
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::{super::pen::SvgPen, *};
674    use crate::{
675        outline::{HintingInstance, HintingOptions},
676        prelude::{LocationRef, Size},
677        MetadataProvider,
678    };
679    use font_test_data::bebuffer::BeBuffer;
680    use raw::tables::cff2::Cff2;
681    use read_fonts::ps::hinting::Blues;
682    use read_fonts::FontRef;
683
684    #[test]
685    fn read_cff_static() {
686        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
687        let cff = Outlines::new(&font).unwrap();
688        assert!(!cff.is_cff2());
689        assert!(cff.top_dict.var_store.is_none());
690        assert!(cff.top_dict.font_dicts.count() == 0);
691        assert!(!cff.top_dict.private_dict_range.is_empty());
692        assert!(cff.top_dict.fd_select.is_none());
693        assert_eq!(cff.subfont_count(), 1);
694        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
695        assert_eq!(cff.global_subrs.count(), 17);
696    }
697
698    #[test]
699    fn read_cff2_static() {
700        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
701        let cff = Outlines::new(&font).unwrap();
702        assert!(cff.is_cff2());
703        assert!(cff.top_dict.var_store.is_some());
704        assert!(cff.top_dict.font_dicts.count() != 0);
705        assert!(cff.top_dict.private_dict_range.is_empty());
706        assert!(cff.top_dict.fd_select.is_none());
707        assert_eq!(cff.subfont_count(), 1);
708        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
709        assert_eq!(cff.global_subrs.count(), 0);
710    }
711
712    #[test]
713    fn read_example_cff2_table() {
714        let cff2 = Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap();
715        let top_dict =
716            TopDict::new(cff2.offset_data().as_bytes(), cff2.top_dict_data(), true).unwrap();
717        assert!(top_dict.var_store.is_some());
718        assert!(top_dict.font_dicts.count() != 0);
719        assert!(top_dict.private_dict_range.is_empty());
720        assert!(top_dict.fd_select.is_none());
721        assert_eq!(cff2.global_subrs().count(), 0);
722    }
723
724    #[test]
725    fn cff2_variable_outlines_match_freetype() {
726        compare_glyphs(
727            font_test_data::CANTARELL_VF_TRIMMED,
728            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
729        );
730    }
731
732    #[test]
733    fn cff_static_outlines_match_freetype() {
734        compare_glyphs(
735            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
736            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
737        );
738    }
739
740    #[test]
741    fn unhinted_ends_with_close() {
742        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
743        let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
744        let mut svg = SvgPen::default();
745        glyph.draw(Size::unscaled(), &mut svg).unwrap();
746        assert!(svg.to_string().ends_with('Z'));
747    }
748
749    #[test]
750    fn hinted_ends_with_close() {
751        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
752        let glyphs = font.outline_glyphs();
753        let hinter = HintingInstance::new(
754            &glyphs,
755            Size::unscaled(),
756            LocationRef::default(),
757            HintingOptions::default(),
758        )
759        .unwrap();
760        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
761        let mut svg = SvgPen::default();
762        glyph.draw(&hinter, &mut svg).unwrap();
763        assert!(svg.to_string().ends_with('Z'));
764    }
765
766    /// Ensure we don't reject an empty Private DICT
767    #[test]
768    fn empty_private_dict() {
769        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
770        let outlines = super::Outlines::new(&font).unwrap();
771        assert!(outlines.top_dict.private_dict_range.is_empty());
772        assert!(outlines
773            .parse_font_dict(0)
774            .unwrap()
775            .private_dict_range
776            .is_empty());
777    }
778
779    /// Fuzzer caught add with overflow when computing subrs offset.
780    /// See <https://issues.oss-fuzz.com/issues/377965575>
781    #[test]
782    fn subrs_offset_overflow() {
783        // A private DICT with an overflowing subrs offset
784        let private_dict = BeBuffer::new()
785            .push(0u32) // pad so that range doesn't start with 0 and we overflow
786            .push(29u8) // integer operator
787            .push(-1i32) // integer value
788            .push(19u8) // subrs offset operator
789            .to_vec();
790        // Just don't panic with overflow
791        assert!(
792            PrivateDict::new(FontData::new(&private_dict), 4..private_dict.len(), None).is_err()
793        );
794    }
795
796    // Fuzzer caught add with overflow when computing offset to
797    // var store.
798    // See <https://issues.oss-fuzz.com/issues/377574377>
799    #[test]
800    fn top_dict_ivs_offset_overflow() {
801        // A top DICT with a var store offset of -1 which will cause an
802        // overflow
803        let top_dict = BeBuffer::new()
804            .push(29u8) // integer operator
805            .push(-1i32) // integer value
806            .push(24u8) // var store offset operator
807            .to_vec();
808        // Just don't panic with overflow
809        assert!(TopDict::new(&[], &top_dict, true).is_err());
810    }
811
812    /// Actually apply a scale when the computed scale factor is
813    /// equal to Fixed::ONE.
814    ///
815    /// Specifically, when upem = 512 and ppem = 8, this results in
816    /// a scale factor of 65536 which was being interpreted as an
817    /// unscaled draw request.
818    #[test]
819    fn proper_scaling_when_factor_equals_fixed_one() {
820        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
821        assert_eq!(font.head().unwrap().units_per_em(), 512);
822        let glyphs = font.outline_glyphs();
823        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
824        let mut svg = SvgPen::with_precision(6);
825        glyph
826            .draw((Size::new(8.0), LocationRef::default()), &mut svg)
827            .unwrap();
828        // This was initially producing unscaled values like M405.000...
829        assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
830    }
831
832    /// For the given font data and extracted outlines, parse the extracted
833    /// outline data into a set of expected values and compare these with the
834    /// results generated by the scaler.
835    ///
836    /// This will compare all outlines at various sizes and (for variable
837    /// fonts), locations in variation space.
838    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
839        use super::super::testing;
840        let font = FontRef::new(font_data).unwrap();
841        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
842        let outlines = super::Outlines::new(&font).unwrap();
843        let mut path = testing::Path::default();
844        for expected_outline in &expected_outlines {
845            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
846                continue;
847            }
848            let size = (expected_outline.size != 0.0).then_some(expected_outline.size);
849            path.elements.clear();
850            let subfont = outlines
851                .subfont(
852                    outlines.subfont_index(expected_outline.glyph_id),
853                    size,
854                    &expected_outline.coords,
855                )
856                .unwrap();
857            outlines
858                .draw(
859                    &subfont,
860                    expected_outline.glyph_id,
861                    &expected_outline.coords,
862                    false,
863                    &mut path,
864                )
865                .unwrap();
866            if path.elements != expected_outline.path {
867                panic!(
868                    "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
869                    expected_outline.glyph_id,
870                    expected_outline.size,
871                    expected_outline.coords,
872                    &path.elements,
873                    &expected_outline.path
874                );
875            }
876        }
877    }
878
879    // We were overwriting family_other_blues with family_blues.
880    #[test]
881    fn capture_family_other_blues() {
882        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
883        let store =
884            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
885        let coords = &[F2Dot14::from_f32(0.0)];
886        let blend_state = BlendState::new(store, coords, 0).unwrap();
887        let private_dict = PrivateDict::new(
888            FontData::new(private_dict_data),
889            0..private_dict_data.len(),
890            Some(blend_state),
891        )
892        .unwrap();
893        assert_eq!(
894            private_dict.hint_params.family_other_blues,
895            Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
896        )
897    }
898
899    #[test]
900    fn implied_seac() {
901        let font = FontRef::new(font_test_data::CHARSTRING_PATH_OPS).unwrap();
902        let glyphs = font.outline_glyphs();
903        let gid = GlyphId::new(3);
904        assert_eq!(font.glyph_names().get(gid).unwrap(), "Scaron");
905        let glyph = glyphs.get(gid).unwrap();
906        let mut pen = SvgPen::new();
907        glyph
908            .draw((Size::unscaled(), LocationRef::default()), &mut pen)
909            .unwrap();
910        // This triggers the seac behavior in the endchar operator which
911        // loads an accent character followed by a base character. Ensure
912        // that we have a path to represent each by checking for two closepath
913        // commands.
914        assert_eq!(pen.to_string().chars().filter(|ch| *ch == 'Z').count(), 2);
915    }
916
917    #[test]
918    fn implied_seac_clears_hints() {
919        let font = FontRef::new(font_test_data::CHARSTRING_PATH_OPS).unwrap();
920        let outlines = Outlines::from_cff(&font, 1000).unwrap();
921        let subfont = outlines.subfont(0, Some(16.0), &[]).unwrap();
922        let cff_data = outlines.offset_data.as_bytes();
923        let charstrings = outlines.top_dict.charstrings.clone();
924        let charstring_data = charstrings.get(3).unwrap();
925        let subrs = subfont.subrs(&outlines).unwrap();
926        let blend_state = None;
927        let cs_eval = CharstringEvaluator {
928            cff_data,
929            charstrings,
930            global_subrs: outlines.global_subrs.clone(),
931            subrs,
932            blend_state,
933            charstring_data,
934        };
935        struct ClearHintsCountingSink(u32);
936        impl CommandSink for ClearHintsCountingSink {
937            fn move_to(&mut self, _: Fixed, _: Fixed) {}
938            fn line_to(&mut self, _: Fixed, _: Fixed) {}
939            fn curve_to(&mut self, _: Fixed, _: Fixed, _: Fixed, _: Fixed, _: Fixed, _: Fixed) {}
940            fn close(&mut self) {}
941            fn clear_hints(&mut self) {
942                self.0 += 1;
943            }
944        }
945        let mut sink = ClearHintsCountingSink(0);
946        cs_eval.evaluate(&mut sink).unwrap();
947        // We should have cleared hints twice.. once for the base and once
948        // for the accent
949        assert_eq!(sink.0, 2);
950    }
951
952    const TRANSFORM: FontMatrix = FontMatrix::from_elements([
953        Fixed::ONE,
954        Fixed::ZERO,
955        // 0.167007446289062
956        Fixed::from_bits(10945),
957        Fixed::ONE,
958        Fixed::ZERO,
959        Fixed::ZERO,
960    ]);
961
962    #[test]
963    fn hinted_transform_sink() {
964        // A few points taken from the test font in <https://github.com/googlefonts/fontations/issues/1581>
965        // Inputs and expected values extracted from FreeType
966        let input = [(383i32, 117i32), (450, 20), (555, -34), (683, -34)]
967            .map(|(x, y)| (Fixed::from_bits(x << 10), Fixed::from_bits(y << 10)));
968        let expected = [(403, 117i32), (453, 20), (549, -34), (677, -34)]
969            .map(|(x, y)| (Fixed::from_bits(x << 10), Fixed::from_bits(y << 10)));
970        let mut dummy = ();
971        let sink = HintedTransformingSink::new(&mut dummy, TRANSFORM);
972        let transformed = input.map(|(x, y)| sink.transform(x, y));
973        assert_eq!(transformed, expected);
974    }
975
976    /// See <https://github.com/googlefonts/fontations/issues/1638>
977    #[test]
978    fn nested_font_matrices() {
979        // Expected values extracted from FreeType debugging session
980        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
981        let outlines = Outlines::from_cff(&font, 512).unwrap();
982        // Check the normalized top dict matrix
983        let top_matrix = outlines.top_dict.font_matrix.unwrap();
984        let expected_top_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
985        assert_eq!(top_matrix.matrix.elements(), expected_top_matrix);
986        assert_eq!(top_matrix.scale, 512);
987        // Check the unnormalized font dict matrix
988        let sub_matrix = outlines.parse_font_dict(0).unwrap().font_matrix.unwrap();
989        let expected_sub_matrix = [327680, 0, 0, 327680, 0, 0].map(Fixed::from_bits);
990        assert_eq!(sub_matrix.matrix.elements(), expected_sub_matrix);
991        assert_eq!(sub_matrix.scale, 10);
992        // Check the normalized combined matrix
993        let subfont = outlines.subfont(0, Some(24.0), &[]).unwrap();
994        let expected_combined_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
995        assert_eq!(
996            subfont.font_matrix.unwrap().elements(),
997            expected_combined_matrix
998        );
999        // Check the final scale
1000        assert_eq!(subfont.scale.unwrap().to_bits(), 98304);
1001    }
1002
1003    /// OSS fuzz caught add with overflow for hint scale computation.
1004    /// See <https://oss-fuzz.com/testcase-detail/6498790355042304>
1005    /// and <https://issues.oss-fuzz.com/issues/444024349>
1006    #[test]
1007    fn subfont_hint_scale_overflow() {
1008        // Just don't panic with overflow
1009        let _ = scale_for_hinting(Some(Fixed::from_bits(i32::MAX)));
1010    }
1011}