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::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 private_dict_range = self.private_dict_range(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 = PrivateDict::new(self.offset_data, private_dict_range, blend_state)?;
154        let scale = match size {
155            Some(ppem) if self.units_per_em > 0 => {
156                // Note: we do an intermediate scale to 26.6 to ensure we
157                // match FreeType
158                Some(
159                    Fixed::from_bits((ppem * 64.) as i32)
160                        / Fixed::from_bits(self.units_per_em as i32),
161                )
162            }
163            _ => None,
164        };
165        // When hinting, use a modified scale factor
166        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279>
167        let hint_scale = Fixed::from_bits((scale.unwrap_or(Fixed::ONE).to_bits() + 32) / 64);
168        let hint_state = HintState::new(&private_dict.hint_params, hint_scale);
169        Ok(Subfont {
170            is_cff2: self.is_cff2(),
171            scale,
172            subrs_offset: private_dict.subrs_offset,
173            hint_state,
174            store_index: private_dict.store_index,
175        })
176    }
177
178    /// Loads and scales an outline for the given subfont instance, glyph
179    /// identifier and normalized variation coordinates.
180    ///
181    /// Before calling this method, use [`subfont_index`](Self::subfont_index)
182    /// to retrieve the subfont index for the desired glyph and then
183    /// [`subfont`](Self::subfont) to create an instance of the subfont for a
184    /// particular size and location in variation space.
185    /// Creating subfont instances is not free, so this process is exposed in
186    /// discrete steps to allow for caching.
187    ///
188    /// The result is emitted to the specified pen.
189    pub fn draw(
190        &self,
191        subfont: &Subfont,
192        glyph_id: GlyphId,
193        coords: &[F2Dot14],
194        hint: bool,
195        pen: &mut impl OutlinePen,
196    ) -> Result<(), Error> {
197        let charstring_data = self.top_dict.charstrings.get(glyph_id.to_u32() as usize)?;
198        let subrs = subfont.subrs(self)?;
199        let blend_state = subfont.blend_state(self, coords)?;
200        let mut pen_sink = PenSink::new(pen);
201        let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink);
202        // Only apply hinting if we have a scale
203        if hint && subfont.scale.is_some() {
204            let mut hinting_adapter =
205                HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
206            charstring::evaluate(
207                charstring_data,
208                self.global_subrs.clone(),
209                subrs,
210                blend_state,
211                &mut hinting_adapter,
212            )?;
213            hinting_adapter.finish();
214        } else {
215            let mut scaling_adapter =
216                ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale);
217            charstring::evaluate(
218                charstring_data,
219                self.global_subrs.clone(),
220                subrs,
221                blend_state,
222                &mut scaling_adapter,
223            )?;
224        }
225        simplifying_adapter.finish();
226        Ok(())
227    }
228
229    fn private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error> {
230        if self.top_dict.font_dicts.count() != 0 {
231            // If we have a font dict array, extract the private dict range
232            // from the font dict at the given index.
233            let font_dict_data = self.top_dict.font_dicts.get(subfont_index as usize)?;
234            let mut range = None;
235            for entry in dict::entries(font_dict_data, None) {
236                if let dict::Entry::PrivateDictRange(r) = entry? {
237                    range = Some(r);
238                    break;
239                }
240            }
241            range
242        } else {
243            // Use the private dict range from the top dict.
244            // Note: "A Private DICT is required but may be specified as having
245            // a length of 0 if there are no non-default values to be stored."
246            // <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=25>
247            let range = self.top_dict.private_dict_range.clone();
248            Some(range.start as usize..range.end as usize)
249        }
250        .ok_or(Error::MissingPrivateDict)
251    }
252}
253
254/// Specifies local subroutines and hinting parameters for some subset of
255/// glyphs in a CFF or CFF2 table.
256///
257/// This type is designed to be cacheable to avoid re-evaluating the private
258/// dict every time a charstring is processed.
259///
260/// For variable fonts, this is dependent on a location in variation space.
261#[derive(Clone)]
262pub(crate) struct Subfont {
263    is_cff2: bool,
264    scale: Option<Fixed>,
265    subrs_offset: Option<usize>,
266    pub(crate) hint_state: HintState,
267    store_index: u16,
268}
269
270impl Subfont {
271    /// Returns the local subroutine index.
272    pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error> {
273        if let Some(subrs_offset) = self.subrs_offset {
274            let offset_data = scaler.offset_data.as_bytes();
275            let index_data = offset_data.get(subrs_offset..).unwrap_or_default();
276            Ok(Some(Index::new(index_data, self.is_cff2)?))
277        } else {
278            Ok(None)
279        }
280    }
281
282    /// Creates a new blend state for the given normalized variation
283    /// coordinates.
284    pub fn blend_state<'a>(
285        &self,
286        scaler: &Outlines<'a>,
287        coords: &'a [F2Dot14],
288    ) -> Result<Option<BlendState<'a>>, Error> {
289        if let Some(var_store) = scaler.top_dict.var_store.clone() {
290            Ok(Some(BlendState::new(var_store, coords, self.store_index)?))
291        } else {
292            Ok(None)
293        }
294    }
295}
296
297/// Entries that we parse from the Private DICT to support charstring
298/// evaluation.
299#[derive(Default)]
300struct PrivateDict {
301    hint_params: HintParams,
302    subrs_offset: Option<usize>,
303    store_index: u16,
304}
305
306impl PrivateDict {
307    fn new(
308        data: FontData,
309        range: Range<usize>,
310        blend_state: Option<BlendState<'_>>,
311    ) -> Result<Self, Error> {
312        let private_dict_data = data.read_array(range.clone())?;
313        let mut dict = Self::default();
314        for entry in dict::entries(private_dict_data, blend_state) {
315            use dict::Entry::*;
316            match entry? {
317                BlueValues(values) => dict.hint_params.blues = values,
318                FamilyBlues(values) => dict.hint_params.family_blues = values,
319                OtherBlues(values) => dict.hint_params.other_blues = values,
320                FamilyOtherBlues(values) => dict.hint_params.family_other_blues = values,
321                BlueScale(value) => dict.hint_params.blue_scale = value,
322                BlueShift(value) => dict.hint_params.blue_shift = value,
323                BlueFuzz(value) => dict.hint_params.blue_fuzz = value,
324                LanguageGroup(group) => dict.hint_params.language_group = group,
325                // Subrs offset is relative to the private DICT
326                SubrsOffset(offset) => {
327                    dict.subrs_offset = Some(
328                        range
329                            .start
330                            .checked_add(offset)
331                            .ok_or(ReadError::OutOfBounds)?,
332                    )
333                }
334                VariationStoreIndex(index) => dict.store_index = index,
335                _ => {}
336            }
337        }
338        Ok(dict)
339    }
340}
341
342/// Entries that we parse from the Top DICT that are required to support
343/// charstring evaluation.
344#[derive(Clone, Default)]
345struct TopDict<'a> {
346    charstrings: Index<'a>,
347    font_dicts: Index<'a>,
348    fd_select: Option<FdSelect<'a>>,
349    private_dict_range: Range<u32>,
350    var_store: Option<ItemVariationStore<'a>>,
351}
352
353impl<'a> TopDict<'a> {
354    fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> {
355        let mut items = TopDict::default();
356        for entry in dict::entries(top_dict_data, None) {
357            match entry? {
358                dict::Entry::CharstringsOffset(offset) => {
359                    items.charstrings =
360                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
361                }
362                dict::Entry::FdArrayOffset(offset) => {
363                    items.font_dicts =
364                        Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
365                }
366                dict::Entry::FdSelectOffset(offset) => {
367                    items.fd_select = Some(FdSelect::read(FontData::new(
368                        table_data.get(offset..).unwrap_or_default(),
369                    ))?);
370                }
371                dict::Entry::PrivateDictRange(range) => {
372                    items.private_dict_range = range.start as u32..range.end as u32;
373                }
374                dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
375                    // IVS is preceded by a 2 byte length, but ensure that
376                    // we don't overflow
377                    // See <https://github.com/googlefonts/fontations/issues/1223>
378                    let offset = offset.checked_add(2).ok_or(ReadError::OutOfBounds)?;
379                    items.var_store = Some(ItemVariationStore::read(FontData::new(
380                        table_data.get(offset..).unwrap_or_default(),
381                    ))?);
382                }
383                _ => {}
384            }
385        }
386        Ok(items)
387    }
388}
389
390/// Command sink that sends the results of charstring evaluation to
391/// an [OutlinePen].
392struct PenSink<'a, P>(&'a mut P);
393
394impl<'a, P> PenSink<'a, P> {
395    fn new(pen: &'a mut P) -> Self {
396        Self(pen)
397    }
398}
399
400impl<P> CommandSink for PenSink<'_, P>
401where
402    P: OutlinePen,
403{
404    fn move_to(&mut self, x: Fixed, y: Fixed) {
405        self.0.move_to(x.to_f32(), y.to_f32());
406    }
407
408    fn line_to(&mut self, x: Fixed, y: Fixed) {
409        self.0.line_to(x.to_f32(), y.to_f32());
410    }
411
412    fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
413        self.0.curve_to(
414            cx0.to_f32(),
415            cy0.to_f32(),
416            cx1.to_f32(),
417            cy1.to_f32(),
418            x.to_f32(),
419            y.to_f32(),
420        );
421    }
422
423    fn close(&mut self) {
424        self.0.close();
425    }
426}
427
428/// Command sink adapter that applies a scaling factor.
429///
430/// This assumes a 26.6 scaling factor packed into a Fixed and thus,
431/// this is not public and exists only to match FreeType's exact
432/// scaling process.
433struct ScalingSink26Dot6<'a, S> {
434    inner: &'a mut S,
435    scale: Option<Fixed>,
436}
437
438impl<'a, S> ScalingSink26Dot6<'a, S> {
439    fn new(sink: &'a mut S, scale: Option<Fixed>) -> Self {
440        Self { scale, inner: sink }
441    }
442
443    fn scale(&self, coord: Fixed) -> Fixed {
444        // The following dance is necessary to exactly match FreeType's
445        // application of scaling factors. This seems to be the result
446        // of merging the contributed Adobe code while not breaking the
447        // FreeType public API.
448        //
449        // The first two steps apply to both scaled and unscaled outlines:
450        //
451        // 1. Multiply by 1/64
452        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284>
453        let a = coord * Fixed::from_bits(0x0400);
454        // 2. Truncate the bottom 10 bits. Combined with the division by 64,
455        // converts to font units.
456        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219>
457        let b = Fixed::from_bits(a.to_bits() >> 10);
458        if let Some(scale) = self.scale {
459            // Scaled case:
460            // 3. Multiply by the original scale factor (to 26.6)
461            // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721>
462            let c = b * scale;
463            // 4. Convert from 26.6 to 16.16
464            Fixed::from_bits(c.to_bits() << 10)
465        } else {
466            // Unscaled case:
467            // 3. Convert from integer to 16.16
468            Fixed::from_bits(b.to_bits() << 16)
469        }
470    }
471}
472
473impl<S: CommandSink> CommandSink for ScalingSink26Dot6<'_, S> {
474    fn hstem(&mut self, y: Fixed, dy: Fixed) {
475        self.inner.hstem(y, dy);
476    }
477
478    fn vstem(&mut self, x: Fixed, dx: Fixed) {
479        self.inner.vstem(x, dx);
480    }
481
482    fn hint_mask(&mut self, mask: &[u8]) {
483        self.inner.hint_mask(mask);
484    }
485
486    fn counter_mask(&mut self, mask: &[u8]) {
487        self.inner.counter_mask(mask);
488    }
489
490    fn move_to(&mut self, x: Fixed, y: Fixed) {
491        self.inner.move_to(self.scale(x), self.scale(y));
492    }
493
494    fn line_to(&mut self, x: Fixed, y: Fixed) {
495        self.inner.line_to(self.scale(x), self.scale(y));
496    }
497
498    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
499        self.inner.curve_to(
500            self.scale(cx1),
501            self.scale(cy1),
502            self.scale(cx2),
503            self.scale(cy2),
504            self.scale(x),
505            self.scale(y),
506        );
507    }
508
509    fn close(&mut self) {
510        self.inner.close();
511    }
512}
513
514/// Command sink adapter that suppresses degenerate move and line commands.
515///
516/// FreeType avoids emitting empty contours and zero length lines to prevent
517/// artifacts when stem darkening is enabled. We don't support stem darkening
518/// because it's not enabled by any of our clients but we remove the degenerate
519/// elements regardless to match the output.
520///
521/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L1786>
522struct NopFilteringSink<'a, S> {
523    start: Option<(Fixed, Fixed)>,
524    last: Option<(Fixed, Fixed)>,
525    pending_move: Option<(Fixed, Fixed)>,
526    inner: &'a mut S,
527}
528
529impl<'a, S> NopFilteringSink<'a, S>
530where
531    S: CommandSink,
532{
533    fn new(inner: &'a mut S) -> Self {
534        Self {
535            start: None,
536            last: None,
537            pending_move: None,
538            inner,
539        }
540    }
541
542    fn flush_pending_move(&mut self) {
543        if let Some((x, y)) = self.pending_move.take() {
544            if let Some((last_x, last_y)) = self.start {
545                if self.last != self.start {
546                    self.inner.line_to(last_x, last_y);
547                }
548            }
549            self.start = Some((x, y));
550            self.last = None;
551            self.inner.move_to(x, y);
552        }
553    }
554
555    pub fn finish(&mut self) {
556        if let Some((x, y)) = self.start {
557            if self.last != self.start {
558                self.inner.line_to(x, y);
559            }
560            self.inner.close();
561        }
562    }
563}
564
565impl<S> CommandSink for NopFilteringSink<'_, S>
566where
567    S: CommandSink,
568{
569    fn hstem(&mut self, y: Fixed, dy: Fixed) {
570        self.inner.hstem(y, dy);
571    }
572
573    fn vstem(&mut self, x: Fixed, dx: Fixed) {
574        self.inner.vstem(x, dx);
575    }
576
577    fn hint_mask(&mut self, mask: &[u8]) {
578        self.inner.hint_mask(mask);
579    }
580
581    fn counter_mask(&mut self, mask: &[u8]) {
582        self.inner.counter_mask(mask);
583    }
584
585    fn move_to(&mut self, x: Fixed, y: Fixed) {
586        self.pending_move = Some((x, y));
587    }
588
589    fn line_to(&mut self, x: Fixed, y: Fixed) {
590        if self.pending_move == Some((x, y)) {
591            return;
592        }
593        self.flush_pending_move();
594        if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) {
595            return;
596        }
597        self.inner.line_to(x, y);
598        self.last = Some((x, y));
599    }
600
601    fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
602        self.flush_pending_move();
603        self.last = Some((x, y));
604        self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
605    }
606
607    fn close(&mut self) {
608        if self.pending_move.is_none() {
609            self.inner.close();
610            self.start = None;
611            self.last = None;
612        }
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::{super::pen::SvgPen, *};
619    use crate::{
620        outline::{HintingInstance, HintingOptions},
621        prelude::{LocationRef, Size},
622        MetadataProvider,
623    };
624    use dict::Blues;
625    use font_test_data::bebuffer::BeBuffer;
626    use raw::tables::cff2::Cff2;
627    use read_fonts::FontRef;
628
629    #[test]
630    fn unscaled_scaling_sink_produces_integers() {
631        let nothing = &mut ();
632        let sink = ScalingSink26Dot6::new(nothing, None);
633        for coord in [50.0, 50.1, 50.125, 50.5, 50.9] {
634            assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0);
635        }
636    }
637
638    #[test]
639    fn scaled_scaling_sink() {
640        let ppem = 20.0;
641        let upem = 1000.0;
642        // match FreeType scaling with intermediate conversion to 26.6
643        let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32);
644        let nothing = &mut ();
645        let sink = ScalingSink26Dot6::new(nothing, Some(scale));
646        let inputs = [
647            // input coord, expected scaled output
648            (0.0, 0.0),
649            (8.0, 0.15625),
650            (16.0, 0.3125),
651            (32.0, 0.640625),
652            (72.0, 1.4375),
653            (128.0, 2.5625),
654        ];
655        for (coord, expected) in inputs {
656            assert_eq!(
657                sink.scale(Fixed::from_f64(coord)).to_f32(),
658                expected,
659                "scaling coord {coord}"
660            );
661        }
662    }
663
664    #[test]
665    fn read_cff_static() {
666        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
667        let cff = Outlines::new(&font).unwrap();
668        assert!(!cff.is_cff2());
669        assert!(cff.top_dict.var_store.is_none());
670        assert!(cff.top_dict.font_dicts.count() == 0);
671        assert!(!cff.top_dict.private_dict_range.is_empty());
672        assert!(cff.top_dict.fd_select.is_none());
673        assert_eq!(cff.subfont_count(), 1);
674        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
675        assert_eq!(cff.global_subrs.count(), 17);
676    }
677
678    #[test]
679    fn read_cff2_static() {
680        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
681        let cff = Outlines::new(&font).unwrap();
682        assert!(cff.is_cff2());
683        assert!(cff.top_dict.var_store.is_some());
684        assert!(cff.top_dict.font_dicts.count() != 0);
685        assert!(cff.top_dict.private_dict_range.is_empty());
686        assert!(cff.top_dict.fd_select.is_none());
687        assert_eq!(cff.subfont_count(), 1);
688        assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
689        assert_eq!(cff.global_subrs.count(), 0);
690    }
691
692    #[test]
693    fn read_example_cff2_table() {
694        let cff2 = Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap();
695        let top_dict =
696            TopDict::new(cff2.offset_data().as_bytes(), cff2.top_dict_data(), true).unwrap();
697        assert!(top_dict.var_store.is_some());
698        assert!(top_dict.font_dicts.count() != 0);
699        assert!(top_dict.private_dict_range.is_empty());
700        assert!(top_dict.fd_select.is_none());
701        assert_eq!(cff2.global_subrs().count(), 0);
702    }
703
704    #[test]
705    fn cff2_variable_outlines_match_freetype() {
706        compare_glyphs(
707            font_test_data::CANTARELL_VF_TRIMMED,
708            font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
709        );
710    }
711
712    #[test]
713    fn cff_static_outlines_match_freetype() {
714        compare_glyphs(
715            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
716            font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
717        );
718    }
719
720    #[test]
721    fn unhinted_ends_with_close() {
722        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
723        let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
724        let mut svg = SvgPen::default();
725        glyph.draw(Size::unscaled(), &mut svg).unwrap();
726        assert!(svg.to_string().ends_with('Z'));
727    }
728
729    #[test]
730    fn hinted_ends_with_close() {
731        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
732        let glyphs = font.outline_glyphs();
733        let hinter = HintingInstance::new(
734            &glyphs,
735            Size::unscaled(),
736            LocationRef::default(),
737            HintingOptions::default(),
738        )
739        .unwrap();
740        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
741        let mut svg = SvgPen::default();
742        glyph.draw(&hinter, &mut svg).unwrap();
743        assert!(svg.to_string().ends_with('Z'));
744    }
745
746    /// Ensure we don't reject an empty Private DICT
747    #[test]
748    fn empty_private_dict() {
749        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
750        let outlines = super::Outlines::new(&font).unwrap();
751        assert!(outlines.top_dict.private_dict_range.is_empty());
752        assert!(outlines.private_dict_range(0).unwrap().is_empty());
753    }
754
755    /// Fuzzer caught add with overflow when computing subrs offset.
756    /// See <https://issues.oss-fuzz.com/issues/377965575>
757    #[test]
758    fn subrs_offset_overflow() {
759        // A private DICT with an overflowing subrs offset
760        let private_dict = BeBuffer::new()
761            .push(0u32) // pad so that range doesn't start with 0 and we overflow
762            .push(29u8) // integer operator
763            .push(-1i32) // integer value
764            .push(19u8) // subrs offset operator
765            .to_vec();
766        // Just don't panic with overflow
767        assert!(
768            PrivateDict::new(FontData::new(&private_dict), 4..private_dict.len(), None).is_err()
769        );
770    }
771
772    // Fuzzer caught add with overflow when computing offset to
773    // var store.
774    // See <https://issues.oss-fuzz.com/issues/377574377>
775    #[test]
776    fn top_dict_ivs_offset_overflow() {
777        // A top DICT with a var store offset of -1 which will cause an
778        // overflow
779        let top_dict = BeBuffer::new()
780            .push(29u8) // integer operator
781            .push(-1i32) // integer value
782            .push(24u8) // var store offset operator
783            .to_vec();
784        // Just don't panic with overflow
785        assert!(TopDict::new(&[], &top_dict, true).is_err());
786    }
787
788    /// Actually apply a scale when the computed scale factor is
789    /// equal to Fixed::ONE.
790    ///
791    /// Specifically, when upem = 512 and ppem = 8, this results in
792    /// a scale factor of 65536 which was being interpreted as an
793    /// unscaled draw request.
794    #[test]
795    fn proper_scaling_when_factor_equals_fixed_one() {
796        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
797        assert_eq!(font.head().unwrap().units_per_em(), 512);
798        let glyphs = font.outline_glyphs();
799        let glyph = glyphs.get(GlyphId::new(1)).unwrap();
800        let mut svg = SvgPen::with_precision(6);
801        glyph
802            .draw((Size::new(8.0), LocationRef::default()), &mut svg)
803            .unwrap();
804        // This was initially producing unscaled values like M405.000...
805        assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
806    }
807
808    /// For the given font data and extracted outlines, parse the extracted
809    /// outline data into a set of expected values and compare these with the
810    /// results generated by the scaler.
811    ///
812    /// This will compare all outlines at various sizes and (for variable
813    /// fonts), locations in variation space.
814    fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
815        use super::super::testing;
816        let font = FontRef::new(font_data).unwrap();
817        let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
818        let outlines = super::Outlines::new(&font).unwrap();
819        let mut path = testing::Path::default();
820        for expected_outline in &expected_outlines {
821            if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
822                continue;
823            }
824            let size = (expected_outline.size != 0.0).then_some(expected_outline.size);
825            path.elements.clear();
826            let subfont = outlines
827                .subfont(
828                    outlines.subfont_index(expected_outline.glyph_id),
829                    size,
830                    &expected_outline.coords,
831                )
832                .unwrap();
833            outlines
834                .draw(
835                    &subfont,
836                    expected_outline.glyph_id,
837                    &expected_outline.coords,
838                    false,
839                    &mut path,
840                )
841                .unwrap();
842            if path.elements != expected_outline.path {
843                panic!(
844                    "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
845                    expected_outline.glyph_id,
846                    expected_outline.size,
847                    expected_outline.coords,
848                    &path.elements,
849                    &expected_outline.path
850                );
851            }
852        }
853    }
854
855    // We were overwriting family_other_blues with family_blues.
856    #[test]
857    fn capture_family_other_blues() {
858        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
859        let store =
860            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
861        let coords = &[F2Dot14::from_f32(0.0)];
862        let blend_state = BlendState::new(store, coords, 0).unwrap();
863        let private_dict = PrivateDict::new(
864            FontData::new(private_dict_data),
865            0..private_dict_data.len(),
866            Some(blend_state),
867        )
868        .unwrap();
869        assert_eq!(
870            private_dict.hint_params.family_other_blues,
871            Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
872        )
873    }
874}