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