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