1mod 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#[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 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 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 pub fn glyph_count(&self) -> usize {
106 self.top_dict.charstrings.count() as usize
107 }
108
109 pub fn subfont_count(&self) -> u32 {
111 self.top_dict.font_dicts.count().max(1)
113 }
114
115 pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 {
118 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 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 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 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 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 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 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 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#[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 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 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#[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 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#[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 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
390struct 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
428struct 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 let a = coord * Fixed::from_bits(0x0400);
454 let b = Fixed::from_bits(a.to_bits() >> 10);
458 if let Some(scale) = self.scale {
459 let c = b * scale;
463 Fixed::from_bits(c.to_bits() << 10)
465 } else {
466 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
514struct 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 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 (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 #[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 #[test]
758 fn subrs_offset_overflow() {
759 let private_dict = BeBuffer::new()
761 .push(0u32) .push(29u8) .push(-1i32) .push(19u8) .to_vec();
766 assert!(
768 PrivateDict::new(FontData::new(&private_dict), 4..private_dict.len(), None).is_err()
769 );
770 }
771
772 #[test]
776 fn top_dict_ivs_offset_overflow() {
777 let top_dict = BeBuffer::new()
780 .push(29u8) .push(-1i32) .push(24u8) .to_vec();
784 assert!(TopDict::new(&[], &top_dict, true).is_err());
786 }
787
788 #[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 assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
806 }
807
808 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 #[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}