read_fonts/tables/postscript/
dict.rs

1//! Parsing for PostScript DICTs.
2
3use std::ops::Range;
4
5use super::{BlendState, Error, Number, Stack, StringId};
6use crate::{types::Fixed, Cursor, ReadError};
7
8/// PostScript DICT operator.
9///
10/// See "Table 9 Top DICT Operator Entries" and "Table 23 Private DICT
11/// Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf>
12#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
13pub enum Operator {
14    Version,
15    Notice,
16    FullName,
17    FamilyName,
18    Weight,
19    FontBbox,
20    CharstringsOffset,
21    PrivateDictRange,
22    VariationStoreOffset,
23    Copyright,
24    IsFixedPitch,
25    ItalicAngle,
26    UnderlinePosition,
27    UnderlineThickness,
28    PaintType,
29    CharstringType,
30    FontMatrix,
31    StrokeWidth,
32    FdArrayOffset,
33    FdSelectOffset,
34    BlueValues,
35    OtherBlues,
36    FamilyBlues,
37    FamilyOtherBlues,
38    SubrsOffset,
39    VariationStoreIndex,
40    BlueScale,
41    BlueShift,
42    BlueFuzz,
43    LanguageGroup,
44    ExpansionFactor,
45    Encoding,
46    Charset,
47    UniqueId,
48    Xuid,
49    SyntheticBase,
50    PostScript,
51    BaseFontName,
52    BaseFontBlend,
53    Ros,
54    CidFontVersion,
55    CidFontRevision,
56    CidFontType,
57    CidCount,
58    UidBase,
59    FontName,
60    StdHw,
61    StdVw,
62    DefaultWidthX,
63    NominalWidthX,
64    Blend,
65    StemSnapH,
66    StemSnapV,
67    ForceBold,
68    InitialRandomSeed,
69}
70
71impl Operator {
72    fn from_opcode(opcode: u8) -> Option<Self> {
73        use Operator::*;
74        Some(match opcode {
75            // Top DICT operators
76            0 => Version,
77            1 => Notice,
78            2 => FullName,
79            3 => FamilyName,
80            4 => Weight,
81            5 => FontBbox,
82            13 => UniqueId,
83            14 => Xuid,
84            15 => Charset,
85            16 => Encoding,
86            17 => CharstringsOffset,
87            18 => PrivateDictRange,
88            24 => VariationStoreOffset,
89            // Private DICT operators
90            6 => BlueValues,
91            7 => OtherBlues,
92            8 => FamilyBlues,
93            9 => FamilyOtherBlues,
94            10 => StdHw,
95            11 => StdVw,
96            19 => SubrsOffset,
97            20 => DefaultWidthX,
98            21 => NominalWidthX,
99            22 => VariationStoreIndex,
100            23 => Blend,
101            // Font DICT only uses PrivateDictRange
102            _ => return None,
103        })
104    }
105
106    fn from_extended_opcode(opcode: u8) -> Option<Self> {
107        use Operator::*;
108        Some(match opcode {
109            // Top DICT operators
110            0 => Copyright,
111            1 => IsFixedPitch,
112            2 => ItalicAngle,
113            3 => UnderlinePosition,
114            4 => UnderlineThickness,
115            5 => PaintType,
116            6 => CharstringType,
117            7 => FontMatrix,
118            8 => StrokeWidth,
119            20 => SyntheticBase,
120            21 => PostScript,
121            22 => BaseFontName,
122            23 => BaseFontBlend,
123            30 => Ros,
124            31 => CidFontVersion,
125            32 => CidFontRevision,
126            33 => CidFontType,
127            34 => CidCount,
128            35 => UidBase,
129            36 => FdArrayOffset,
130            37 => FdSelectOffset,
131            38 => FontName,
132            // Private DICT operators
133            9 => BlueScale,
134            10 => BlueShift,
135            11 => BlueFuzz,
136            12 => StemSnapH,
137            13 => StemSnapV,
138            14 => ForceBold,
139            17 => LanguageGroup,
140            18 => ExpansionFactor,
141            19 => InitialRandomSeed,
142            _ => return None,
143        })
144    }
145}
146
147/// Either a PostScript DICT operator or a (numeric) operand.
148#[derive(Copy, Clone, PartialEq, Eq, Debug)]
149pub enum Token {
150    Operator(Operator),
151    Operand(Number),
152}
153
154impl From<Operator> for Token {
155    fn from(value: Operator) -> Self {
156        Self::Operator(value)
157    }
158}
159
160impl<T> From<T> for Token
161where
162    T: Into<Number>,
163{
164    fn from(value: T) -> Self {
165        Self::Operand(value.into())
166    }
167}
168
169/// Given a byte slice containing DICT data, returns an iterator yielding
170/// raw operands and operators.
171///
172/// This does not perform any additional processing such as type conversion,
173/// delta decoding or blending.
174pub fn tokens(dict_data: &[u8]) -> impl Iterator<Item = Result<Token, Error>> + '_ + Clone {
175    let mut cursor = crate::FontData::new(dict_data).cursor();
176    std::iter::from_fn(move || {
177        if cursor.remaining_bytes() == 0 {
178            None
179        } else {
180            Some(parse_token(&mut cursor))
181        }
182    })
183}
184
185fn parse_token(cursor: &mut Cursor) -> Result<Token, Error> {
186    // Escape opcode for accessing extensions.
187    const ESCAPE: u8 = 12;
188    let b0 = cursor.read::<u8>()?;
189    Ok(if b0 == ESCAPE {
190        let b1 = cursor.read::<u8>()?;
191        Token::Operator(Operator::from_extended_opcode(b1).ok_or(Error::InvalidDictOperator(b1))?)
192    } else {
193        // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-3-operand-encoding>
194        match b0 {
195            28 | 29 | 32..=254 => Token::Operand(parse_int(cursor, b0)?.into()),
196            30 => Token::Operand(parse_bcd(cursor)?.into()),
197            _ => Token::Operator(Operator::from_opcode(b0).ok_or(Error::InvalidDictOperator(b0))?),
198        }
199    })
200}
201
202/// PostScript DICT Operator with its associated operands.
203#[derive(Clone, PartialEq, Eq, Debug)]
204pub enum Entry {
205    Version(StringId),
206    Notice(StringId),
207    FullName(StringId),
208    FamilyName(StringId),
209    Weight(StringId),
210    FontBbox([Fixed; 4]),
211    CharstringsOffset(usize),
212    PrivateDictRange(Range<usize>),
213    VariationStoreOffset(usize),
214    Copyright(StringId),
215    IsFixedPitch(bool),
216    ItalicAngle(Fixed),
217    UnderlinePosition(Fixed),
218    UnderlineThickness(Fixed),
219    PaintType(i32),
220    CharstringType(i32),
221    FontMatrix([Fixed; 6]),
222    StrokeWidth(Fixed),
223    FdArrayOffset(usize),
224    FdSelectOffset(usize),
225    BlueValues(Blues),
226    OtherBlues(Blues),
227    FamilyBlues(Blues),
228    FamilyOtherBlues(Blues),
229    SubrsOffset(usize),
230    VariationStoreIndex(u16),
231    BlueScale(Fixed),
232    BlueShift(Fixed),
233    BlueFuzz(Fixed),
234    LanguageGroup(i32),
235    ExpansionFactor(Fixed),
236    Encoding(usize),
237    Charset(usize),
238    UniqueId(i32),
239    Xuid,
240    SyntheticBase(i32),
241    PostScript(StringId),
242    BaseFontName(StringId),
243    BaseFontBlend,
244    Ros {
245        registry: StringId,
246        ordering: StringId,
247        supplement: Fixed,
248    },
249    CidFontVersion(Fixed),
250    CidFontRevision(Fixed),
251    CidFontType(i32),
252    CidCount(u32),
253    UidBase(i32),
254    FontName(StringId),
255    StdHw(Fixed),
256    StdVw(Fixed),
257    DefaultWidthX(Fixed),
258    NominalWidthX(Fixed),
259    StemSnapH(StemSnaps),
260    StemSnapV(StemSnaps),
261    ForceBold(bool),
262    InitialRandomSeed(i32),
263}
264
265/// Given a byte slice containing DICT data, returns an iterator yielding
266/// each operator with its associated operands.
267///
268/// This performs appropriate type conversions, decodes deltas and applies
269/// blending.
270///
271/// If processing a Private DICT from a CFF2 table and an item variation
272/// store is present, then `blend_state` must be provided.
273pub fn entries<'a>(
274    dict_data: &'a [u8],
275    mut blend_state: Option<BlendState<'a>>,
276) -> impl Iterator<Item = Result<Entry, Error>> + 'a {
277    let mut stack = Stack::new();
278    let mut token_iter = tokens(dict_data);
279    std::iter::from_fn(move || loop {
280        let token = match token_iter.next()? {
281            Ok(token) => token,
282            Err(e) => return Some(Err(e)),
283        };
284        match token {
285            Token::Operand(number) => match stack.push(number) {
286                Ok(_) => continue,
287                Err(e) => return Some(Err(e)),
288            },
289            Token::Operator(op) => {
290                if op == Operator::Blend || op == Operator::VariationStoreIndex {
291                    let state = match blend_state.as_mut() {
292                        Some(state) => state,
293                        None => return Some(Err(Error::MissingBlendState)),
294                    };
295                    if op == Operator::VariationStoreIndex {
296                        match stack
297                            .get_i32(0)
298                            .and_then(|ix| state.set_store_index(ix as u16))
299                        {
300                            Ok(_) => {}
301                            Err(e) => return Some(Err(e)),
302                        }
303                    }
304                    if op == Operator::Blend {
305                        match stack.apply_blend(state) {
306                            Ok(_) => continue,
307                            Err(e) => return Some(Err(e)),
308                        }
309                    }
310                }
311                let entry = parse_entry(op, &mut stack);
312                stack.clear();
313                return Some(entry);
314            }
315        }
316    })
317}
318
319fn parse_entry(op: Operator, stack: &mut Stack) -> Result<Entry, Error> {
320    use Operator::*;
321    Ok(match op {
322        Version => Entry::Version(stack.pop_i32()?.into()),
323        Notice => Entry::Notice(stack.pop_i32()?.into()),
324        FullName => Entry::FullName(stack.pop_i32()?.into()),
325        FamilyName => Entry::FamilyName(stack.pop_i32()?.into()),
326        Weight => Entry::Weight(stack.pop_i32()?.into()),
327        FontBbox => Entry::FontBbox([
328            stack.get_fixed(0)?,
329            stack.get_fixed(1)?,
330            stack.get_fixed(2)?,
331            stack.get_fixed(3)?,
332        ]),
333        CharstringsOffset => Entry::CharstringsOffset(stack.pop_i32()? as usize),
334        PrivateDictRange => {
335            let len = stack.get_i32(0)? as usize;
336            let start = stack.get_i32(1)? as usize;
337            let end = start.checked_add(len).ok_or(ReadError::OutOfBounds)?;
338            Entry::PrivateDictRange(start..end)
339        }
340        VariationStoreOffset => Entry::VariationStoreOffset(stack.pop_i32()? as usize),
341        Copyright => Entry::Copyright(stack.pop_i32()?.into()),
342        IsFixedPitch => Entry::IsFixedPitch(stack.pop_i32()? != 0),
343        ItalicAngle => Entry::ItalicAngle(stack.pop_fixed()?),
344        UnderlinePosition => Entry::UnderlinePosition(stack.pop_fixed()?),
345        UnderlineThickness => Entry::UnderlineThickness(stack.pop_fixed()?),
346        PaintType => Entry::PaintType(stack.pop_i32()?),
347        CharstringType => Entry::CharstringType(stack.pop_i32()?),
348        FontMatrix => Entry::FontMatrix([
349            stack.get_fixed(0)?,
350            stack.get_fixed(1)?,
351            stack.get_fixed(2)?,
352            stack.get_fixed(3)?,
353            stack.get_fixed(4)?,
354            stack.get_fixed(5)?,
355        ]),
356        StrokeWidth => Entry::StrokeWidth(stack.pop_fixed()?),
357        FdArrayOffset => Entry::FdArrayOffset(stack.pop_i32()? as usize),
358        FdSelectOffset => Entry::FdSelectOffset(stack.pop_i32()? as usize),
359        BlueValues => {
360            stack.apply_delta_prefix_sum();
361            Entry::BlueValues(Blues::new(stack.fixed_values()))
362        }
363        OtherBlues => {
364            stack.apply_delta_prefix_sum();
365            Entry::OtherBlues(Blues::new(stack.fixed_values()))
366        }
367        FamilyBlues => {
368            stack.apply_delta_prefix_sum();
369            Entry::FamilyBlues(Blues::new(stack.fixed_values()))
370        }
371        FamilyOtherBlues => {
372            stack.apply_delta_prefix_sum();
373            Entry::FamilyOtherBlues(Blues::new(stack.fixed_values()))
374        }
375        SubrsOffset => Entry::SubrsOffset(stack.pop_i32()? as usize),
376        VariationStoreIndex => Entry::VariationStoreIndex(stack.pop_i32()? as u16),
377        BlueScale => Entry::BlueScale(stack.pop_fixed()?),
378        BlueShift => Entry::BlueShift(stack.pop_fixed()?),
379        BlueFuzz => Entry::BlueFuzz(stack.pop_fixed()?),
380        LanguageGroup => Entry::LanguageGroup(stack.pop_i32()?),
381        ExpansionFactor => Entry::ExpansionFactor(stack.pop_fixed()?),
382        Encoding => Entry::Encoding(stack.pop_i32()? as usize),
383        Charset => Entry::Charset(stack.pop_i32()? as usize),
384        UniqueId => Entry::UniqueId(stack.pop_i32()?),
385        Xuid => Entry::Xuid,
386        SyntheticBase => Entry::SyntheticBase(stack.pop_i32()?),
387        PostScript => Entry::PostScript(stack.pop_i32()?.into()),
388        BaseFontName => Entry::BaseFontName(stack.pop_i32()?.into()),
389        BaseFontBlend => Entry::BaseFontBlend,
390        Ros => Entry::Ros {
391            registry: stack.get_i32(0)?.into(),
392            ordering: stack.get_i32(1)?.into(),
393            supplement: stack.get_fixed(2)?,
394        },
395        CidFontVersion => Entry::CidFontVersion(stack.pop_fixed()?),
396        CidFontRevision => Entry::CidFontRevision(stack.pop_fixed()?),
397        CidFontType => Entry::CidFontType(stack.pop_i32()?),
398        CidCount => Entry::CidCount(stack.pop_i32()? as u32),
399        UidBase => Entry::UidBase(stack.pop_i32()?),
400        FontName => Entry::FontName(stack.pop_i32()?.into()),
401        StdHw => Entry::StdHw(stack.pop_fixed()?),
402        StdVw => Entry::StdVw(stack.pop_fixed()?),
403        DefaultWidthX => Entry::DefaultWidthX(stack.pop_fixed()?),
404        NominalWidthX => Entry::NominalWidthX(stack.pop_fixed()?),
405        StemSnapH => {
406            stack.apply_delta_prefix_sum();
407            Entry::StemSnapH(StemSnaps::new(stack.fixed_values()))
408        }
409        StemSnapV => {
410            stack.apply_delta_prefix_sum();
411            Entry::StemSnapV(StemSnaps::new(stack.fixed_values()))
412        }
413        ForceBold => Entry::ForceBold(stack.pop_i32()? != 0),
414        InitialRandomSeed => Entry::InitialRandomSeed(stack.pop_i32()?),
415        // Blend is handled at the layer above
416        Blend => unreachable!(),
417    })
418}
419
420/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L141>
421const MAX_BLUE_VALUES: usize = 7;
422
423/// Operand for the `BlueValues`, `OtherBlues`, `FamilyBlues` and
424/// `FamilyOtherBlues` operators.
425///
426/// These are used to generate zones when applying hints.
427#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
428pub struct Blues {
429    values: [(Fixed, Fixed); MAX_BLUE_VALUES],
430    len: u32,
431}
432
433impl Blues {
434    pub fn new(values: impl Iterator<Item = Fixed>) -> Self {
435        let mut blues = Self::default();
436        let mut stash = Fixed::ZERO;
437        for (i, value) in values.take(MAX_BLUE_VALUES * 2).enumerate() {
438            if (i & 1) == 0 {
439                stash = value;
440            } else {
441                blues.values[i / 2] = (stash, value);
442                blues.len += 1;
443            }
444        }
445        blues
446    }
447
448    pub fn values(&self) -> &[(Fixed, Fixed)] {
449        &self.values[..self.len as usize]
450    }
451}
452
453/// Summary: older PostScript interpreters accept two values, but newer ones
454/// accept 12. We'll assume that as maximum.
455/// <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5049.StemSnap.pdf>
456const MAX_STEM_SNAPS: usize = 12;
457
458/// Operand for the `StemSnapH` and `StemSnapV` operators.
459///
460/// These are used for stem darkening when applying hints.
461#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
462pub struct StemSnaps {
463    values: [Fixed; MAX_STEM_SNAPS],
464    len: u32,
465}
466
467impl StemSnaps {
468    fn new(values: impl Iterator<Item = Fixed>) -> Self {
469        let mut snaps = Self::default();
470        for (value, target_value) in values.take(MAX_STEM_SNAPS).zip(&mut snaps.values) {
471            *target_value = value;
472            snaps.len += 1;
473        }
474        snaps
475    }
476
477    pub fn values(&self) -> &[Fixed] {
478        &self.values[..self.len as usize]
479    }
480}
481
482pub(crate) fn parse_int(cursor: &mut Cursor, b0: u8) -> Result<i32, Error> {
483    // Size   b0 range     Value range              Value calculation
484    //--------------------------------------------------------------------------------
485    // 1      32 to 246    -107 to +107             b0 - 139
486    // 2      247 to 250   +108 to +1131            (b0 - 247) * 256 + b1 + 108
487    // 2      251 to 254   -1131 to -108            -(b0 - 251) * 256 - b1 - 108
488    // 3      28           -32768 to +32767         b1 << 8 | b2
489    // 5      29           -(2^31) to +(2^31 - 1)   b1 << 24 | b2 << 16 | b3 << 8 | b4
490    // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-3-operand-encoding>
491    Ok(match b0 {
492        32..=246 => b0 as i32 - 139,
493        247..=250 => (b0 as i32 - 247) * 256 + cursor.read::<u8>()? as i32 + 108,
494        251..=254 => -(b0 as i32 - 251) * 256 - cursor.read::<u8>()? as i32 - 108,
495        28 => cursor.read::<i16>()? as i32,
496        29 => cursor.read::<i32>()?,
497        _ => {
498            return Err(Error::InvalidNumber);
499        }
500    })
501}
502
503/// Parse a binary coded decimal number.
504fn parse_bcd(cursor: &mut Cursor) -> Result<Fixed, Error> {
505    // fonttools says:
506    // "Note: 14 decimal digits seems to be the limitation for CFF real numbers
507    // in macOS. However, we use 8 here to match the implementation of AFDKO."
508    // <https://github.com/fonttools/fonttools/blob/84cebca6a1709085b920783400ceb1a147d51842/Lib/fontTools/misc/psCharStrings.py#L269>
509    // So, 32 should be big enough for anybody?
510    const MAX_LEN: usize = 32;
511    let mut buf = [0u8; MAX_LEN];
512    let mut n = 0;
513    let mut push = |byte| {
514        if n < MAX_LEN {
515            buf[n] = byte;
516            n += 1;
517            Ok(())
518        } else {
519            Err(Error::InvalidNumber)
520        }
521    };
522    // Nibble value    Represents
523    //----------------------------------
524    // 0 to 9          0 to 9
525    // a               . (decimal point)
526    // b               E
527    // c               E-
528    // d               <reserved>
529    // e               - (minus)
530    // f               end of number
531    // <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-5-nibble-definitions>
532    'outer: loop {
533        let b = cursor.read::<u8>()?;
534        for nibble in [(b >> 4) & 0xF, b & 0xF] {
535            match nibble {
536                0x0..=0x9 => push(b'0' + nibble)?,
537                0xA => push(b'.')?,
538                0xB => push(b'E')?,
539                0xC => {
540                    push(b'E')?;
541                    push(b'-')?;
542                }
543                0xE => push(b'-')?,
544                0xF => break 'outer,
545                _ => return Err(Error::InvalidNumber),
546            }
547        }
548    }
549    std::str::from_utf8(&buf[..n])
550        .map_or(None, |buf| buf.parse::<f64>().ok())
551        .map(Fixed::from_f64)
552        .ok_or(Error::InvalidNumber)
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use crate::{
559        tables::variations::ItemVariationStore, test_helpers::BeBuffer, types::F2Dot14, FontData,
560        FontRead, FontRef, TableProvider,
561    };
562
563    #[test]
564    fn int_operands() {
565        // Test the boundary conditions of the ranged int operators
566        let empty = FontData::new(&[]);
567        let min_byte = FontData::new(&[0]);
568        let max_byte = FontData::new(&[255]);
569        // 32..=246 => -107..=107
570        assert_eq!(parse_int(&mut empty.cursor(), 32).unwrap(), -107);
571        assert_eq!(parse_int(&mut empty.cursor(), 246).unwrap(), 107);
572        // 247..=250 => +108 to +1131
573        assert_eq!(parse_int(&mut min_byte.cursor(), 247).unwrap(), 108);
574        assert_eq!(parse_int(&mut max_byte.cursor(), 250).unwrap(), 1131);
575        // 251..=254 => -1131 to -108
576        assert_eq!(parse_int(&mut min_byte.cursor(), 251).unwrap(), -108);
577        assert_eq!(parse_int(&mut max_byte.cursor(), 254).unwrap(), -1131);
578    }
579
580    #[test]
581    fn binary_coded_decimal_operands() {
582        // From <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-5-nibble-definitions>:
583        //
584        // "A real number is terminated by one (or two) 0xf nibbles so that it is always padded
585        // to a full byte. Thus, the value -2.25 is encoded by the byte sequence (1e e2 a2 5f)
586        // and the value 0.140541E-3 by the sequence (1e 0a 14 05 41 c3 ff)."
587        //
588        // The initial 1e byte in the examples above is the dictionary operator to trigger
589        // parsing of BCD so it is dropped in the tests here.
590        let bytes = FontData::new(&[0xe2, 0xa2, 0x5f]);
591        assert_eq!(
592            parse_bcd(&mut bytes.cursor()).unwrap(),
593            Fixed::from_f64(-2.25)
594        );
595        let bytes = FontData::new(&[0x0a, 0x14, 0x05, 0x41, 0xc3, 0xff]);
596        assert_eq!(
597            parse_bcd(&mut bytes.cursor()).unwrap(),
598            Fixed::from_f64(0.140541E-3)
599        );
600    }
601
602    #[test]
603    fn example_top_dict_tokens() {
604        use Operator::*;
605        let top_dict_data = &font_test_data::cff2::EXAMPLE[5..12];
606        let tokens: Vec<_> = tokens(top_dict_data).map(|entry| entry.unwrap()).collect();
607        let expected: &[Token] = &[
608            68.into(),
609            FdArrayOffset.into(),
610            56.into(),
611            CharstringsOffset.into(),
612            16.into(),
613            VariationStoreOffset.into(),
614        ];
615        assert_eq!(&tokens, expected);
616    }
617
618    #[test]
619    fn example_top_dict_entries() {
620        use Entry::*;
621        let top_dict_data = &font_test_data::cff2::EXAMPLE[0x5..=0xB];
622        let entries: Vec<_> = entries(top_dict_data, None)
623            .map(|entry| entry.unwrap())
624            .collect();
625        let expected: &[Entry] = &[
626            FdArrayOffset(68),
627            CharstringsOffset(56),
628            VariationStoreOffset(16),
629        ];
630        assert_eq!(&entries, expected);
631    }
632
633    #[test]
634    fn example_private_dict_entries() {
635        use Entry::*;
636        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
637        let store =
638            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
639        let coords = &[F2Dot14::from_f32(0.0)];
640        let blend_state = BlendState::new(store, coords, 0).unwrap();
641        let entries: Vec<_> = entries(private_dict_data, Some(blend_state))
642            .map(|entry| entry.unwrap())
643            .collect();
644        fn make_blues(values: &[f64]) -> Blues {
645            Blues::new(values.iter().copied().map(Fixed::from_f64))
646        }
647        fn make_stem_snaps(values: &[f64]) -> StemSnaps {
648            StemSnaps::new(values.iter().copied().map(Fixed::from_f64))
649        }
650        let expected: &[Entry] = &[
651            BlueValues(make_blues(&[
652                -20.0, 0.0, 472.0, 490.0, 525.0, 540.0, 645.0, 660.0, 670.0, 690.0, 730.0, 750.0,
653            ])),
654            OtherBlues(make_blues(&[-250.0, -240.0])),
655            FamilyBlues(make_blues(&[
656                -20.0, 0.0, 473.0, 491.0, 525.0, 540.0, 644.0, 659.0, 669.0, 689.0, 729.0, 749.0,
657            ])),
658            FamilyOtherBlues(make_blues(&[-249.0, -239.0])),
659            BlueScale(Fixed::from_f64(0.037506103515625)),
660            BlueFuzz(Fixed::ZERO),
661            StdHw(Fixed::from_f64(55.0)),
662            StdVw(Fixed::from_f64(80.0)),
663            StemSnapH(make_stem_snaps(&[40.0, 55.0])),
664            StemSnapV(make_stem_snaps(&[80.0, 90.0])),
665            SubrsOffset(114),
666        ];
667        assert_eq!(&entries, expected);
668    }
669
670    #[test]
671    fn noto_serif_display_top_dict_entries() {
672        use Entry::*;
673        let top_dict_data = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED)
674            .unwrap()
675            .cff()
676            .unwrap()
677            .top_dicts()
678            .get(0)
679            .unwrap();
680        let entries: Vec<_> = entries(top_dict_data, None)
681            .map(|entry| entry.unwrap())
682            .collect();
683        let expected = &[
684            Version(StringId::new(391)),
685            Notice(StringId::new(392)),
686            Copyright(StringId::new(393)),
687            FullName(StringId::new(394)),
688            FamilyName(StringId::new(395)),
689            FontBbox([-693.0, -470.0, 2797.0, 1048.0].map(Fixed::from_f64)),
690            Charset(517),
691            PrivateDictRange(549..587),
692            CharstringsOffset(521),
693        ];
694        assert_eq!(&entries, expected);
695    }
696
697    // Fuzzer caught add with overflow when constructing private DICT
698    // range.
699    // See <https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=71746>
700    // and <https://oss-fuzz.com/testcase?key=4591358306746368>
701    #[test]
702    fn private_dict_range_avoid_overflow() {
703        // A Private DICT that tries to construct a range from -1..(-1 + -1)
704        // which overflows when converted to usize
705        let private_dict = BeBuffer::new()
706            .push(29u8) // integer operator
707            .push(-1i32) // integer value
708            .push(29u8) // integer operator
709            .push(-1i32) // integer value
710            .push(18u8) // PrivateDICT operator
711            .to_vec();
712        // Just don't panic
713        let _ = entries(&private_dict, None).count();
714    }
715}