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 raw::{tables::postscript::dict::normalize_font_matrix, FontRef};
8use read_fonts::{
9    tables::{
10        postscript::{
11            charstring::{self, CommandSink},
12            dict, BlendState, Error, FdSelect, Index,
13        },
14        variations::ItemVariationStore,
15    },
16    types::{F2Dot14, Fixed, GlyphId},
17    FontData, FontRead, ReadError, TableProvider,
18};
19use std::ops::Range;
20
21/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables.
22///
23/// The skrifa crate provides a higher level interface for this that handles
24/// caching and abstracting over the different outline formats. Consider using
25/// that if detailed control over resources is not required.
26///
27/// # Subfonts
28///
29/// CFF tables can contain multiple logical "subfonts" which determine the
30/// state required for processing some subset of glyphs. This state is
31/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28)
32/// operators to select an appropriate subfont for any given glyph identifier.
33/// This process is exposed on this type with the
34/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont
35/// index for the requested glyph followed by using the
36/// [`subfont`](Self::subfont) method to create an appropriately configured
37/// subfont for that glyph.
38#[derive(Clone)]
39pub(crate) struct Outlines<'a> {
40    pub(crate) font: FontRef<'a>,
41    pub(crate) glyph_metrics: GlyphHMetrics<'a>,
42    offset_data: FontData<'a>,
43    global_subrs: Index<'a>,
44    top_dict: TopDict<'a>,
45    version: u16,
46    units_per_em: u16,
47}
48
49impl<'a> Outlines<'a> {
50    /// Creates a new scaler for the given font.
51    ///
52    /// This will choose an underlying CFF2 or CFF table from the font, in that
53    /// order.
54    pub fn new(font: &FontRef<'a>) -> Option<Self> {
55        let units_per_em = font.head().ok()?.units_per_em();
56        Self::from_cff2(font, units_per_em).or_else(|| Self::from_cff(font, units_per_em))
57    }
58
59    pub fn from_cff(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
60        let cff1 = font.cff().ok()?;
61        let glyph_metrics = GlyphHMetrics::new(font)?;
62        // "The Name INDEX in the CFF data must contain only one entry;
63        // that is, there must be only one font in the CFF FontSet"
64        // So we always pass 0 for Top DICT index when reading from an
65        // OpenType font.
66        // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff>
67        let top_dict_data = cff1.top_dicts().get(0).ok()?;
68        let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false).ok()?;
69        Some(Self {
70            font: font.clone(),
71            glyph_metrics,
72            offset_data: cff1.offset_data(),
73            global_subrs: cff1.global_subrs().into(),
74            top_dict,
75            version: 1,
76            units_per_em,
77        })
78    }
79
80    pub fn from_cff2(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
81        let cff2 = font.cff2().ok()?;
82        let glyph_metrics = GlyphHMetrics::new(font)?;
83        let table_data = cff2.offset_data().as_bytes();
84        let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true).ok()?;
85        Some(Self {
86            font: font.clone(),
87            glyph_metrics,
88            offset_data: cff2.offset_data(),
89            global_subrs: cff2.global_subrs().into(),
90            top_dict,
91            version: 2,
92            units_per_em,
93        })
94    }
95
96    pub fn is_cff2(&self) -> bool {
97        self.version == 2
98    }
99
100    pub fn units_per_em(&self) -> u16 {
101        self.units_per_em
102    }
103
104    /// Returns the number of available glyphs.
105    pub fn glyph_count(&self) -> usize {
106        self.top_dict.charstrings.count() as usize
107    }
108
109    /// Returns the number of available subfonts.
110    pub fn subfont_count(&self) -> u32 {
111        // All CFF fonts have at least one logical subfont.
112        self.top_dict.font_dicts.count().max(1)
113    }
114
115    /// Returns the subfont (or Font DICT) index for the given glyph
116    /// identifier.
117    pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 {
118        // For CFF tables, an FDSelect index will be present for CID-keyed
119        // fonts. Otherwise, the Top DICT will contain an entry for the
120        // "global" Private DICT.
121        // See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=27>
122        //
123        // CFF2 tables always contain a Font DICT and an FDSelect is only
124        // present if the size of the DICT is greater than 1.
125        // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>
126        //
127        // In both cases, we return a subfont index of 0 when FDSelect is missing.
128        self.top_dict
129            .fd_select
130            .as_ref()
131            .and_then(|select| select.font_index(glyph_id))
132            .unwrap_or(0) as u32
133    }
134
135    /// Creates a new subfont for the given index, size, normalized
136    /// variation coordinates and hinting state.
137    ///
138    /// The index of a subfont for a particular glyph can be retrieved with
139    /// the [`subfont_index`](Self::subfont_index) method.
140    pub fn subfont(
141        &self,
142        index: u32,
143        size: Option<f32>,
144        coords: &[F2Dot14],
145    ) -> Result<Subfont, Error> {
146        let font_dict = self.parse_font_dict(index)?;
147        let blend_state = self
148            .top_dict
149            .var_store
150            .clone()
151            .map(|store| BlendState::new(store, coords, 0))
152            .transpose()?;
153        let private_dict =
154            PrivateDict::new(self.offset_data, font_dict.private_dict_range, blend_state)?;
155        let upem = self.units_per_em as i32;
156        let mut scale = match size {
157            Some(ppem) if upem > 0 => {
158                // Note: we do an intermediate scale to 26.6 to ensure we
159                // match FreeType
160                Some(Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem))
161            }
162            _ => None,
163        };
164        let scale_requested = size.is_some();
165        // Compute our font matrix and adjusted UPEM
166        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L746>
167        let font_matrix = if let Some((top_matrix, top_upem)) = self.top_dict.font_matrix {
168            // We have a top dict matrix. Now check for a font dict matrix.
169            if let Some((sub_matrix, sub_upem)) = font_dict.font_matrix {
170                let scaling = if top_upem > 1 && sub_upem > 1 {
171                    top_upem.min(sub_upem)
172                } else {
173                    1
174                };
175                // Concatenate and scale
176                let matrix = matrix_mul_scaled(&top_matrix, &sub_matrix, scaling);
177                let upem = Fixed::from_bits(sub_upem)
178                    .mul_div(Fixed::from_bits(top_upem), Fixed::from_bits(scaling));
179                // Then normalize
180                Some(normalize_font_matrix(matrix, upem.to_bits()))
181            } else {
182                // Top matrix was already normalized on load
183                Some((top_matrix, top_upem))
184            }
185        } else if let Some((matrix, upem)) = font_dict.font_matrix {
186            // Just normalize
187            Some(normalize_font_matrix(matrix, upem))
188        } else {
189            None
190        };
191        // Now adjust our scale factor if necessary
192        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffgload.c#L450>
193        let mut font_matrix = if let Some((matrix, matrix_upem)) = font_matrix {
194            // If the scaling factor from our matrix does not equal the nominal
195            // UPEM of the font then adjust the scale.
196            if matrix_upem != upem {
197                // In this case, we need to force a scale for "unscaled"
198                // requests in order to apply the adjusted UPEM from the
199                // font matrix.
200                let original_scale = scale.unwrap_or(Fixed::from_i32(64));
201                scale = Some(
202                    original_scale.mul_div(Fixed::from_bits(upem), Fixed::from_bits(matrix_upem)),
203                );
204            }
205            Some(matrix)
206        } else {
207            None
208        };
209        if font_matrix
210            == Some([
211                Fixed::ONE,
212                Fixed::ZERO,
213                Fixed::ZERO,
214                Fixed::ONE,
215                Fixed::ZERO,
216                Fixed::ZERO,
217            ])
218        {
219            // Let's not waste time applying an identity matrix. This occurs
220            // fairly often after normalization.
221            font_matrix = None;
222        }
223        let hint_scale = scale_for_hinting(scale);
224        let hint_state = HintState::new(&private_dict.hint_params, hint_scale);
225        Ok(Subfont {
226            is_cff2: self.is_cff2(),
227            scale,
228            scale_requested,
229            subrs_offset: private_dict.subrs_offset,
230            hint_state,
231            store_index: private_dict.store_index,
232            font_matrix,
233        })
234    }
235
236    /// Loads and scales an outline for the given subfont instance, glyph
237    /// identifier and normalized variation coordinates.
238    ///
239    /// Before calling this method, use [`subfont_index`](Self::subfont_index)
240    /// to retrieve the subfont index for the desired glyph and then
241    /// [`subfont`](Self::subfont) to create an instance of the subfont for a
242    /// particular size and location in variation space.
243    /// Creating subfont instances is not free, so this process is exposed in
244    /// discrete steps to allow for caching.
245    ///
246    /// The result is emitted to the specified pen.
247    pub fn draw(
248        &self,
249        subfont: &Subfont,
250        glyph_id: GlyphId,
251        coords: &[F2Dot14],
252        hint: bool,
253        pen: &mut impl OutlinePen,
254    ) -> Result<(), Error> {
255        let cff_data = self.offset_data.as_bytes();
256        let charstrings = self.top_dict.charstrings.clone();
257        let charstring_data = charstrings.get(glyph_id.to_u32() as usize)?;
258        let subrs = subfont.subrs(self)?;
259        let blend_state = subfont.blend_state(self, coords)?;
260        let cs_eval = CharstringEvaluator {
261            cff_data,
262            charstrings,
263            global_subrs: self.global_subrs.clone(),
264            subrs,
265            blend_state,
266            charstring_data,
267        };
268        // Only apply hinting if we have a scale
269        let apply_hinting = hint && subfont.scale_requested;
270        let mut pen_sink = PenSink::new(pen);
271        let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink);
272        if let Some(matrix) = subfont.font_matrix {
273            if apply_hinting {
274                let mut transform_sink =
275                    HintedTransformingSink::new(&mut simplifying_adapter, matrix);
276                let mut hinting_adapter =
277                    HintingSink::new(&subfont.hint_state, &mut transform_sink);
278                cs_eval.evaluate(&mut hinting_adapter)?;
279                hinting_adapter.finish();
280            } else {
281                let mut transform_sink =
282                    ScalingTransformingSink::new(&mut simplifying_adapter, matrix, subfont.scale);
283                cs_eval.evaluate(&mut transform_sink)?;
284            }
285        } else if apply_hinting {
286            let mut hinting_adapter =
287                HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
288            cs_eval.evaluate(&mut hinting_adapter)?;
289            hinting_adapter.finish();
290        } else {
291            let mut scaling_adapter =
292                ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale);
293            cs_eval.evaluate(&mut scaling_adapter)?;
294        }
295        simplifying_adapter.finish();
296        Ok(())
297    }
298
299    fn parse_font_dict(&self, subfont_index: u32) -> Result<FontDict, Error> {
300        if self.top_dict.font_dicts.count() != 0 {
301            // If we have a font dict array, extract the private dict range
302            // from the font dict at the given index.
303            let font_dict_data = self.top_dict.font_dicts.get(subfont_index as usize)?;
304            FontDict::new(font_dict_data)
305        } else {
306            // Use the private dict range from the top dict.
307            // Note: "A Private DICT is required but may be specified as having
308            // a length of 0 if there are no non-default values to be stored."
309            // <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=25>
310            let range = self.top_dict.private_dict_range.clone();
311            Ok(FontDict {
312                private_dict_range: range.start as usize..range.end as usize,
313                font_matrix: None,
314            })
315        }
316    }
317}
318
319/// When hinting, use a modified scale factor.
320///
321/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279>
322fn scale_for_hinting(scale: Option<Fixed>) -> Fixed {
323    Fixed::from_bits((scale.unwrap_or(Fixed::ONE).to_bits().saturating_add(32)) / 64)
324}
325
326struct CharstringEvaluator<'a> {
327    cff_data: &'a [u8],
328    charstrings: Index<'a>,
329    global_subrs: Index<'a>,
330    subrs: Option<Index<'a>>,
331    blend_state: Option<BlendState<'a>>,
332    charstring_data: &'a [u8],
333}
334
335impl CharstringEvaluator<'_> {
336    fn evaluate(self, sink: &mut impl CommandSink) -> Result<(), Error> {
337        charstring::evaluate(
338            self.cff_data,
339            self.charstrings,
340            self.global_subrs,
341            self.subrs,
342            self.blend_state,
343            self.charstring_data,
344            sink,
345        )
346    }
347}
348
349/// Specifies local subroutines and hinting parameters for some subset of
350/// glyphs in a CFF or CFF2 table.
351///
352/// This type is designed to be cacheable to avoid re-evaluating the private
353/// dict every time a charstring is processed.
354///
355/// For variable fonts, this is dependent on a location in variation space.
356#[derive(Clone)]
357pub(crate) struct Subfont {
358    is_cff2: bool,
359    scale: Option<Fixed>,
360    /// When we have a font matrix, we might force a scale even if the user
361    /// requested unscaled output. In this case, we shouldn't apply hinting
362    /// and this keeps track of that.
363    scale_requested: bool,
364    subrs_offset: Option<usize>,
365    pub(crate) hint_state: HintState,
366    store_index: u16,
367    font_matrix: Option<[Fixed; 6]>,
368}
369
370impl Subfont {
371    /// Returns the local subroutine index.
372    pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error> {
373        if let Some(subrs_offset) = self.subrs_offset {
374            let offset_data = scaler.offset_data.as_bytes();
375            let index_data = offset_data.get(subrs_offset..).unwrap_or_default();
376            Ok(Some(Index::new(index_data, self.is_cff2)?))
377        } else {
378            Ok(None)
379        }
380    }
381
382    /// Creates a new blend state for the given normalized variation
383    /// coordinates.
384    pub fn blend_state<'a>(
385        &self,
386        scaler: &Outlines<'a>,
387        coords: &'a [F2Dot14],
388    ) -> Result<Option<BlendState<'a>>, Error> {
389        if let Some(var_store) = scaler.top_dict.var_store.clone() {
390            Ok(Some(BlendState::new(var_store, coords, self.store_index)?))
391        } else {
392            Ok(None)
393        }
394    }
395}
396
397/// Entries that we parse from the Private DICT to support charstring
398/// evaluation.
399#[derive(Default)]
400struct PrivateDict {
401    hint_params: HintParams,
402    subrs_offset: Option<usize>,
403    store_index: u16,
404}
405
406impl PrivateDict {
407    fn new(
408        data: FontData,
409        range: Range<usize>,
410        blend_state: Option<BlendState<'_>>,
411    ) -> Result<Self, Error> {
412        let private_dict_data = data.read_array(range.clone())?;
413        let mut dict = Self::default();
414        for entry in dict::entries(private_dict_data, blend_state) {
415            use dict::Entry::*;
416            match entry? {
417                BlueValues(values) => dict.hint_params.blues = values,
418                FamilyBlues(values) => dict.hint_params.family_blues = values,
419                OtherBlues(values) => dict.hint_params.other_blues = values,
420                FamilyOtherBlues(values) => dict.hint_params.family_other_blues = values,
421                BlueScale(value) => dict.hint_params.blue_scale = value,
422                BlueShift(value) => dict.hint_params.blue_shift = value,
423                BlueFuzz(value) => dict.hint_params.blue_fuzz = value,
424                LanguageGroup(group) => dict.hint_params.language_group = group,
425                // Subrs offset is relative to the private DICT
426                SubrsOffset(offset) => {
427                    dict.subrs_offset = Some(
428                        range
429                            .start
430                            .checked_add(offset)
431                            .ok_or(ReadError::OutOfBounds)?,
432                    )
433                }
434                VariationStoreIndex(index) => dict.store_index = index,
435                _ => {}
436            }
437        }
438        Ok(dict)
439    }
440}
441
442/// Entries that we parse from a Font DICT.
443#[derive(Clone, Default)]
444struct FontDict {
445    private_dict_range: Range<usize>,
446    font_matrix: Option<([Fixed; 6], i32)>,
447}
448
449impl FontDict {
450    fn new(font_dict_data: &[u8]) -> Result<Self, Error> {
451        let mut range = None;
452        let mut font_matrix = None;
453        for entry in dict::entries(font_dict_data, None) {
454            match entry? {
455                dict::Entry::PrivateDictRange(r) => {
456                    range = Some(r);
457                }
458                // We store this matrix unnormalized since FreeType
459                // concatenates this with the top dict matrix (if present)
460                // before normalizing
461                dict::Entry::FontMatrix(matrix, upem) => font_matrix = Some((matrix, upem)),
462                _ => {}
463            }
464        }
465        Ok(Self {
466            private_dict_range: range.ok_or(Error::MissingPrivateDict)?,
467            font_matrix,
468        })
469    }
470}
471
472/// Entries that we parse from the Top DICT that are required to support
473/// charstring evaluation.
474#[derive(Clone, Default)]
475struct TopDict<'a> {
476    charstrings: Index<'a>,
477    font_dicts: Index<'a>,
478    fd_select: Option<FdSelect<'a>>,
479    private_dict_range: Range<u32>,
480    font_matrix: Option<([Fixed; 6], i32)>,
481    var_store: Option<ItemVariationStore<'a>>,
482}
483
484impl<'a> TopDict<'a> {
485    fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> {
486        let mut items = TopDict::default();
487        for entry in dict::entries(top_dict_data, None) {
488            match entry? {
489                dict::Entry::CharstringsOffset(offset) => {
490                    items.charstrings =
491                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
492                }
493                dict::Entry::FdArrayOffset(offset) => {
494                    items.font_dicts =
495                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
496                }
497                dict::Entry::FdSelectOffset(offset) => {
498                    items.fd_select = Some(FdSelect::read(FontData::new(
499                        table_data.get(offset..).unwrap_or_default(),
500                    ))?);
501                }
502                dict::Entry::PrivateDictRange(range) => {
503                    items.private_dict_range = range.start as u32..range.end as u32;
504                }
505                dict::Entry::FontMatrix(matrix, upem) => {
506                    // Store this matrix normalized since FT always applies normalization
507                    items.font_matrix = Some(normalize_font_matrix(matrix, upem));
508                }
509                dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
510                    // IVS is preceded by a 2 byte length, but ensure that
511                    // we don't overflow
512                    // See <https://github.com/googlefonts/fontations/issues/1223>
513                    let offset = offset.checked_add(2).ok_or(ReadError::OutOfBounds)?;
514                    items.var_store = Some(ItemVariationStore::read(FontData::new(
515                        table_data.get(offset..).unwrap_or_default(),
516                    ))?);
517                }
518                _ => {}
519            }
520        }
521        Ok(items)
522    }
523}
524
525/// Command sink that sends the results of charstring evaluation to
526/// an [OutlinePen].
527struct PenSink<'a, P>(&'a mut P);
528
529impl<'a, P> PenSink<'a, P> {
530    fn new(pen: &'a mut P) -> Self {
531        Self(pen)
532    }
533}
534
535impl<P> CommandSink for PenSink<'_, P>
536where
537    P: OutlinePen,
538{
539    fn move_to(&mut self, x: Fixed, y: Fixed) {
540        self.0.move_to(x.to_f32(), y.to_f32());
541    }
542
543    fn line_to(&mut self, x: Fixed, y: Fixed) {
544        self.0.line_to(x.to_f32(), y.to_f32());
545    }
546
547    fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
548        self.0.curve_to(
549            cx0.to_f32(),
550            cy0.to_f32(),
551            cx1.to_f32(),
552            cy1.to_f32(),
553            x.to_f32(),
554            y.to_f32(),
555        );
556    }
557
558    fn close(&mut self) {
559        self.0.close();
560    }
561}
562
563fn transform(matrix: &[Fixed; 6], x: Fixed, y: Fixed) -> (Fixed, Fixed) {
564    (
565        matrix[0] * x + matrix[2] * y + matrix[4],
566        matrix[1] * x + matrix[3] * y + matrix[5],
567    )
568}
569
570/// Command sink adapter that applies a transform to hinted coordinates.
571struct HintedTransformingSink<'a, S> {
572    inner: &'a mut S,
573    matrix: [Fixed; 6],
574}
575
576impl<'a, S> HintedTransformingSink<'a, S> {
577    fn new(sink: &'a mut S, matrix: [Fixed; 6]) -> Self {
578        Self {
579            inner: sink,
580            matrix,
581        }
582    }
583
584    fn transform(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
585        // FreeType applies the transform to 26.6 values but we maintain
586        // values in 16.16 so convert, transform and then convert back
587        let (x, y) = transform(
588            &self.matrix,
589            Fixed::from_bits(x.to_bits() >> 10),
590            Fixed::from_bits(y.to_bits() >> 10),
591        );
592        (
593            Fixed::from_bits(x.to_bits() << 10),
594            Fixed::from_bits(y.to_bits() << 10),
595        )
596    }
597}
598
599impl<S: CommandSink> CommandSink for HintedTransformingSink<'_, S> {
600    fn hstem(&mut self, y: Fixed, dy: Fixed) {
601        self.inner.hstem(y, dy);
602    }
603
604    fn vstem(&mut self, x: Fixed, dx: Fixed) {
605        self.inner.vstem(x, dx);
606    }
607
608    fn hint_mask(&mut self, mask: &[u8]) {
609        self.inner.hint_mask(mask);
610    }
611
612    fn counter_mask(&mut self, mask: &[u8]) {
613        self.inner.counter_mask(mask);
614    }
615
616    fn clear_hints(&mut self) {
617        self.inner.clear_hints();
618    }
619
620    fn move_to(&mut self, x: Fixed, y: Fixed) {
621        let (x, y) = self.transform(x, y);
622        self.inner.move_to(x, y);
623    }
624
625    fn line_to(&mut self, x: Fixed, y: Fixed) {
626        let (x, y) = self.transform(x, y);
627        self.inner.line_to(x, y);
628    }
629
630    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
631        let (cx1, cy1) = self.transform(cx1, cy1);
632        let (cx2, cy2) = self.transform(cx2, cy2);
633        let (x, y) = self.transform(x, y);
634        self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
635    }
636
637    fn close(&mut self) {
638        self.inner.close();
639    }
640}
641
642// Used for scaling sinks below
643const ONE_OVER_64: Fixed = Fixed::from_bits(0x400);
644
645/// Command sink adapter that applies both a transform and a scaling
646/// factor.
647struct ScalingTransformingSink<'a, S> {
648    inner: &'a mut S,
649    matrix: [Fixed; 6],
650    scale: Option<Fixed>,
651}
652
653impl<'a, S> ScalingTransformingSink<'a, S> {
654    fn new(sink: &'a mut S, matrix: [Fixed; 6], scale: Option<Fixed>) -> Self {
655        Self {
656            inner: sink,
657            matrix,
658            scale,
659        }
660    }
661
662    fn transform(&self, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
663        // The following dance is necessary to exactly match FreeType's
664        // application of scaling factors. This seems to be the result
665        // of merging the contributed Adobe code while not breaking the
666        // FreeType public API.
667        //
668        // The first two steps apply to both scaled and unscaled outlines:
669        //
670        // 1. Multiply by 1/64
671        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284>
672        let ax = x * ONE_OVER_64;
673        let ay = y * ONE_OVER_64;
674        // 2. Truncate the bottom 10 bits. Combined with the division by 64,
675        // converts to font units.
676        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219>
677        let bx = Fixed::from_bits(ax.to_bits() >> 10);
678        let by = Fixed::from_bits(ay.to_bits() >> 10);
679        // 3. Apply the transform. It must be done here to match FreeType.
680        let (cx, cy) = transform(&self.matrix, bx, by);
681        if let Some(scale) = self.scale {
682            // Scaled case:
683            // 4. Multiply by the original scale factor (to 26.6)
684            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721>
685            let dx = cx * scale;
686            let dy = cy * scale;
687            // 5. Convert from 26.6 to 16.16
688            (
689                Fixed::from_bits(dx.to_bits() << 10),
690                Fixed::from_bits(dy.to_bits() << 10),
691            )
692        } else {
693            // Unscaled case:
694            // 4. Convert from integer to 16.16
695            (
696                Fixed::from_bits(cx.to_bits() << 16),
697                Fixed::from_bits(cy.to_bits() << 16),
698            )
699        }
700    }
701}
702
703impl<S: CommandSink> CommandSink for ScalingTransformingSink<'_, S> {
704    fn hstem(&mut self, y: Fixed, dy: Fixed) {
705        self.inner.hstem(y, dy);
706    }
707
708    fn vstem(&mut self, x: Fixed, dx: Fixed) {
709        self.inner.vstem(x, dx);
710    }
711
712    fn hint_mask(&mut self, mask: &[u8]) {
713        self.inner.hint_mask(mask);
714    }
715
716    fn counter_mask(&mut self, mask: &[u8]) {
717        self.inner.counter_mask(mask);
718    }
719
720    fn clear_hints(&mut self) {
721        self.inner.clear_hints();
722    }
723
724    fn move_to(&mut self, x: Fixed, y: Fixed) {
725        let (x, y) = self.transform(x, y);
726        self.inner.move_to(x, y);
727    }
728
729    fn line_to(&mut self, x: Fixed, y: Fixed) {
730        let (x, y) = self.transform(x, y);
731        self.inner.line_to(x, y);
732    }
733
734    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
735        let (cx1, cy1) = self.transform(cx1, cy1);
736        let (cx2, cy2) = self.transform(cx2, cy2);
737        let (x, y) = self.transform(x, y);
738        self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
739    }
740
741    fn close(&mut self) {
742        self.inner.close();
743    }
744}
745
746/// Command sink adapter that applies a scaling factor.
747///
748/// This assumes a 26.6 scaling factor packed into a Fixed and thus,
749/// this is not public and exists only to match FreeType's exact
750/// scaling process.
751struct ScalingSink26Dot6<'a, S> {
752    inner: &'a mut S,
753    scale: Option<Fixed>,
754}
755
756impl<'a, S> ScalingSink26Dot6<'a, S> {
757    fn new(sink: &'a mut S, scale: Option<Fixed>) -> Self {
758        Self { scale, inner: sink }
759    }
760
761    fn scale(&self, coord: Fixed) -> Fixed {
762        // The following dance is necessary to exactly match FreeType's
763        // application of scaling factors. This seems to be the result
764        // of merging the contributed Adobe code while not breaking the
765        // FreeType public API.
766        //
767        // The first two steps apply to both scaled and unscaled outlines:
768        //
769        // 1. Multiply by 1/64
770        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284>
771        let a = coord * ONE_OVER_64;
772        // 2. Truncate the bottom 10 bits. Combined with the division by 64,
773        // converts to font units.
774        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219>
775        let b = Fixed::from_bits(a.to_bits() >> 10);
776        if let Some(scale) = self.scale {
777            // Scaled case:
778            // 3. Multiply by the original scale factor (to 26.6)
779            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721>
780            let c = b * scale;
781            // 4. Convert from 26.6 to 16.16
782            Fixed::from_bits(c.to_bits() << 10)
783        } else {
784            // Unscaled case:
785            // 3. Convert from integer to 16.16
786            Fixed::from_bits(b.to_bits() << 16)
787        }
788    }
789}
790
791impl<S: CommandSink> CommandSink for ScalingSink26Dot6<'_, S> {
792    fn hstem(&mut self, y: Fixed, dy: Fixed) {
793        self.inner.hstem(y, dy);
794    }
795
796    fn vstem(&mut self, x: Fixed, dx: Fixed) {
797        self.inner.vstem(x, dx);
798    }
799
800    fn hint_mask(&mut self, mask: &[u8]) {
801        self.inner.hint_mask(mask);
802    }
803
804    fn counter_mask(&mut self, mask: &[u8]) {
805        self.inner.counter_mask(mask);
806    }
807
808    fn clear_hints(&mut self) {
809        self.inner.clear_hints();
810    }
811
812    fn move_to(&mut self, x: Fixed, y: Fixed) {
813        self.inner.move_to(self.scale(x), self.scale(y));
814    }
815
816    fn line_to(&mut self, x: Fixed, y: Fixed) {
817        self.inner.line_to(self.scale(x), self.scale(y));
818    }
819
820    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
821        self.inner.curve_to(
822            self.scale(cx1),
823            self.scale(cy1),
824            self.scale(cx2),
825            self.scale(cy2),
826            self.scale(x),
827            self.scale(y),
828        );
829    }
830
831    fn close(&mut self) {
832        self.inner.close();
833    }
834}
835
836#[derive(Copy, Clone)]
837enum PendingElement {
838    Move([Fixed; 2]),
839    Line([Fixed; 2]),
840    Curve([Fixed; 6]),
841}
842
843impl PendingElement {
844    fn target_point(&self) -> [Fixed; 2] {
845        match self {
846            Self::Move(xy) | Self::Line(xy) => *xy,
847            Self::Curve([.., x, y]) => [*x, *y],
848        }
849    }
850}
851
852/// Command sink adapter that suppresses degenerate move and line commands.
853///
854/// FreeType avoids emitting empty contours and zero length lines to prevent
855/// artifacts when stem darkening is enabled. We don't support stem darkening
856/// because it's not enabled by any of our clients but we remove the degenerate
857/// elements regardless to match the output.
858///
859/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L1786>
860struct NopFilteringSink<'a, S> {
861    is_open: bool,
862    start: Option<(Fixed, Fixed)>,
863    pending_element: Option<PendingElement>,
864    inner: &'a mut S,
865}
866
867impl<'a, S> NopFilteringSink<'a, S>
868where
869    S: CommandSink,
870{
871    fn new(inner: &'a mut S) -> Self {
872        Self {
873            is_open: false,
874            start: None,
875            pending_element: None,
876            inner,
877        }
878    }
879
880    fn flush_pending(&mut self, for_close: bool) {
881        if let Some(pending) = self.pending_element.take() {
882            match pending {
883                PendingElement::Move([x, y]) => {
884                    if !for_close {
885                        self.is_open = true;
886                        self.inner.move_to(x, y);
887                        self.start = Some((x, y));
888                    }
889                }
890                PendingElement::Line([x, y]) => {
891                    if !for_close || self.start != Some((x, y)) {
892                        self.inner.line_to(x, y);
893                    }
894                }
895                PendingElement::Curve([cx0, cy0, cx1, cy1, x, y]) => {
896                    self.inner.curve_to(cx0, cy0, cx1, cy1, x, y);
897                }
898            }
899        }
900    }
901
902    pub fn finish(&mut self) {
903        self.close();
904    }
905}
906
907impl<S> CommandSink for NopFilteringSink<'_, S>
908where
909    S: CommandSink,
910{
911    fn hstem(&mut self, y: Fixed, dy: Fixed) {
912        self.inner.hstem(y, dy);
913    }
914
915    fn vstem(&mut self, x: Fixed, dx: Fixed) {
916        self.inner.vstem(x, dx);
917    }
918
919    fn hint_mask(&mut self, mask: &[u8]) {
920        self.inner.hint_mask(mask);
921    }
922
923    fn counter_mask(&mut self, mask: &[u8]) {
924        self.inner.counter_mask(mask);
925    }
926
927    fn clear_hints(&mut self) {
928        self.inner.clear_hints();
929    }
930
931    fn move_to(&mut self, x: Fixed, y: Fixed) {
932        self.pending_element = Some(PendingElement::Move([x, y]));
933    }
934
935    fn line_to(&mut self, x: Fixed, y: Fixed) {
936        // Omit the line if we're already at the given position
937        if self
938            .pending_element
939            .map(|element| element.target_point() == [x, y])
940            .unwrap_or_default()
941        {
942            return;
943        }
944        self.flush_pending(false);
945        self.pending_element = Some(PendingElement::Line([x, y]));
946    }
947
948    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
949        self.flush_pending(false);
950        self.pending_element = Some(PendingElement::Curve([cx1, cy1, cx2, cy2, x, y]));
951    }
952
953    fn close(&mut self) {
954        self.flush_pending(true);
955        if self.is_open {
956            self.inner.close();
957            self.is_open = false;
958        }
959    }
960}
961
962/// Simple fixed point matrix multiplication with a scaling factor.
963///
964/// Note: this transforms the translation component of `b` by the upper 2x2 of
965/// `a`. This matches the offset transform FreeType uses when concatenating
966/// the matrices from the top and font dicts.
967///
968/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/base/ftcalc.c#L719>
969fn matrix_mul_scaled(a: &[Fixed; 6], b: &[Fixed; 6], scaling: i32) -> [Fixed; 6] {
970    let val = Fixed::from_i32(scaling);
971    let xx = a[0].mul_div(b[0], val) + a[2].mul_div(b[1], val);
972    let yx = a[1].mul_div(b[0], val) + a[3].mul_div(b[1], val);
973    let xy = a[0].mul_div(b[2], val) + a[2].mul_div(b[3], val);
974    let yy = a[1].mul_div(b[2], val) + a[3].mul_div(b[3], val);
975    let x = b[4];
976    let y = b[5];
977    let dx = x.mul_div(a[0], val) + y.mul_div(a[2], val);
978    let dy = x.mul_div(a[1], val) + y.mul_div(a[3], val);
979    [xx, yx, xy, yy, dx, dy]
980}
981
982#[cfg(test)]
983mod tests {
984    use super::{super::pen::SvgPen, *};
985    use crate::{
986        outline::{HintingInstance, HintingOptions},
987        prelude::{LocationRef, Size},
988        MetadataProvider,
989    };
990    use dict::Blues;
991    use font_test_data::bebuffer::BeBuffer;
992    use raw::tables::cff2::Cff2;
993    use read_fonts::FontRef;
994
995    #[test]
996    fn unscaled_scaling_sink_produces_integers() {
997        let nothing = &mut ();
998        let sink = ScalingSink26Dot6::new(nothing, None);
999        for coord in [50.0, 50.1, 50.125, 50.5, 50.9] {
1000            assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0);
1001        }
1002    }
1003
1004    #[test]
1005    fn scaled_scaling_sink() {
1006        let ppem = 20.0;
1007        let upem = 1000.0;
1008        // match FreeType scaling with intermediate conversion to 26.6
1009        let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32);
1010        let nothing = &mut ();
1011        let sink = ScalingSink26Dot6::new(nothing, Some(scale));
1012        let inputs = [
1013            // input coord, expected scaled output
1014            (0.0, 0.0),
1015            (8.0, 0.15625),
1016            (16.0, 0.3125),
1017            (32.0, 0.640625),
1018            (72.0, 1.4375),
1019            (128.0, 2.5625),
1020        ];
1021        for (coord, expected) in inputs {
1022            assert_eq!(
1023                sink.scale(Fixed::from_f64(coord)).to_f32(),
1024                expected,
1025                "scaling coord {coord}"
1026            );
1027        }
1028    }
1029
1030    #[test]
1031    fn read_cff_static() {
1032        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
1033        let cff = Outlines::new(&font).unwrap();
1034        assert!(!cff.is_cff2());
1035        assert!(cff.top_dict.var_store.is_none());
1036        assert!(cff.top_dict.font_dicts.count() == 0);
1037        assert!(!cff.top_dict.private_dict_range.is_empty());
1038        assert!(cff.top_dict.fd_select.is_none());
1039        assert_eq!(cff.subfont_count(), 1);
1040        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
1041        assert_eq!(cff.global_subrs.count(), 17);
1042    }
1043
1044    #[test]
1045    fn read_cff2_static() {
1046        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1047        let cff = Outlines::new(&font).unwrap();
1048        assert!(cff.is_cff2());
1049        assert!(cff.top_dict.var_store.is_some());
1050        assert!(cff.top_dict.font_dicts.count() != 0);
1051        assert!(cff.top_dict.private_dict_range.is_empty());
1052        assert!(cff.top_dict.fd_select.is_none());
1053        assert_eq!(cff.subfont_count(), 1);
1054        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
1055        assert_eq!(cff.global_subrs.count(), 0);
1056    }
1057
1058    #[test]
1059    fn read_example_cff2_table() {
1060        let cff2 = Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap();
1061        let top_dict =
1062            TopDict::new(cff2.offset_data().as_bytes(), cff2.top_dict_data(), true).unwrap();
1063        assert!(top_dict.var_store.is_some());
1064        assert!(top_dict.font_dicts.count() != 0);
1065        assert!(top_dict.private_dict_range.is_empty());
1066        assert!(top_dict.fd_select.is_none());
1067        assert_eq!(cff2.global_subrs().count(), 0);
1068    }
1069
1070    #[test]
1071    fn cff2_variable_outlines_match_freetype() {
1072        compare_glyphs(
1073            font_test_data::CANTARELL_VF_TRIMMED,
1074            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
1075        );
1076    }
1077
1078    #[test]
1079    fn cff_static_outlines_match_freetype() {
1080        compare_glyphs(
1081            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
1082            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
1083        );
1084    }
1085
1086    #[test]
1087    fn unhinted_ends_with_close() {
1088        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1089        let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
1090        let mut svg = SvgPen::default();
1091        glyph.draw(Size::unscaled(), &mut svg).unwrap();
1092        assert!(svg.to_string().ends_with('Z'));
1093    }
1094
1095    #[test]
1096    fn hinted_ends_with_close() {
1097        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1098        let glyphs = font.outline_glyphs();
1099        let hinter = HintingInstance::new(
1100            &glyphs,
1101            Size::unscaled(),
1102            LocationRef::default(),
1103            HintingOptions::default(),
1104        )
1105        .unwrap();
1106        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1107        let mut svg = SvgPen::default();
1108        glyph.draw(&hinter, &mut svg).unwrap();
1109        assert!(svg.to_string().ends_with('Z'));
1110    }
1111
1112    /// Ensure we don't reject an empty Private DICT
1113    #[test]
1114    fn empty_private_dict() {
1115        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
1116        let outlines = super::Outlines::new(&font).unwrap();
1117        assert!(outlines.top_dict.private_dict_range.is_empty());
1118        assert!(outlines
1119            .parse_font_dict(0)
1120            .unwrap()
1121            .private_dict_range
1122            .is_empty());
1123    }
1124
1125    /// Fuzzer caught add with overflow when computing subrs offset.
1126    /// See <https://issues.oss-fuzz.com/issues/377965575>
1127    #[test]
1128    fn subrs_offset_overflow() {
1129        // A private DICT with an overflowing subrs offset
1130        let private_dict = BeBuffer::new()
1131            .push(0u32) // pad so that range doesn't start with 0 and we overflow
1132            .push(29u8) // integer operator
1133            .push(-1i32) // integer value
1134            .push(19u8) // subrs offset operator
1135            .to_vec();
1136        // Just don't panic with overflow
1137        assert!(
1138            PrivateDict::new(FontData::new(&private_dict), 4..private_dict.len(), None).is_err()
1139        );
1140    }
1141
1142    // Fuzzer caught add with overflow when computing offset to
1143    // var store.
1144    // See <https://issues.oss-fuzz.com/issues/377574377>
1145    #[test]
1146    fn top_dict_ivs_offset_overflow() {
1147        // A top DICT with a var store offset of -1 which will cause an
1148        // overflow
1149        let top_dict = BeBuffer::new()
1150            .push(29u8) // integer operator
1151            .push(-1i32) // integer value
1152            .push(24u8) // var store offset operator
1153            .to_vec();
1154        // Just don't panic with overflow
1155        assert!(TopDict::new(&[], &top_dict, true).is_err());
1156    }
1157
1158    /// Actually apply a scale when the computed scale factor is
1159    /// equal to Fixed::ONE.
1160    ///
1161    /// Specifically, when upem = 512 and ppem = 8, this results in
1162    /// a scale factor of 65536 which was being interpreted as an
1163    /// unscaled draw request.
1164    #[test]
1165    fn proper_scaling_when_factor_equals_fixed_one() {
1166        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
1167        assert_eq!(font.head().unwrap().units_per_em(), 512);
1168        let glyphs = font.outline_glyphs();
1169        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
1170        let mut svg = SvgPen::with_precision(6);
1171        glyph
1172            .draw((Size::new(8.0), LocationRef::default()), &mut svg)
1173            .unwrap();
1174        // This was initially producing unscaled values like M405.000...
1175        assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
1176    }
1177
1178    /// For the given font data and extracted outlines, parse the extracted
1179    /// outline data into a set of expected values and compare these with the
1180    /// results generated by the scaler.
1181    ///
1182    /// This will compare all outlines at various sizes and (for variable
1183    /// fonts), locations in variation space.
1184    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
1185        use super::super::testing;
1186        let font = FontRef::new(font_data).unwrap();
1187        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
1188        let outlines = super::Outlines::new(&font).unwrap();
1189        let mut path = testing::Path::default();
1190        for expected_outline in &expected_outlines {
1191            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
1192                continue;
1193            }
1194            let size = (expected_outline.size != 0.0).then_some(expected_outline.size);
1195            path.elements.clear();
1196            let subfont = outlines
1197                .subfont(
1198                    outlines.subfont_index(expected_outline.glyph_id),
1199                    size,
1200                    &expected_outline.coords,
1201                )
1202                .unwrap();
1203            outlines
1204                .draw(
1205                    &subfont,
1206                    expected_outline.glyph_id,
1207                    &expected_outline.coords,
1208                    false,
1209                    &mut path,
1210                )
1211                .unwrap();
1212            if path.elements != expected_outline.path {
1213                panic!(
1214                    "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
1215                    expected_outline.glyph_id,
1216                    expected_outline.size,
1217                    expected_outline.coords,
1218                    &path.elements,
1219                    &expected_outline.path
1220                );
1221            }
1222        }
1223    }
1224
1225    // We were overwriting family_other_blues with family_blues.
1226    #[test]
1227    fn capture_family_other_blues() {
1228        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
1229        let store =
1230            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
1231        let coords = &[F2Dot14::from_f32(0.0)];
1232        let blend_state = BlendState::new(store, coords, 0).unwrap();
1233        let private_dict = PrivateDict::new(
1234            FontData::new(private_dict_data),
1235            0..private_dict_data.len(),
1236            Some(blend_state),
1237        )
1238        .unwrap();
1239        assert_eq!(
1240            private_dict.hint_params.family_other_blues,
1241            Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
1242        )
1243    }
1244
1245    #[test]
1246    fn implied_seac() {
1247        let font = FontRef::new(font_test_data::CHARSTRING_PATH_OPS).unwrap();
1248        let glyphs = font.outline_glyphs();
1249        let gid = GlyphId::new(3);
1250        assert_eq!(font.glyph_names().get(gid).unwrap(), "Scaron");
1251        let glyph = glyphs.get(gid).unwrap();
1252        let mut pen = SvgPen::new();
1253        glyph
1254            .draw((Size::unscaled(), LocationRef::default()), &mut pen)
1255            .unwrap();
1256        // This triggers the seac behavior in the endchar operator which
1257        // loads an accent character followed by a base character. Ensure
1258        // that we have a path to represent each by checking for two closepath
1259        // commands.
1260        assert_eq!(pen.to_string().chars().filter(|ch| *ch == 'Z').count(), 2);
1261    }
1262
1263    #[test]
1264    fn implied_seac_clears_hints() {
1265        let font = FontRef::new(font_test_data::CHARSTRING_PATH_OPS).unwrap();
1266        let outlines = Outlines::from_cff(&font, 1000).unwrap();
1267        let subfont = outlines.subfont(0, Some(16.0), &[]).unwrap();
1268        let cff_data = outlines.offset_data.as_bytes();
1269        let charstrings = outlines.top_dict.charstrings.clone();
1270        let charstring_data = charstrings.get(3).unwrap();
1271        let subrs = subfont.subrs(&outlines).unwrap();
1272        let blend_state = None;
1273        let cs_eval = CharstringEvaluator {
1274            cff_data,
1275            charstrings,
1276            global_subrs: outlines.global_subrs.clone(),
1277            subrs,
1278            blend_state,
1279            charstring_data,
1280        };
1281        struct ClearHintsCountingSink(u32);
1282        impl CommandSink for ClearHintsCountingSink {
1283            fn move_to(&mut self, _: Fixed, _: Fixed) {}
1284            fn line_to(&mut self, _: Fixed, _: Fixed) {}
1285            fn curve_to(&mut self, _: Fixed, _: Fixed, _: Fixed, _: Fixed, _: Fixed, _: Fixed) {}
1286            fn close(&mut self) {}
1287            fn clear_hints(&mut self) {
1288                self.0 += 1;
1289            }
1290        }
1291        let mut sink = ClearHintsCountingSink(0);
1292        cs_eval.evaluate(&mut sink).unwrap();
1293        // We should have cleared hints twice.. once for the base and once
1294        // for the accent
1295        assert_eq!(sink.0, 2);
1296    }
1297
1298    const TRANSFORM: [Fixed; 6] = [
1299        Fixed::ONE,
1300        Fixed::ZERO,
1301        // 0.167007446289062
1302        Fixed::from_bits(10945),
1303        Fixed::ONE,
1304        Fixed::ZERO,
1305        Fixed::ZERO,
1306    ];
1307
1308    #[test]
1309    fn hinted_transform_sink() {
1310        // A few points taken from the test font in <https://github.com/googlefonts/fontations/issues/1581>
1311        // Inputs and expected values extracted from FreeType
1312        let input = [(383i32, 117i32), (450, 20), (555, -34), (683, -34)]
1313            .map(|(x, y)| (Fixed::from_bits(x << 10), Fixed::from_bits(y << 10)));
1314        let expected = [(403, 117i32), (453, 20), (549, -34), (677, -34)]
1315            .map(|(x, y)| (Fixed::from_bits(x << 10), Fixed::from_bits(y << 10)));
1316        let mut dummy = ();
1317        let sink = HintedTransformingSink::new(&mut dummy, TRANSFORM);
1318        let transformed = input.map(|(x, y)| sink.transform(x, y));
1319        assert_eq!(transformed, expected);
1320    }
1321
1322    #[test]
1323    fn unhinted_scaled_transform_sink() {
1324        // A few points taken from the test font in <https://github.com/googlefonts/fontations/issues/1581>
1325        // Inputs and expected values extracted from FreeType
1326        let input = [(150i32, 46i32), (176, 8), (217, -13), (267, -13)]
1327            .map(|(x, y)| (Fixed::from_bits(x << 16), Fixed::from_bits(y << 16)));
1328        let expected = [(404, 118i32), (453, 20), (550, -33), (678, -33)]
1329            .map(|(x, y)| (Fixed::from_bits(x << 10), Fixed::from_bits(y << 10)));
1330        let mut dummy = ();
1331        let sink =
1332            ScalingTransformingSink::new(&mut dummy, TRANSFORM, Some(Fixed::from_bits(167772)));
1333        let transformed = input.map(|(x, y)| sink.transform(x, y));
1334        assert_eq!(transformed, expected);
1335    }
1336
1337    #[test]
1338    fn unhinted_unscaled_transform_sink() {
1339        // A few points taken from the test font in <https://github.com/googlefonts/fontations/issues/1581>
1340        // Inputs and expected values extracted from FreeType
1341        let input = [(150i32, 46i32), (176, 8), (217, -13), (267, -13)]
1342            .map(|(x, y)| (Fixed::from_bits(x << 16), Fixed::from_bits(y << 16)));
1343        let expected = [(158, 46i32), (177, 8), (215, -13), (265, -13)]
1344            .map(|(x, y)| (Fixed::from_bits(x << 16), Fixed::from_bits(y << 16)));
1345        let mut dummy = ();
1346        let sink = ScalingTransformingSink::new(&mut dummy, TRANSFORM, None);
1347        let transformed = input.map(|(x, y)| sink.transform(x, y));
1348        assert_eq!(transformed, expected);
1349    }
1350
1351    #[test]
1352    fn fixed_matrix_mul() {
1353        let a = [0.5, 0.75, -1.0, 2.0, 0.0, 0.0].map(Fixed::from_f64);
1354        let b = [1.5, -1.0, 0.25, -1.0, 1.0, 2.0].map(Fixed::from_f64);
1355        let expected = [1.75, -0.875, 1.125, -1.8125, -1.5, 4.75].map(Fixed::from_f64);
1356        let result = matrix_mul_scaled(&a, &b, 1);
1357        assert_eq!(result, expected);
1358    }
1359
1360    /// See <https://github.com/googlefonts/fontations/issues/1638>
1361    #[test]
1362    fn nested_font_matrices() {
1363        // Expected values extracted from FreeType debugging session
1364        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
1365        let outlines = Outlines::from_cff(&font, 512).unwrap();
1366        // Check the normalized top dict matrix
1367        let (top_matrix, top_upem) = outlines.top_dict.font_matrix.unwrap();
1368        let expected_top_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
1369        assert_eq!(top_matrix, expected_top_matrix);
1370        assert_eq!(top_upem, 512);
1371        // Check the unnormalized font dict matrix
1372        let (sub_matrix, sub_upem) = outlines.parse_font_dict(0).unwrap().font_matrix.unwrap();
1373        let expected_sub_matrix = [327680, 0, 0, 327680, 0, 0].map(Fixed::from_bits);
1374        assert_eq!(sub_matrix, expected_sub_matrix);
1375        assert_eq!(sub_upem, 10);
1376        // Check the normalized combined matrix
1377        let subfont = outlines.subfont(0, Some(24.0), &[]).unwrap();
1378        let expected_combined_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
1379        assert_eq!(subfont.font_matrix.unwrap(), expected_combined_matrix);
1380        // Check the final scale
1381        assert_eq!(subfont.scale.unwrap().to_bits(), 98304);
1382    }
1383
1384    /// OSS fuzz caught add with overflow for hint scale computation.
1385    /// See <https://oss-fuzz.com/testcase-detail/6498790355042304>
1386    /// and <https://issues.oss-fuzz.com/issues/444024349>
1387    #[test]
1388    fn subfont_hint_scale_overflow() {
1389        // Just don't panic with overflow
1390        let _ = scale_for_hinting(Some(Fixed::from_bits(i32::MAX)));
1391    }
1392}