xref: /aosp_15_r20/external/cronet/third_party/rust/chromium_crates_io/vendor/skrifa-0.15.5/src/outline/cff/mod.rs (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 //! Support for scaling CFF outlines.
2 
3 mod hint;
4 
5 use std::ops::Range;
6 
7 use read_fonts::{
8     tables::{
9         cff::Cff,
10         cff2::Cff2,
11         postscript::{
12             charstring::{self, CommandSink},
13             dict, BlendState, Error, FdSelect, Index,
14         },
15         variations::ItemVariationStore,
16     },
17     types::{F2Dot14, Fixed, GlyphId, Pen},
18     FontData, FontRead, TableProvider,
19 };
20 
21 use hint::{HintParams, HintState, HintingSink};
22 
23 /// Type for loading, scaling and hinting outlines in CFF/CFF2 tables.
24 ///
25 /// The skrifa crate provides a higher level interface for this that handles
26 /// caching and abstracting over the different outline formats. Consider using
27 /// that if detailed control over resources is not required.
28 ///
29 /// # Subfonts
30 ///
31 /// CFF tables can contain multiple logical "subfonts" which determine the
32 /// state required for processing some subset of glyphs. This state is
33 /// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28)
34 /// operators to select an appropriate subfont for any given glyph identifier.
35 /// This process is exposed on this type with the
36 /// [`subfont_index`](Self::subfont_index) method to retrieve the subfont
37 /// index for the requested glyph followed by using the
38 /// [`subfont`](Self::subfont) method to create an appropriately configured
39 /// subfont for that glyph.
40 #[derive(Clone)]
41 pub(crate) struct Outlines<'a> {
42     version: Version<'a>,
43     top_dict: TopDict<'a>,
44     units_per_em: u16,
45 }
46 
47 impl<'a> Outlines<'a> {
48     /// Creates a new scaler for the given font.
49     ///
50     /// This will choose an underyling CFF2 or CFF table from the font, in that
51     /// order.
new(font: &impl TableProvider<'a>) -> Result<Self, Error>52     pub fn new(font: &impl TableProvider<'a>) -> Result<Self, Error> {
53         let units_per_em = font.head()?.units_per_em();
54         if let Ok(cff2) = font.cff2() {
55             Self::from_cff2(cff2, units_per_em)
56         } else {
57             // "The Name INDEX in the CFF data must contain only one entry;
58             // that is, there must be only one font in the CFF FontSet"
59             // So we always pass 0 for Top DICT index when reading from an
60             // OpenType font.
61             // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff>
62             Self::from_cff(font.cff()?, 0, units_per_em)
63         }
64     }
65 
from_cff( cff1: Cff<'a>, top_dict_index: usize, units_per_em: u16, ) -> Result<Self, Error>66     pub fn from_cff(
67         cff1: Cff<'a>,
68         top_dict_index: usize,
69         units_per_em: u16,
70     ) -> Result<Self, Error> {
71         let top_dict_data = cff1.top_dicts().get(top_dict_index)?;
72         let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false)?;
73         Ok(Self {
74             version: Version::Version1(cff1),
75             top_dict,
76             units_per_em,
77         })
78     }
79 
from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result<Self, Error>80     pub fn from_cff2(cff2: Cff2<'a>, units_per_em: u16) -> Result<Self, Error> {
81         let table_data = cff2.offset_data().as_bytes();
82         let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true)?;
83         Ok(Self {
84             version: Version::Version2(cff2),
85             top_dict,
86             units_per_em,
87         })
88     }
89 
is_cff2(&self) -> bool90     pub fn is_cff2(&self) -> bool {
91         matches!(self.version, Version::Version2(_))
92     }
93 
94     /// Returns the number of available glyphs.
glyph_count(&self) -> usize95     pub fn glyph_count(&self) -> usize {
96         self.top_dict
97             .charstrings
98             .as_ref()
99             .map(|cs| cs.count() as usize)
100             .unwrap_or_default()
101     }
102 
103     /// Returns the number of available subfonts.
subfont_count(&self) -> u32104     pub fn subfont_count(&self) -> u32 {
105         self.top_dict
106             .font_dicts
107             .as_ref()
108             .map(|font_dicts| font_dicts.count())
109             // All CFF fonts have at least one logical subfont.
110             .unwrap_or(1)
111     }
112 
113     /// Returns the subfont (or Font DICT) index for the given glyph
114     /// identifier.
subfont_index(&self, glyph_id: GlyphId) -> u32115     pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 {
116         // For CFF tables, an FDSelect index will be present for CID-keyed
117         // fonts. Otherwise, the Top DICT will contain an entry for the
118         // "global" Private DICT.
119         // See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=27>
120         //
121         // CFF2 tables always contain a Font DICT and an FDSelect is only
122         // present if the size of the DICT is greater than 1.
123         // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>
124         //
125         // In both cases, we return a subfont index of 0 when FDSelect is missing.
126         self.top_dict
127             .fd_select
128             .as_ref()
129             .and_then(|select| select.font_index(glyph_id))
130             .unwrap_or(0) as u32
131     }
132 
133     /// Creates a new subfont for the given index, size, normalized
134     /// variation coordinates and hinting state.
135     ///
136     /// The index of a subfont for a particular glyph can be retrieved with
137     /// the [`subfont_index`](Self::subfont_index) method.
subfont( &self, index: u32, size: Option<f32>, coords: &[F2Dot14], ) -> Result<Subfont, Error>138     pub fn subfont(
139         &self,
140         index: u32,
141         size: Option<f32>,
142         coords: &[F2Dot14],
143     ) -> Result<Subfont, Error> {
144         let private_dict_range = self.private_dict_range(index)?;
145         let private_dict_data = self.offset_data().read_array(private_dict_range.clone())?;
146         let mut hint_params = HintParams::default();
147         let mut subrs_offset = None;
148         let mut store_index = 0;
149         let blend_state = self
150             .top_dict
151             .var_store
152             .clone()
153             .map(|store| BlendState::new(store, coords, store_index))
154             .transpose()?;
155         for entry in dict::entries(private_dict_data, blend_state) {
156             use dict::Entry::*;
157             match entry? {
158                 BlueValues(values) => hint_params.blues = values,
159                 FamilyBlues(values) => hint_params.family_blues = values,
160                 OtherBlues(values) => hint_params.other_blues = values,
161                 FamilyOtherBlues(values) => hint_params.family_blues = values,
162                 BlueScale(value) => hint_params.blue_scale = value,
163                 BlueShift(value) => hint_params.blue_shift = value,
164                 BlueFuzz(value) => hint_params.blue_fuzz = value,
165                 LanguageGroup(group) => hint_params.language_group = group,
166                 // Subrs offset is relative to the private DICT
167                 SubrsOffset(offset) => subrs_offset = Some(private_dict_range.start + offset),
168                 VariationStoreIndex(index) => store_index = index,
169                 _ => {}
170             }
171         }
172         let scale = match size {
173             Some(ppem) if self.units_per_em > 0 => {
174                 // Note: we do an intermediate scale to 26.6 to ensure we
175                 // match FreeType
176                 Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(self.units_per_em as i32)
177             }
178             _ => Fixed::ONE,
179         };
180         // When hinting, use a modified scale factor
181         // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279>
182         let hint_scale = Fixed::from_bits((scale.to_bits() + 32) / 64);
183         let hint_state = HintState::new(&hint_params, hint_scale);
184         Ok(Subfont {
185             is_cff2: self.is_cff2(),
186             scale,
187             subrs_offset,
188             hint_state,
189             store_index,
190         })
191     }
192 
193     /// Loads and scales an outline for the given subfont instance, glyph
194     /// identifier and normalized variation coordinates.
195     ///
196     /// Before calling this method, use [`subfont_index`](Self::subfont_index)
197     /// to retrieve the subfont index for the desired glyph and then
198     /// [`subfont`](Self::subfont) to create an instance of the subfont for a
199     /// particular size and location in variation space.
200     /// Creating subfont instances is not free, so this process is exposed in
201     /// discrete steps to allow for caching.
202     ///
203     /// The result is emitted to the specified pen.
draw( &self, subfont: &Subfont, glyph_id: GlyphId, coords: &[F2Dot14], hint: bool, pen: &mut impl Pen, ) -> Result<(), Error>204     pub fn draw(
205         &self,
206         subfont: &Subfont,
207         glyph_id: GlyphId,
208         coords: &[F2Dot14],
209         hint: bool,
210         pen: &mut impl Pen,
211     ) -> Result<(), Error> {
212         let charstring_data = self
213             .top_dict
214             .charstrings
215             .as_ref()
216             .ok_or(Error::MissingCharstrings)?
217             .get(glyph_id.to_u16() as usize)?;
218         let subrs = subfont.subrs(self)?;
219         let blend_state = subfont.blend_state(self, coords)?;
220         let mut pen_sink = charstring::PenSink::new(pen);
221         let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink);
222         if hint {
223             let mut hinting_adapter =
224                 HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
225             charstring::evaluate(
226                 charstring_data,
227                 self.global_subrs(),
228                 subrs,
229                 blend_state,
230                 &mut hinting_adapter,
231             )?;
232             hinting_adapter.finish();
233         } else {
234             let mut scaling_adapter =
235                 ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale);
236             charstring::evaluate(
237                 charstring_data,
238                 self.global_subrs(),
239                 subrs,
240                 blend_state,
241                 &mut scaling_adapter,
242             )?;
243         }
244         simplifying_adapter.finish();
245         Ok(())
246     }
247 
offset_data(&self) -> FontData<'a>248     fn offset_data(&self) -> FontData<'a> {
249         match &self.version {
250             Version::Version1(cff1) => cff1.offset_data(),
251             Version::Version2(cff2) => cff2.offset_data(),
252         }
253     }
254 
global_subrs(&self) -> Index<'a>255     fn global_subrs(&self) -> Index<'a> {
256         match &self.version {
257             Version::Version1(cff1) => cff1.global_subrs().into(),
258             Version::Version2(cff2) => cff2.global_subrs().into(),
259         }
260     }
261 
private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error>262     fn private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error> {
263         if let Some(font_dicts) = &self.top_dict.font_dicts {
264             // If we have a font dict array, extract the private dict range
265             // from the font dict at the given index.
266             let font_dict_data = font_dicts.get(subfont_index as usize)?;
267             let mut range = None;
268             for entry in dict::entries(font_dict_data, None) {
269                 if let dict::Entry::PrivateDictRange(r) = entry? {
270                     range = Some(r);
271                     break;
272                 }
273             }
274             range
275         } else {
276             // Last chance, use the private dict range from the top dict if
277             // available.
278             self.top_dict.private_dict_range.clone()
279         }
280         .ok_or(Error::MissingPrivateDict)
281     }
282 }
283 
284 #[derive(Clone)]
285 enum Version<'a> {
286     /// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff>
287     Version1(Cff<'a>),
288     /// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2>
289     Version2(Cff2<'a>),
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)]
300 pub(crate) struct Subfont {
301     is_cff2: bool,
302     scale: Fixed,
303     subrs_offset: Option<usize>,
304     pub(crate) hint_state: HintState,
305     store_index: u16,
306 }
307 
308 impl Subfont {
309     /// Returns the local subroutine index.
subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error>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.
blend_state<'a>( &self, scaler: &Outlines<'a>, coords: &'a [F2Dot14], ) -> Result<Option<BlendState<'a>>, Error>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 Top DICT that are required to support
336 /// charstring evaluation.
337 #[derive(Clone, Default)]
338 struct TopDict<'a> {
339     charstrings: Option<Index<'a>>,
340     font_dicts: Option<Index<'a>>,
341     fd_select: Option<FdSelect<'a>>,
342     private_dict_range: Option<Range<usize>>,
343     var_store: Option<ItemVariationStore<'a>>,
344 }
345 
346 impl<'a> TopDict<'a> {
new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error>347     fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> {
348         let mut items = TopDict::default();
349         for entry in dict::entries(top_dict_data, None) {
350             match entry? {
351                 dict::Entry::CharstringsOffset(offset) => {
352                     items.charstrings = Some(Index::new(
353                         table_data.get(offset..).unwrap_or_default(),
354                         is_cff2,
355                     )?);
356                 }
357                 dict::Entry::FdArrayOffset(offset) => {
358                     items.font_dicts = Some(Index::new(
359                         table_data.get(offset..).unwrap_or_default(),
360                         is_cff2,
361                     )?);
362                 }
363                 dict::Entry::FdSelectOffset(offset) => {
364                     items.fd_select = Some(FdSelect::read(FontData::new(
365                         table_data.get(offset..).unwrap_or_default(),
366                     ))?);
367                 }
368                 dict::Entry::PrivateDictRange(range) => {
369                     items.private_dict_range = Some(range);
370                 }
371                 dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
372                     items.var_store = Some(ItemVariationStore::read(FontData::new(
373                         // IVS is preceded by a 2 byte length
374                         table_data.get(offset + 2..).unwrap_or_default(),
375                     ))?);
376                 }
377                 _ => {}
378             }
379         }
380         Ok(items)
381     }
382 }
383 
384 /// Command sink adapter that applies a scaling factor.
385 ///
386 /// This assumes a 26.6 scaling factor packed into a Fixed and thus,
387 /// this is not public and exists only to match FreeType's exact
388 /// scaling process.
389 struct ScalingSink26Dot6<'a, S> {
390     inner: &'a mut S,
391     scale: Fixed,
392 }
393 
394 impl<'a, S> ScalingSink26Dot6<'a, S> {
new(sink: &'a mut S, scale: Fixed) -> Self395     fn new(sink: &'a mut S, scale: Fixed) -> Self {
396         Self { scale, inner: sink }
397     }
398 
scale(&self, coord: Fixed) -> Fixed399     fn scale(&self, coord: Fixed) -> Fixed {
400         // The following dance is necessary to exactly match FreeType's
401         // application of scaling factors. This seems to be the result
402         // of merging the contributed Adobe code while not breaking the
403         // FreeType public API.
404         //
405         // The first two steps apply to both scaled and unscaled outlines:
406         //
407         // 1. Multiply by 1/64
408         // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284>
409         let a = coord * Fixed::from_bits(0x0400);
410         // 2. Truncate the bottom 10 bits. Combined with the division by 64,
411         // converts to font units.
412         // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219>
413         let b = Fixed::from_bits(a.to_bits() >> 10);
414         if self.scale != Fixed::ONE {
415             // Scaled case:
416             // 3. Multiply by the original scale factor (to 26.6)
417             // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721>
418             let c = b * self.scale;
419             // 4. Convert from 26.6 to 16.16
420             Fixed::from_bits(c.to_bits() << 10)
421         } else {
422             // Unscaled case:
423             // 3. Convert from integer to 16.16
424             Fixed::from_bits(b.to_bits() << 16)
425         }
426     }
427 }
428 
429 impl<'a, S: CommandSink> CommandSink for ScalingSink26Dot6<'a, S> {
hstem(&mut self, y: Fixed, dy: Fixed)430     fn hstem(&mut self, y: Fixed, dy: Fixed) {
431         self.inner.hstem(y, dy);
432     }
433 
vstem(&mut self, x: Fixed, dx: Fixed)434     fn vstem(&mut self, x: Fixed, dx: Fixed) {
435         self.inner.vstem(x, dx);
436     }
437 
hint_mask(&mut self, mask: &[u8])438     fn hint_mask(&mut self, mask: &[u8]) {
439         self.inner.hint_mask(mask);
440     }
441 
counter_mask(&mut self, mask: &[u8])442     fn counter_mask(&mut self, mask: &[u8]) {
443         self.inner.counter_mask(mask);
444     }
445 
move_to(&mut self, x: Fixed, y: Fixed)446     fn move_to(&mut self, x: Fixed, y: Fixed) {
447         self.inner.move_to(self.scale(x), self.scale(y));
448     }
449 
line_to(&mut self, x: Fixed, y: Fixed)450     fn line_to(&mut self, x: Fixed, y: Fixed) {
451         self.inner.line_to(self.scale(x), self.scale(y));
452     }
453 
curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed)454     fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
455         self.inner.curve_to(
456             self.scale(cx1),
457             self.scale(cy1),
458             self.scale(cx2),
459             self.scale(cy2),
460             self.scale(x),
461             self.scale(y),
462         );
463     }
464 
close(&mut self)465     fn close(&mut self) {
466         self.inner.close();
467     }
468 }
469 
470 /// Command sink adapter that supresses degenerate move and line commands.
471 ///
472 /// FreeType avoids emitting empty contours and zero length lines to prevent
473 /// artifacts when stem darkening is enabled. We don't support stem darkening
474 /// because it's not enabled by any of our clients but we remove the degenerate
475 /// elements regardless to match the output.
476 ///
477 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L1786>
478 struct NopFilteringSink<'a, S> {
479     start: Option<(Fixed, Fixed)>,
480     last: Option<(Fixed, Fixed)>,
481     pending_move: Option<(Fixed, Fixed)>,
482     inner: &'a mut S,
483 }
484 
485 impl<'a, S> NopFilteringSink<'a, S>
486 where
487     S: CommandSink,
488 {
new(inner: &'a mut S) -> Self489     fn new(inner: &'a mut S) -> Self {
490         Self {
491             start: None,
492             last: None,
493             pending_move: None,
494             inner,
495         }
496     }
497 
flush_pending_move(&mut self)498     fn flush_pending_move(&mut self) {
499         if let Some((x, y)) = self.pending_move.take() {
500             if let Some((last_x, last_y)) = self.start {
501                 if self.last != self.start {
502                     self.inner.line_to(last_x, last_y);
503                 }
504             }
505             self.start = Some((x, y));
506             self.last = None;
507             self.inner.move_to(x, y);
508         }
509     }
510 
finish(&mut self)511     pub fn finish(&mut self) {
512         match self.start {
513             Some((x, y)) if self.last != self.start => {
514                 self.inner.line_to(x, y);
515             }
516             _ => {}
517         }
518     }
519 }
520 
521 impl<'a, S> CommandSink for NopFilteringSink<'a, S>
522 where
523     S: CommandSink,
524 {
hstem(&mut self, y: Fixed, dy: Fixed)525     fn hstem(&mut self, y: Fixed, dy: Fixed) {
526         self.inner.hstem(y, dy);
527     }
528 
vstem(&mut self, x: Fixed, dx: Fixed)529     fn vstem(&mut self, x: Fixed, dx: Fixed) {
530         self.inner.vstem(x, dx);
531     }
532 
hint_mask(&mut self, mask: &[u8])533     fn hint_mask(&mut self, mask: &[u8]) {
534         self.inner.hint_mask(mask);
535     }
536 
counter_mask(&mut self, mask: &[u8])537     fn counter_mask(&mut self, mask: &[u8]) {
538         self.inner.counter_mask(mask);
539     }
540 
move_to(&mut self, x: Fixed, y: Fixed)541     fn move_to(&mut self, x: Fixed, y: Fixed) {
542         self.pending_move = Some((x, y));
543     }
544 
line_to(&mut self, x: Fixed, y: Fixed)545     fn line_to(&mut self, x: Fixed, y: Fixed) {
546         if self.pending_move == Some((x, y)) {
547             return;
548         }
549         self.flush_pending_move();
550         if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) {
551             return;
552         }
553         self.inner.line_to(x, y);
554         self.last = Some((x, y));
555     }
556 
curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed)557     fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
558         self.flush_pending_move();
559         self.last = Some((x, y));
560         self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
561     }
562 
close(&mut self)563     fn close(&mut self) {
564         if self.pending_move.is_none() {
565             self.inner.close();
566             self.start = None;
567             self.last = None;
568         }
569     }
570 }
571 
572 #[cfg(test)]
573 mod tests {
574     use super::*;
575     use read_fonts::FontRef;
576 
577     #[test]
unscaled_scaling_sink_produces_integers()578     fn unscaled_scaling_sink_produces_integers() {
579         let nothing = &mut ();
580         let sink = ScalingSink26Dot6::new(nothing, Fixed::ONE);
581         for coord in [50.0, 50.1, 50.125, 50.5, 50.9] {
582             assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0);
583         }
584     }
585 
586     #[test]
scaled_scaling_sink()587     fn scaled_scaling_sink() {
588         let ppem = 20.0;
589         let upem = 1000.0;
590         // match FreeType scaling with intermediate conversion to 26.6
591         let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32);
592         let nothing = &mut ();
593         let sink = ScalingSink26Dot6::new(nothing, scale);
594         let inputs = [
595             // input coord, expected scaled output
596             (0.0, 0.0),
597             (8.0, 0.15625),
598             (16.0, 0.3125),
599             (32.0, 0.640625),
600             (72.0, 1.4375),
601             (128.0, 2.5625),
602         ];
603         for (coord, expected) in inputs {
604             assert_eq!(
605                 sink.scale(Fixed::from_f64(coord)).to_f32(),
606                 expected,
607                 "scaling coord {coord}"
608             );
609         }
610     }
611 
612     #[test]
read_cff_static()613     fn read_cff_static() {
614         let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
615         let cff = Outlines::new(&font).unwrap();
616         assert!(!cff.is_cff2());
617         assert!(cff.top_dict.var_store.is_none());
618         assert!(cff.top_dict.font_dicts.is_none());
619         assert!(cff.top_dict.private_dict_range.is_some());
620         assert!(cff.top_dict.fd_select.is_none());
621         assert_eq!(cff.subfont_count(), 1);
622         assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
623         assert_eq!(cff.global_subrs().count(), 17);
624     }
625 
626     #[test]
read_cff2_static()627     fn read_cff2_static() {
628         let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
629         let cff = Outlines::new(&font).unwrap();
630         assert!(cff.is_cff2());
631         assert!(cff.top_dict.var_store.is_some());
632         assert!(cff.top_dict.font_dicts.is_some());
633         assert!(cff.top_dict.private_dict_range.is_none());
634         assert!(cff.top_dict.fd_select.is_none());
635         assert_eq!(cff.subfont_count(), 1);
636         assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
637         assert_eq!(cff.global_subrs().count(), 0);
638     }
639 
640     #[test]
read_example_cff2_table()641     fn read_example_cff2_table() {
642         let cff = Outlines::from_cff2(
643             Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap(),
644             1000,
645         )
646         .unwrap();
647         assert!(cff.is_cff2());
648         assert!(cff.top_dict.var_store.is_some());
649         assert!(cff.top_dict.font_dicts.is_some());
650         assert!(cff.top_dict.private_dict_range.is_none());
651         assert!(cff.top_dict.fd_select.is_none());
652         assert_eq!(cff.subfont_count(), 1);
653         assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
654         assert_eq!(cff.global_subrs().count(), 0);
655     }
656 
657     #[test]
cff2_variable_outlines_match_freetype()658     fn cff2_variable_outlines_match_freetype() {
659         compare_glyphs(
660             font_test_data::CANTARELL_VF_TRIMMED,
661             font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
662         );
663     }
664 
665     #[test]
cff_static_outlines_match_freetype()666     fn cff_static_outlines_match_freetype() {
667         compare_glyphs(
668             font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
669             font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
670         );
671     }
672 
673     /// For the given font data and extracted outlines, parse the extracted
674     /// outline data into a set of expected values and compare these with the
675     /// results generated by the scaler.
676     ///
677     /// This will compare all outlines at various sizes and (for variable
678     /// fonts), locations in variation space.
compare_glyphs(font_data: &[u8], expected_outlines: &str)679     fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
680         let font = FontRef::new(font_data).unwrap();
681         let expected_outlines = read_fonts::scaler_test::parse_glyph_outlines(expected_outlines);
682         let outlines = super::Outlines::new(&font).unwrap();
683         let mut path = read_fonts::scaler_test::Path::default();
684         for expected_outline in &expected_outlines {
685             if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
686                 continue;
687             }
688             let size = (expected_outline.size != 0.0).then_some(expected_outline.size);
689             path.elements.clear();
690             let subfont = outlines
691                 .subfont(
692                     outlines.subfont_index(expected_outline.glyph_id),
693                     size,
694                     &expected_outline.coords,
695                 )
696                 .unwrap();
697             outlines
698                 .draw(
699                     &subfont,
700                     expected_outline.glyph_id,
701                     &expected_outline.coords,
702                     false,
703                     &mut path,
704                 )
705                 .unwrap();
706             if path.elements != expected_outline.path {
707                 panic!(
708                     "mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
709                     expected_outline.glyph_id,
710                     expected_outline.size,
711                     expected_outline.coords,
712                     &path.elements,
713                     &expected_outline.path
714                 );
715             }
716         }
717     }
718 }
719