Skip to main content

read_fonts/ps/cff/
dict.rs

1//! Parsing for PostScript DICTs.
2
3use super::{
4    blend::BlendState,
5    stack::{Number, Stack},
6};
7use crate::model::Error;
8use crate::{
9    ps::{
10        hinting::{Blues, StemSnaps},
11        num::{self, BcdComponents},
12        string::Sid,
13        transform::ScaledFontMatrix,
14    },
15    types::Fixed,
16    Cursor,
17};
18use std::ops::Range;
19
20/// PostScript DICT operator.
21///
22/// See "Table 9 Top DICT Operator Entries" and "Table 23 Private DICT
23/// Operators" at <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf>
24#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
25pub enum Operator {
26    Version,
27    Notice,
28    FullName,
29    FamilyName,
30    Weight,
31    FontBbox,
32    CharstringsOffset,
33    PrivateDictRange,
34    VariationStoreOffset,
35    Copyright,
36    IsFixedPitch,
37    ItalicAngle,
38    UnderlinePosition,
39    UnderlineThickness,
40    PaintType,
41    CharstringType,
42    FontMatrix,
43    StrokeWidth,
44    FdArrayOffset,
45    FdSelectOffset,
46    BlueValues,
47    OtherBlues,
48    FamilyBlues,
49    FamilyOtherBlues,
50    SubrsOffset,
51    VariationStoreIndex,
52    BlueScale,
53    BlueShift,
54    BlueFuzz,
55    LanguageGroup,
56    ExpansionFactor,
57    Encoding,
58    Charset,
59    UniqueId,
60    Xuid,
61    SyntheticBase,
62    PostScript,
63    BaseFontName,
64    BaseFontBlend,
65    Ros,
66    CidFontVersion,
67    CidFontRevision,
68    CidFontType,
69    CidCount,
70    UidBase,
71    FontName,
72    StdHw,
73    StdVw,
74    DefaultWidthX,
75    NominalWidthX,
76    Blend,
77    StemSnapH,
78    StemSnapV,
79    ForceBold,
80    InitialRandomSeed,
81}
82
83impl Operator {
84    fn from_opcode(opcode: u8) -> Option<Self> {
85        use Operator::*;
86        Some(match opcode {
87            // Top DICT operators
88            0 => Version,
89            1 => Notice,
90            2 => FullName,
91            3 => FamilyName,
92            4 => Weight,
93            5 => FontBbox,
94            13 => UniqueId,
95            14 => Xuid,
96            15 => Charset,
97            16 => Encoding,
98            17 => CharstringsOffset,
99            18 => PrivateDictRange,
100            24 => VariationStoreOffset,
101            // Private DICT operators
102            6 => BlueValues,
103            7 => OtherBlues,
104            8 => FamilyBlues,
105            9 => FamilyOtherBlues,
106            10 => StdHw,
107            11 => StdVw,
108            19 => SubrsOffset,
109            20 => DefaultWidthX,
110            21 => NominalWidthX,
111            22 => VariationStoreIndex,
112            23 => Blend,
113            // Font DICT only uses PrivateDictRange
114            _ => return None,
115        })
116    }
117
118    fn from_extended_opcode(opcode: u8) -> Option<Self> {
119        use Operator::*;
120        Some(match opcode {
121            // Top DICT operators
122            0 => Copyright,
123            1 => IsFixedPitch,
124            2 => ItalicAngle,
125            3 => UnderlinePosition,
126            4 => UnderlineThickness,
127            5 => PaintType,
128            6 => CharstringType,
129            7 => FontMatrix,
130            8 => StrokeWidth,
131            20 => SyntheticBase,
132            21 => PostScript,
133            22 => BaseFontName,
134            23 => BaseFontBlend,
135            30 => Ros,
136            31 => CidFontVersion,
137            32 => CidFontRevision,
138            33 => CidFontType,
139            34 => CidCount,
140            35 => UidBase,
141            36 => FdArrayOffset,
142            37 => FdSelectOffset,
143            38 => FontName,
144            // Private DICT operators
145            9 => BlueScale,
146            10 => BlueShift,
147            11 => BlueFuzz,
148            12 => StemSnapH,
149            13 => StemSnapV,
150            14 => ForceBold,
151            17 => LanguageGroup,
152            18 => ExpansionFactor,
153            19 => InitialRandomSeed,
154            _ => return None,
155        })
156    }
157}
158
159/// Either a PostScript DICT operator or a (numeric) operand.
160#[derive(Copy, Clone, PartialEq, Eq, Debug)]
161pub enum Token {
162    /// An operator parsed from a DICT.
163    Operator(Operator),
164    /// An opcode that is not a known operator.
165    ///
166    /// Reported rather than refused: a DICT containing one is still a DICT,
167    /// and what to do about it is the caller's decision. FreeType ignores
168    /// unknown operators and clears the stack, which is what
169    /// [`entries`] does; refusing here would leave a caller no way to do the
170    /// same through the raw view.
171    InvalidOperator(u8),
172    /// As [`InvalidOperator`][Self::InvalidOperator], for the two-byte form.
173    ///
174    /// Carries the extended opcode only; the byte before it is always the
175    /// escape.
176    InvalidTwoByteOperator(u8),
177    /// A number parsed from a DICT. If the source was in
178    /// binary coded decimal format, then the second field
179    /// contains the parsed components.
180    Operand(Number, Option<BcdComponents>),
181}
182
183impl From<Operator> for Token {
184    fn from(value: Operator) -> Self {
185        Self::Operator(value)
186    }
187}
188
189impl<T> From<T> for Token
190where
191    T: Into<Number>,
192{
193    fn from(value: T) -> Self {
194        Self::Operand(value.into(), None)
195    }
196}
197
198/// Given a byte slice containing DICT data, returns an iterator yielding
199/// raw operands and operators.
200///
201/// This does not perform any additional processing such as type conversion,
202/// delta decoding or blending.
203pub fn tokens(dict_data: &[u8]) -> impl Iterator<Item = Result<Token, Error>> + '_ + Clone {
204    let mut cursor = crate::FontData::new(dict_data).cursor();
205    std::iter::from_fn(move || {
206        if cursor.remaining_bytes() == 0 {
207            None
208        } else {
209            Some(parse_token(&mut cursor))
210        }
211    })
212}
213
214fn parse_token(cursor: &mut Cursor) -> Result<Token, Error> {
215    // Escape opcode for accessing extensions.
216    const ESCAPE: u8 = 12;
217    let b0 = cursor.read::<u8>()?;
218    Ok(if b0 == ESCAPE {
219        let b1 = cursor.read::<u8>()?;
220        match Operator::from_extended_opcode(b1) {
221            Some(op) => Token::Operator(op),
222            None => Token::InvalidTwoByteOperator(b1),
223        }
224    } else {
225        // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-3-operand-encoding>
226        match b0 {
227            28 | 29 | 32..=254 => Token::Operand(num::parse_int(cursor, b0)?.into(), None),
228            30 => {
229                let components = BcdComponents::parse(cursor)?;
230                Token::Operand(components.value(false).into(), Some(components))
231            }
232            _ => match Operator::from_opcode(b0) {
233                Some(op) => Token::Operator(op),
234                None => Token::InvalidOperator(b0),
235            },
236        }
237    })
238}
239
240/// PostScript DICT Operator with its associated operands.
241#[derive(Clone, PartialEq, Eq, Debug)]
242pub enum Entry {
243    Version(Sid),
244    Notice(Sid),
245    FullName(Sid),
246    FamilyName(Sid),
247    Weight(Sid),
248    FontBbox([Fixed; 4]),
249    CharstringsOffset(usize),
250    PrivateDictRange(Range<usize>),
251    VariationStoreOffset(usize),
252    Copyright(Sid),
253    IsFixedPitch(bool),
254    ItalicAngle(Fixed),
255    UnderlinePosition(Fixed),
256    UnderlineThickness(Fixed),
257    PaintType(i32),
258    CharstringType(i32),
259    FontMatrix(ScaledFontMatrix),
260    StrokeWidth(Fixed),
261    FdArrayOffset(usize),
262    FdSelectOffset(usize),
263    BlueValues(Blues),
264    OtherBlues(Blues),
265    FamilyBlues(Blues),
266    FamilyOtherBlues(Blues),
267    SubrsOffset(usize),
268    VariationStoreIndex(u16),
269    BlueScale(Fixed),
270    BlueShift(Fixed),
271    BlueFuzz(Fixed),
272    LanguageGroup(i32),
273    ExpansionFactor(Fixed),
274    Encoding(usize),
275    Charset(usize),
276    UniqueId(i32),
277    Xuid,
278    SyntheticBase(i32),
279    PostScript(Sid),
280    BaseFontName(Sid),
281    BaseFontBlend,
282    Ros {
283        registry: Sid,
284        ordering: Sid,
285        supplement: Fixed,
286    },
287    CidFontVersion(Fixed),
288    CidFontRevision(Fixed),
289    CidFontType(i32),
290    CidCount(u32),
291    UidBase(i32),
292    FontName(Sid),
293    StdHw(Fixed),
294    StdVw(Fixed),
295    DefaultWidthX(Fixed),
296    NominalWidthX(Fixed),
297    StemSnapH(StemSnaps),
298    StemSnapV(StemSnaps),
299    ForceBold(bool),
300    InitialRandomSeed(i32),
301}
302
303/// Given a byte slice containing DICT data, returns an iterator yielding
304/// each operator with its associated operands.
305///
306/// This performs appropriate type conversions, decodes deltas and applies
307/// blending.
308///
309/// If processing a Private DICT from a CFF2 table and an item variation
310/// store is present, then `blend_state` must be provided.
311pub fn entries<'a>(
312    dict_data: &'a [u8],
313    blend_state: Option<BlendState<'a>>,
314) -> impl Iterator<Item = Result<Entry, Error>> + 'a {
315    entries_impl(dict_data, blend_state, false)
316}
317
318/// Reads only the fields needed by an unhinted Private DICT subfont, sharing
319/// tokenization, blend state, and special operand handling with the full reader.
320pub(crate) fn unhinted_private_entries<'a>(
321    dict_data: &'a [u8],
322    blend_state: Option<BlendState<'a>>,
323) -> impl Iterator<Item = Result<Entry, Error>> + 'a {
324    entries_impl(dict_data, blend_state, true)
325}
326
327fn entries_impl<'a>(
328    dict_data: &'a [u8],
329    mut blend_state: Option<BlendState<'a>>,
330    unhinted_private: bool,
331) -> impl Iterator<Item = Result<Entry, Error>> + 'a {
332    let mut stack = Stack::new();
333    let mut last_bcd_components = None;
334    let mut cursor = crate::FontData::new(dict_data).cursor();
335    let mut cursor_pos = 0;
336    std::iter::from_fn(move || loop {
337        if cursor.remaining_bytes() == 0 {
338            return None;
339        }
340        // Operands go straight onto the stack rather than through a `Token`:
341        // this loop is the hot part of subfont construction, and a token
342        // carries binary coded decimal state that only one operator reads.
343        const ESCAPE: u8 = 12;
344        let b0 = match cursor.read::<u8>() {
345            Ok(b0) => b0,
346            Err(e) => return Some(Err(e.into())),
347        };
348        let op = match b0 {
349            // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-3-operand-encoding>
350            28 | 29 | 32..=254 => {
351                last_bcd_components = None;
352                match num::parse_int(&mut cursor, b0).and_then(|number| stack.push(number)) {
353                    Ok(_) => continue,
354                    Err(e) => return Some(Err(e)),
355                }
356            }
357            30 => {
358                let components = match BcdComponents::parse(&mut cursor) {
359                    Ok(components) => components,
360                    Err(e) => return Some(Err(e)),
361                };
362                let result = stack.push(components.value(false));
363                last_bcd_components = Some(components);
364                match result {
365                    Ok(_) => continue,
366                    Err(e) => return Some(Err(e)),
367                }
368            }
369            _ => {
370                let op = if b0 == ESCAPE {
371                    match cursor.read::<u8>() {
372                        Ok(b1) => Operator::from_extended_opcode(b1),
373                        Err(e) => return Some(Err(e.into())),
374                    }
375                } else {
376                    Operator::from_opcode(b0)
377                };
378                match op {
379                    Some(op) => op,
380                    None => {
381                        // Some buggy fonts have invalid dict operators. Clear
382                        // the stack and attempt to continue.
383                        // FreeType only processes known fields:
384                        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffparse.c#L1328>
385                        // And then clears the stack regardless:
386                        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffparse.c#L1469>
387                        stack.clear();
388                        continue;
389                    }
390                }
391            }
392        };
393        if op == Operator::Blend || op == Operator::VariationStoreIndex {
394            let state = match blend_state.as_mut() {
395                Some(state) => state,
396                None => return Some(Err(Error::MissingResource)),
397            };
398            if op == Operator::VariationStoreIndex {
399                match stack
400                    .get_i32(0)
401                    .and_then(|ix| state.set_store_index(ix as u16))
402                {
403                    Ok(_) => {}
404                    Err(e) => return Some(Err(e)),
405                }
406            }
407            if op == Operator::Blend {
408                match stack.apply_blend(state) {
409                    Ok(_) => continue,
410                    Err(e) => return Some(Err(e)),
411                }
412            }
413        }
414        let keep = !unhinted_private
415            || matches!(
416                op,
417                Operator::SubrsOffset
418                    | Operator::VariationStoreIndex
419                    | Operator::DefaultWidthX
420                    | Operator::NominalWidthX
421            );
422        if op == Operator::BlueScale {
423            // FreeType parses BlueScale using a scaling factor of
424            // 1000, presumably to capture more precision in the
425            // fractional part. We do the same.
426            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/src/cff/cfftoken.h?ref_type=heads#L87>
427            if let Some(bcd_components) = last_bcd_components.take() {
428                // If the most recent numeric value was parsed as a
429                // binary coded decimal then recompute the value using
430                // the desired scaling and replace it on the stack
431                stack.pop_fixed().ok()?;
432                stack.push(bcd_components.value(true)).ok()?;
433            }
434        }
435        if op == Operator::FontMatrix {
436            // FontMatrix is also parsed specially... *sigh*
437            // Redo the entire thing with special scaling factors
438            // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffparse.c#L623>
439            // Dump the current values
440            stack.clear();
441            last_bcd_components = None;
442            // Now reparse with dynamic scaling
443            let mut cursor = crate::FontData::new(dict_data).cursor();
444            cursor.advance_by(cursor_pos);
445            if let Some(matrix) = ScaledFontMatrix::parse(&mut cursor) {
446                if keep {
447                    return Some(Ok(Entry::FontMatrix(matrix)));
448                }
449            }
450            continue;
451        }
452        last_bcd_components = None;
453        let entry = keep.then(|| parse_entry(op, &mut stack));
454        stack.clear();
455        cursor_pos = cursor.position().unwrap_or_default();
456        if let Some(entry) = entry {
457            return Some(entry);
458        }
459    })
460}
461
462fn parse_entry(op: Operator, stack: &mut Stack) -> Result<Entry, Error> {
463    use Operator::*;
464    Ok(match op {
465        Version => Entry::Version(stack.pop_i32()?.into()),
466        Notice => Entry::Notice(stack.pop_i32()?.into()),
467        FullName => Entry::FullName(stack.pop_i32()?.into()),
468        FamilyName => Entry::FamilyName(stack.pop_i32()?.into()),
469        Weight => Entry::Weight(stack.pop_i32()?.into()),
470        FontBbox => Entry::FontBbox([
471            stack.get_fixed(0)?,
472            stack.get_fixed(1)?,
473            stack.get_fixed(2)?,
474            stack.get_fixed(3)?,
475        ]),
476        CharstringsOffset => Entry::CharstringsOffset(stack.pop_i32()? as usize),
477        PrivateDictRange => {
478            let len = stack.get_i32(0)? as usize;
479            let start = stack.get_i32(1)? as usize;
480            let end = start.checked_add(len).ok_or(Error::Malformed)?;
481            Entry::PrivateDictRange(start..end)
482        }
483        VariationStoreOffset => Entry::VariationStoreOffset(stack.pop_i32()? as usize),
484        Copyright => Entry::Copyright(stack.pop_i32()?.into()),
485        IsFixedPitch => Entry::IsFixedPitch(stack.pop_i32()? != 0),
486        ItalicAngle => Entry::ItalicAngle(stack.pop_fixed()?),
487        UnderlinePosition => Entry::UnderlinePosition(stack.pop_fixed()?),
488        UnderlineThickness => Entry::UnderlineThickness(stack.pop_fixed()?),
489        PaintType => Entry::PaintType(stack.pop_i32()?),
490        CharstringType => Entry::CharstringType(stack.pop_i32()?),
491        FontMatrix => unreachable!(),
492        StrokeWidth => Entry::StrokeWidth(stack.pop_fixed()?),
493        FdArrayOffset => Entry::FdArrayOffset(stack.pop_i32()? as usize),
494        FdSelectOffset => Entry::FdSelectOffset(stack.pop_i32()? as usize),
495        BlueValues => {
496            stack.apply_delta_prefix_sum();
497            Entry::BlueValues(Blues::new(stack.fixed_values()))
498        }
499        OtherBlues => {
500            stack.apply_delta_prefix_sum();
501            Entry::OtherBlues(Blues::new(stack.fixed_values()))
502        }
503        FamilyBlues => {
504            stack.apply_delta_prefix_sum();
505            Entry::FamilyBlues(Blues::new(stack.fixed_values()))
506        }
507        FamilyOtherBlues => {
508            stack.apply_delta_prefix_sum();
509            Entry::FamilyOtherBlues(Blues::new(stack.fixed_values()))
510        }
511        SubrsOffset => Entry::SubrsOffset(stack.pop_i32()? as usize),
512        VariationStoreIndex => Entry::VariationStoreIndex(stack.pop_i32()? as u16),
513        BlueScale => Entry::BlueScale(stack.pop_fixed()?),
514        BlueShift => Entry::BlueShift(stack.pop_fixed()?),
515        BlueFuzz => Entry::BlueFuzz(stack.pop_fixed()?),
516        LanguageGroup => Entry::LanguageGroup(stack.pop_i32()?),
517        ExpansionFactor => Entry::ExpansionFactor(stack.pop_fixed()?),
518        Encoding => Entry::Encoding(stack.pop_i32()? as usize),
519        Charset => Entry::Charset(stack.pop_i32()? as usize),
520        UniqueId => Entry::UniqueId(stack.pop_i32()?),
521        Xuid => Entry::Xuid,
522        SyntheticBase => Entry::SyntheticBase(stack.pop_i32()?),
523        PostScript => Entry::PostScript(stack.pop_i32()?.into()),
524        BaseFontName => Entry::BaseFontName(stack.pop_i32()?.into()),
525        BaseFontBlend => Entry::BaseFontBlend,
526        Ros => Entry::Ros {
527            registry: stack.get_i32(0)?.into(),
528            ordering: stack.get_i32(1)?.into(),
529            supplement: stack.get_fixed(2)?,
530        },
531        CidFontVersion => Entry::CidFontVersion(stack.pop_fixed()?),
532        CidFontRevision => Entry::CidFontRevision(stack.pop_fixed()?),
533        CidFontType => Entry::CidFontType(stack.pop_i32()?),
534        CidCount => Entry::CidCount(stack.pop_i32()? as u32),
535        UidBase => Entry::UidBase(stack.pop_i32()?),
536        FontName => Entry::FontName(stack.pop_i32()?.into()),
537        StdHw => Entry::StdHw(stack.pop_fixed()?),
538        StdVw => Entry::StdVw(stack.pop_fixed()?),
539        DefaultWidthX => Entry::DefaultWidthX(stack.pop_fixed()?),
540        NominalWidthX => Entry::NominalWidthX(stack.pop_fixed()?),
541        StemSnapH => {
542            stack.apply_delta_prefix_sum();
543            Entry::StemSnapH(StemSnaps::new(stack.fixed_values()))
544        }
545        StemSnapV => {
546            stack.apply_delta_prefix_sum();
547            Entry::StemSnapV(StemSnaps::new(stack.fixed_values()))
548        }
549        ForceBold => Entry::ForceBold(stack.pop_i32()? != 0),
550        InitialRandomSeed => Entry::InitialRandomSeed(stack.pop_i32()?),
551        // Blend is handled at the layer above
552        Blend => unreachable!(),
553    })
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::{
560        tables::variations::ItemVariationStore, types::F2Dot14, FontData, FontRead, FontRef,
561        TableProvider,
562    };
563    use font_test_data::bebuffer::BeBuffer;
564
565    /// An unknown operator is a token, not the end of the iteration.
566    ///
567    /// This is the whole point of reporting it that way: `entries` skips
568    /// unknown operators and clears the stack, as FreeType does, and a caller
569    /// working from the raw view has to be able to do the same. Refusing here
570    /// would end the walk at the first buggy font.
571    #[test]
572    fn unknown_operators_are_tokens() {
573        // a single byte operand encodes `b0 - 139`; 25 is not a one byte
574        // operator and 12/77 is not a two byte one
575        let dict = [139u8, 25, 139, 12, 77, 139, 17];
576        let tokens: Vec<_> = tokens(&dict).map(|tok| tok.unwrap()).collect();
577        assert_eq!(
578            &tokens,
579            &[
580                0.into(),
581                Token::InvalidOperator(25),
582                0.into(),
583                Token::InvalidTwoByteOperator(77),
584                0.into(),
585                Operator::CharstringsOffset.into(),
586            ]
587        );
588    }
589
590    #[test]
591    fn example_top_dict_tokens() {
592        use Operator::*;
593        let top_dict_data = &font_test_data::cff2::EXAMPLE[5..12];
594        let tokens: Vec<_> = tokens(top_dict_data).map(|entry| entry.unwrap()).collect();
595        let expected: &[Token] = &[
596            68.into(),
597            FdArrayOffset.into(),
598            56.into(),
599            CharstringsOffset.into(),
600            16.into(),
601            VariationStoreOffset.into(),
602        ];
603        assert_eq!(&tokens, expected);
604    }
605
606    #[test]
607    fn example_top_dict_entries() {
608        use Entry::*;
609        let top_dict_data = &font_test_data::cff2::EXAMPLE[0x5..=0xB];
610        let entries: Vec<_> = entries(top_dict_data, None)
611            .map(|entry| entry.unwrap())
612            .collect();
613        let expected: &[Entry] = &[
614            FdArrayOffset(68),
615            CharstringsOffset(56),
616            VariationStoreOffset(16),
617        ];
618        assert_eq!(&entries, expected);
619    }
620
621    #[test]
622    fn unhinted_private_entries_preserve_blends() {
623        let data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
624        let store =
625            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
626        for coord in [-0.75, 0.0, 0.25, 0.75] {
627            let coords = [F2Dot14::from_f32(coord)];
628            let expected = entries(
629                data,
630                Some(BlendState::new(store.clone(), &coords, 0).unwrap()),
631            )
632            .filter_map(Result::ok)
633            .filter(|entry| {
634                matches!(
635                    entry,
636                    Entry::SubrsOffset(_)
637                        | Entry::VariationStoreIndex(_)
638                        | Entry::DefaultWidthX(_)
639                        | Entry::NominalWidthX(_)
640                )
641            })
642            .collect::<Vec<_>>();
643            let actual = unhinted_private_entries(
644                data,
645                Some(BlendState::new(store.clone(), &coords, 0).unwrap()),
646            )
647            .filter_map(Result::ok)
648            .collect::<Vec<_>>();
649            assert_eq!(actual, expected);
650        }
651    }
652
653    #[test]
654    fn unhinted_private_entries_preserve_stack_and_special_operators() {
655        let data = [
656            142, 143, 6,  // ignored BlueValues
657            10, // ignored StdHW with a missing operand
658            30, 10, 0, 31, 12, 9, // BlueScale with its special BCD handling
659            28, 2, 88, 20, // default width 600
660            30, 10, 0, 31, 139, 139, 30, 10, 0, 31, 139, 139, 12, 7, // FontMatrix
661            140, 255, // unknown operator must clear its operand
662            159, 21, // nominal width 20
663            149, 19, // subrs offset 10
664        ];
665        let actual = unhinted_private_entries(&data, None)
666            .map(Result::unwrap)
667            .collect::<Vec<_>>();
668        assert_eq!(
669            actual,
670            [
671                Entry::DefaultWidthX(Fixed::from_i32(600)),
672                Entry::NominalWidthX(Fixed::from_i32(20)),
673                Entry::SubrsOffset(10),
674            ]
675        );
676    }
677
678    #[test]
679    fn example_private_dict_entries() {
680        use Entry::*;
681        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
682        let store =
683            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
684        let coords = &[F2Dot14::from_f32(0.0)];
685        let blend_state = BlendState::new(store, coords, 0).unwrap();
686        let entries: Vec<_> = entries(private_dict_data, Some(blend_state))
687            .map(|entry| entry.unwrap())
688            .collect();
689        fn make_blues(values: &[f64]) -> Blues {
690            Blues::new(values.iter().copied().map(Fixed::from_f64))
691        }
692        fn make_stem_snaps(values: &[f64]) -> StemSnaps {
693            StemSnaps::new(values.iter().copied().map(Fixed::from_f64))
694        }
695        let expected: &[Entry] = &[
696            BlueValues(make_blues(&[
697                -20.0, 0.0, 472.0, 490.0, 525.0, 540.0, 645.0, 660.0, 670.0, 690.0, 730.0, 750.0,
698            ])),
699            OtherBlues(make_blues(&[-250.0, -240.0])),
700            FamilyBlues(make_blues(&[
701                -20.0, 0.0, 473.0, 491.0, 525.0, 540.0, 644.0, 659.0, 669.0, 689.0, 729.0, 749.0,
702            ])),
703            FamilyOtherBlues(make_blues(&[-249.0, -239.0])),
704            BlueScale(Fixed::from_f64(0.037506103515625)),
705            BlueFuzz(Fixed::ZERO),
706            StdHw(Fixed::from_f64(55.0)),
707            StdVw(Fixed::from_f64(80.0)),
708            StemSnapH(make_stem_snaps(&[40.0, 55.0])),
709            StemSnapV(make_stem_snaps(&[80.0, 90.0])),
710            SubrsOffset(114),
711        ];
712        assert_eq!(&entries, expected);
713    }
714
715    #[test]
716    fn noto_serif_display_top_dict_entries() {
717        use Entry::*;
718        let top_dict_data = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED)
719            .unwrap()
720            .cff()
721            .unwrap()
722            .top_dicts()
723            .get(0)
724            .unwrap();
725        let entries: Vec<_> = entries(top_dict_data, None)
726            .map(|entry| entry.unwrap())
727            .collect();
728        let expected = &[
729            Version(Sid::new(391)),
730            Notice(Sid::new(392)),
731            Copyright(Sid::new(393)),
732            FullName(Sid::new(394)),
733            FamilyName(Sid::new(395)),
734            FontBbox([-693.0, -470.0, 2797.0, 1048.0].map(Fixed::from_f64)),
735            Charset(517),
736            PrivateDictRange(549..587),
737            CharstringsOffset(521),
738        ];
739        assert_eq!(&entries, expected);
740    }
741
742    // Fuzzer caught add with overflow when constructing private DICT
743    // range.
744    // See <https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=71746>
745    // and <https://oss-fuzz.com/testcase?key=4591358306746368>
746    #[test]
747    fn private_dict_range_avoid_overflow() {
748        // A Private DICT that tries to construct a range from -1..(-1 + -1)
749        // which overflows when converted to usize
750        let private_dict = BeBuffer::new()
751            .push(29u8) // integer operator
752            .push(-1i32) // integer value
753            .push(29u8) // integer operator
754            .push(-1i32) // integer value
755            .push(18u8) // PrivateDICT operator
756            .to_vec();
757        // Just don't panic
758        let _ = entries(&private_dict, None).count();
759    }
760
761    #[test]
762    fn read_font_matrix() {
763        let dict_data = [
764            30u8, 10, 0, 31, 139, 30, 10, 0, 1, 103, 255, 30, 10, 0, 31, 139, 139, 12, 7,
765        ];
766        let Entry::FontMatrix(matrix) = entries(&dict_data, None).next().unwrap().unwrap() else {
767            panic!("This was totally a font matrix");
768        };
769        // From ttx: <FontMatrix value="0.001 0 0.000167 0.001 0 0"/>
770        // But scaled by 1000 because that's how FreeType does it
771        assert_eq!(
772            matrix.matrix.elements(),
773            [
774                Fixed::ONE,
775                Fixed::ZERO,
776                Fixed::from_f64(0.167007446289062),
777                Fixed::ONE,
778                Fixed::ZERO,
779                Fixed::ZERO,
780            ]
781        );
782    }
783
784    #[test]
785    fn parse_degenerate_font_matrix() {
786        let dict_data = [
787            30u8, 0x0F, 30, 0x0F, 30, 0x0F, 30, 0x0F, 30, 0x0F, 30, 0x0F, 12, 7,
788        ];
789        // Don't return a degenerate matrix at all
790        assert!(entries(&dict_data, None).next().is_none());
791    }
792}