Skip to main content

read_fonts/ps/cff/
font.rs

1//! Unified access to CFF/CFF2 fonts.
2
3use crate::{
4    model::pen::OutlinePen,
5    ps::{
6        cff::{
7            blend::BlendState, charset::Charset, dict, encoding::Encoding as RawEncoding,
8            fd_select::FdSelect, index::Index,
9        },
10        cs::{self, CommandSink, NopFilterSink, TransformSink},
11        encoding::PredefinedEncoding,
12        error::Error,
13        hinting::HintingParams,
14        string::Sid,
15        transform::{self, ScaledFontMatrix, Transform},
16    },
17    tables::{cff, cff2, variations::ItemVariationStore},
18    FontData, FontRead, ReadError,
19};
20use core::ops::Range;
21use types::{BoundingBox, F2Dot14, Fixed, GlyphId};
22
23/// A CFF or CFF2 font.
24///
25/// The source data may be a raw CFF blob as embedded in a PDF or the content
26/// of a CFF or CFF2 table in an OpenType font.
27#[derive(Clone)]
28pub struct CffFontRef<'a> {
29    data: &'a [u8],
30    is_cff2: bool,
31    upem: i32,
32    global_subrs: Index<'a>,
33    top_dict: TopDict<'a>,
34    top_dict_index: u16,
35}
36
37impl<'a> CffFontRef<'a> {
38    /// Creates a new font for the given CFF or CFF2 data.
39    ///
40    /// Tries to determine the CFF version by reading the first word of the
41    /// header.
42    ///
43    /// For CFF blobs embedded in an OpenType font, the upem should be taken
44    /// from the `head` table. Otherwise, 1000 will be assumed.
45    pub fn new(data: &'a [u8], top_dict_index: u32, upem: Option<i32>) -> Result<Self, Error> {
46        let version: u8 = FontData::new(data).read_at(0)?;
47        match version {
48            1 => Self::new_cff(data, top_dict_index, upem),
49            2 => Self::new_cff2(data, upem),
50            _ => Err(Error::InvalidFontFormat),
51        }
52    }
53
54    /// Creates a new font for the given CFF data.
55    ///
56    /// For CFF blobs embedded in an OpenType font, the upem should be taken
57    /// from the `head` table. Otherwise, 1000 will be assumed.
58    pub fn new_cff(data: &'a [u8], top_dict_index: u32, upem: Option<i32>) -> Result<Self, Error> {
59        let cff = cff::Cff::read(data.into())?;
60        let top_dict_data = cff.top_dicts().get(top_dict_index as usize)?;
61        let top_dict_index: u16 = top_dict_index
62            .try_into()
63            .map_err(|_| ReadError::OutOfBounds)?;
64        Self::new_impl(
65            data,
66            false,
67            upem,
68            top_dict_data,
69            top_dict_index,
70            cff.strings().into(),
71            cff.global_subrs().into(),
72        )
73    }
74
75    /// Creates a new font for the given CFF2 data.
76    ///
77    /// For CFF blobs embedded in an OpenType font, the upem should be taken
78    /// from the `head` table. Otherwise, 1000 will be assumed.
79    pub fn new_cff2(data: &'a [u8], upem: Option<i32>) -> Result<Self, Error> {
80        let cff = cff2::Cff2::read(data.into())?;
81        Self::new_impl(
82            data,
83            true,
84            upem,
85            cff.top_dict_data(),
86            0,
87            Index::Empty,
88            cff.global_subrs().into(),
89        )
90    }
91
92    fn new_impl(
93        data: &'a [u8],
94        is_cff2: bool,
95        upem: Option<i32>,
96        top_dict_data: &'a [u8],
97        top_dict_index: u16,
98        strings: Index<'a>,
99        global_subrs: Index<'a>,
100    ) -> Result<Self, Error> {
101        let top_dict = TopDict::new(data, top_dict_data, strings, is_cff2)?;
102        let top_upem = top_dict.matrix.map(|mat| mat.scale).unwrap_or(1000);
103        Ok(Self {
104            data,
105            is_cff2,
106            upem: upem.unwrap_or(top_upem),
107            global_subrs,
108            top_dict,
109            top_dict_index,
110        })
111    }
112
113    /// Returns the raw CFF blob.
114    pub fn data(&self) -> &'a [u8] {
115        self.data
116    }
117
118    /// Returns the CFF version (either 1 or 2).
119    pub fn version(&self) -> u16 {
120        if self.is_cff2 {
121            2
122        } else {
123            1
124        }
125    }
126
127    /// Returns additional metadata such as font names and metrics.
128    pub fn metadata(&self) -> Option<Metadata<'a>> {
129        Metadata::new(self.data, self.top_dict_index)
130    }
131
132    /// Returns true if this is a CID-keyed font.
133    pub fn is_cid(&self) -> bool {
134        matches!(&self.top_dict.kind, CffFontKind::Cid { .. })
135    }
136
137    /// Returns the global subroutine index.
138    pub fn global_subrs(&self) -> &Index<'a> {
139        &self.global_subrs
140    }
141
142    /// Returns the number of glyphs in the CFF font.
143    pub fn num_glyphs(&self) -> u32 {
144        self.top_dict.charstrings.count()
145    }
146
147    /// Returns the charstring index.
148    pub fn charstrings(&self) -> &Index<'a> {
149        &self.top_dict.charstrings
150    }
151
152    /// Returns the string index.
153    pub fn strings(&self) -> Option<&Index<'a>> {
154        match &self.top_dict.kind {
155            CffFontKind::Sid { strings, .. } => Some(strings),
156            _ => None,
157        }
158    }
159
160    /// Returns the string for the given identifier.
161    pub fn string(&self, sid: Sid) -> Option<&'a [u8]> {
162        match sid.resolve_standard() {
163            Ok(s) => Some(s),
164            Err(idx) => self.strings()?.get(idx).ok(),
165        }
166    }
167
168    /// Returns the mapping for glyph identifiers.
169    ///
170    /// For a CID font, this maps between CIDs and glyph identifiers.
171    /// Otherwise maps between SIDs and glyph identifiers.
172    pub fn charset(&self) -> Option<Charset<'a>> {
173        Charset::new(
174            self.data.into(),
175            self.top_dict.charset_offset.get()?,
176            self.top_dict.charstrings.count(),
177        )
178        .ok()
179    }
180
181    /// Returns the mapping from character codes to glyph identifiers.
182    pub fn encoding(&self) -> Option<Encoding<'a>> {
183        let charset = self.charset()?;
184        let encoding = RawEncoding::new(self.data, self.top_dict.encoding_offset.get()?).ok()?;
185        Some(Encoding { encoding, charset })
186    }
187
188    /// Returns the top level font matrix.
189    pub fn matrix(&self) -> Option<&ScaledFontMatrix> {
190        self.top_dict.matrix.as_ref()
191    }
192
193    /// Returns the units per em.
194    pub fn upem(&self) -> i32 {
195        self.upem
196    }
197
198    /// Returns the item variation store.
199    ///
200    /// Only present for CFF2 variable fonts.
201    pub fn var_store(&self) -> Option<&ItemVariationStore<'a>> {
202        self.top_dict.var_store.as_ref()
203    }
204
205    /// Returns the number of available subfonts.
206    pub fn num_subfonts(&self) -> u16 {
207        match &self.top_dict.kind {
208            CffFontKind::Sid { .. } => 1,
209            CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
210                fd_array.count() as u16
211            }
212        }
213    }
214
215    /// Returns the subfont index for the given glyph.
216    pub fn subfont_index(&self, gid: GlyphId) -> Option<u16> {
217        match &self.top_dict.kind {
218            CffFontKind::Sid { .. } => Some(0),
219            CffFontKind::Cid { fd_select, .. } | CffFontKind::Cff2 { fd_select, .. } => fd_select
220                .as_ref()
221                .map_or(Some(0), |fds| fds.font_index(gid)),
222        }
223    }
224
225    /// Returns the subfont with the given index and normalized variation
226    /// coordinates.
227    pub fn subfont(&self, index: u16, coords: &[F2Dot14]) -> Result<Subfont, Error> {
228        let blend = self.blend_state(0, coords);
229        match &self.top_dict.kind {
230            CffFontKind::Sid { private_dict, .. } => Subfont::new(
231                self.data,
232                private_dict.start as usize..private_dict.end as usize,
233                blend,
234                None,
235            ),
236            CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
237                let font_dict = FontDict::new(fd_array.get(index as usize)?)?;
238                Subfont::new(
239                    self.data,
240                    font_dict.private_dict_range,
241                    blend,
242                    font_dict.matrix,
243                )
244            }
245        }
246    }
247
248    /// Returns the subfont and hinting parameters for the given index and
249    /// normalized variation coordinates.
250    pub fn subfont_hinted(
251        &self,
252        index: u16,
253        coords: &[F2Dot14],
254    ) -> Result<(Subfont, HintingParams), Error> {
255        let blend = self.blend_state(0, coords);
256        match &self.top_dict.kind {
257            CffFontKind::Sid { private_dict, .. } => Subfont::new_hinted(
258                self.data,
259                private_dict.start as usize..private_dict.end as usize,
260                blend,
261                None,
262            ),
263            CffFontKind::Cid { fd_array, .. } | CffFontKind::Cff2 { fd_array, .. } => {
264                let font_dict = FontDict::new(fd_array.get(index as usize)?)?;
265                Subfont::new_hinted(
266                    self.data,
267                    font_dict.private_dict_range,
268                    blend,
269                    font_dict.matrix,
270                )
271            }
272        }
273    }
274
275    /// Returns the effective transform for the given subfont and optional size
276    /// in pixels per em.
277    pub fn transform(&self, subfont: &Subfont, ppem: Option<f32>) -> Transform {
278        let mut scale = ppem.map(|ppem| Transform::compute_scale(ppem, self.upem));
279        // Compute our font matrix and adjusted UPEM
280        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffobjs.c#L746>
281        let scaled_matrix = if let Some(top_matrix) = self.top_dict.matrix {
282            // We have a top dict matrix. Now check for a font dict matrix.
283            if let Some(sub_matrix) = subfont.matrix {
284                let scaling = if top_matrix.scale > 1 && sub_matrix.scale > 1 {
285                    top_matrix.scale.min(sub_matrix.scale)
286                } else {
287                    1
288                };
289                // Concatenate and scale
290                let matrix =
291                    transform::combine_scaled(&top_matrix.matrix, &sub_matrix.matrix, scaling);
292                let scaled_upem = Fixed::from_bits(sub_matrix.scale).mul_div(
293                    Fixed::from_bits(top_matrix.scale),
294                    Fixed::from_bits(scaling),
295                );
296                // Then normalize
297                Some(
298                    ScaledFontMatrix {
299                        matrix,
300                        scale: scaled_upem.to_bits(),
301                    }
302                    .normalize(),
303                )
304            } else {
305                // Top matrix was already normalized on load
306                Some(top_matrix)
307            }
308        } else {
309            // Just normalize if we have a subfont matrix
310            subfont.matrix.map(|matrix| matrix.normalize())
311        };
312        // Now adjust our scale factor if necessary
313        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/f1cd6dbfa0c98f352b698448f40ac27e8fb3832e/src/cff/cffgload.c#L450>
314        if let Some(matrix) = scaled_matrix.as_ref() {
315            // If the scaling factor from our matrix does not equal the nominal
316            // UPEM of the font then adjust the scale.
317            if matrix.scale != self.upem {
318                // In this case, we need to force a scale for "unscaled"
319                // requests in order to apply the adjusted UPEM from the
320                // font matrix.
321                let original_scale = scale.unwrap_or(Fixed::from_i32(64));
322                scale = Some(
323                    original_scale
324                        .mul_div(Fixed::from_bits(self.upem), Fixed::from_bits(matrix.scale)),
325                );
326            }
327        }
328        Transform {
329            matrix: scaled_matrix
330                .map(|scaled_mat| scaled_mat.matrix)
331                .unwrap_or_default(),
332            scale,
333        }
334    }
335
336    /// Evaluates the charstring for the requested glyph and sends the results
337    /// to the given sink.
338    ///
339    /// Returns the advance with of the glyph in font units if the charstring
340    /// provides one.
341    pub fn evaluate_charstring(
342        &self,
343        subfont: &Subfont,
344        gid: GlyphId,
345        coords: &[F2Dot14],
346        sink: &mut impl CommandSink,
347    ) -> Result<Option<Fixed>, Error> {
348        let charstrings = self.top_dict.charstrings.clone();
349        let blend = self.blend_state(subfont.vs_index, coords);
350        let subrs = if subfont.subrs_offset != 0 {
351            let data = self
352                .data
353                .get(subfont.subrs_offset as usize..)
354                .ok_or(ReadError::OutOfBounds)?;
355            Index::new(data, self.is_cff2)?
356        } else {
357            Index::Empty
358        };
359        let charstring_data = charstrings.get(gid.to_u32() as usize)?;
360        let ctx = (self.data, &charstrings, &self.global_subrs, &subrs);
361        if let Some(width) = cs::evaluate(&ctx, blend, charstring_data, sink)? {
362            Ok(Some(width + subfont.nominal_width))
363        } else {
364            Ok(subfont.default_width)
365        }
366    }
367
368    /// Draws the glyph with an optional size in ppem to the given pen.
369    ///
370    /// Returns the advance width of the glyph if the charstring provides
371    /// one.
372    pub fn draw(
373        &self,
374        subfont: &Subfont,
375        gid: GlyphId,
376        coords: &[F2Dot14],
377        ppem: Option<f32>,
378        pen: &mut impl OutlinePen,
379    ) -> Result<Option<f32>, Error> {
380        let mut nop_filter = NopFilterSink::new(pen);
381        let transform = self.transform(subfont, ppem);
382        let mut transformer = TransformSink::new(&mut nop_filter, transform);
383        let width = self.evaluate_charstring(subfont, gid, coords, &mut transformer)?;
384        Ok(width.map(|w| transform.transform_h_metric(w).to_f32().max(0.0)))
385    }
386
387    /// Returns a blend state for the given variation store index and
388    /// normalized coordinates.
389    fn blend_state(&self, vs_index: u16, coords: &'a [F2Dot14]) -> Option<BlendState<'a>> {
390        self.top_dict
391            .var_store
392            .as_ref()
393            .and_then(|store| BlendState::new(store.clone(), coords, vs_index).ok())
394    }
395}
396
397/// Mapping from character codes to glyph identifiers.
398#[derive(Clone)]
399pub struct Encoding<'a> {
400    encoding: RawEncoding<'a>,
401    charset: Charset<'a>,
402}
403
404impl<'a> Encoding<'a> {
405    /// Returns the predefined encoding, if any. Otherwise this is a custom
406    /// encoding.
407    pub fn predefined(&self) -> Option<PredefinedEncoding> {
408        if let RawEncoding::Predefined(pre) = &self.encoding {
409            Some(*pre)
410        } else {
411            None
412        }
413    }
414
415    /// Returns the associated character set.
416    pub fn charset(&self) -> &Charset<'a> {
417        &self.charset
418    }
419
420    /// Maps a character code to a glyph identifier.
421    pub fn map(&self, code: u8) -> Option<GlyphId> {
422        self.encoding.map(&self.charset, code)
423    }
424}
425
426/// An SID or CID font.
427#[derive(Clone)]
428enum CffFontKind<'a> {
429    /// A CFF font.
430    Sid {
431        /// Index for resolving glyph names.
432        strings: Index<'a>,
433        /// Byte range of the private dict from the base of the font data.
434        private_dict: Range<u32>,
435    },
436    /// A CFF font with an externally defined encoding.
437    Cid {
438        /// Maps from glyph identifiers to font dict indices.
439        fd_select: Option<FdSelect<'a>>,
440        /// Index containing font dicts.
441        fd_array: Index<'a>,
442    },
443    /// A CFF2 font.
444    Cff2 {
445        /// Maps from glyph identifiers to font dict indices.
446        fd_select: Option<FdSelect<'a>>,
447        /// Index containing font dicts.
448        fd_array: Index<'a>,
449    },
450}
451
452/// Metadata for a CFF subfont.
453///
454/// A subfont is the collection of data read from both the
455/// [Font DICT](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28)
456/// and [Private Dict](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=24)
457/// structures. These determine the set of subroutines, metrics and hinting
458/// parameters for some group of glyphs.
459///
460/// Use [CffFontRef::subfont_index] to determine the subfont index for a
461/// particular glyph and then [CffFontRef::subfont] (or
462/// [CffFontRef::subfont_hinted]) to retrieve the associated subfont.
463#[derive(Copy, Clone, Default, Debug)]
464pub struct Subfont {
465    subrs_offset: u32,
466    default_width: Option<Fixed>,
467    nominal_width: Fixed,
468    matrix: Option<ScaledFontMatrix>,
469    vs_index: u16,
470}
471
472impl Subfont {
473    fn new(
474        data: &[u8],
475        range: Range<usize>,
476        blend: Option<BlendState>,
477        matrix: Option<ScaledFontMatrix>,
478    ) -> Result<Self, Error> {
479        let mut subfont = Self {
480            matrix,
481            ..Default::default()
482        };
483        let data = data.get(range.clone()).ok_or(ReadError::OutOfBounds)?;
484        for entry in dict::entries(data, blend).filter_map(|e| e.ok()) {
485            match entry {
486                dict::Entry::SubrsOffset(offset) => {
487                    subfont.subrs_offset = range
488                        .start
489                        .checked_add(offset)
490                        .ok_or(ReadError::OutOfBounds)?
491                        as u32;
492                }
493                dict::Entry::VariationStoreIndex(index) => subfont.vs_index = index,
494                // FreeType truncates the width values to int on read
495                dict::Entry::DefaultWidthX(width) => subfont.default_width = Some(width.floor()),
496                dict::Entry::NominalWidthX(width) => subfont.nominal_width = width.floor(),
497                _ => {}
498            }
499        }
500        Ok(subfont)
501    }
502
503    fn new_hinted(
504        data: &[u8],
505        range: Range<usize>,
506        blend: Option<BlendState>,
507        matrix: Option<ScaledFontMatrix>,
508    ) -> Result<(Self, HintingParams), Error> {
509        let mut subfont = Self {
510            matrix,
511            ..Default::default()
512        };
513        let mut params = HintingParams::default();
514        let data = data.get(range.clone()).ok_or(ReadError::OutOfBounds)?;
515        for entry in dict::entries(data, blend).filter_map(|e| e.ok()) {
516            match entry {
517                dict::Entry::SubrsOffset(offset) => {
518                    subfont.subrs_offset = range
519                        .start
520                        .checked_add(offset)
521                        .ok_or(ReadError::OutOfBounds)?
522                        as u32;
523                }
524                dict::Entry::VariationStoreIndex(index) => subfont.vs_index = index,
525                // FreeType truncates the width values to int on read
526                dict::Entry::DefaultWidthX(width) => subfont.default_width = Some(width.floor()),
527                dict::Entry::NominalWidthX(width) => subfont.nominal_width = width.floor(),
528                dict::Entry::BlueValues(values) => params.blues = values,
529                dict::Entry::FamilyBlues(values) => params.family_blues = values,
530                dict::Entry::OtherBlues(values) => params.other_blues = values,
531                dict::Entry::FamilyOtherBlues(values) => params.family_other_blues = values,
532                dict::Entry::BlueScale(value) => params.blue_scale = value,
533                dict::Entry::BlueShift(value) => params.blue_shift = value,
534                dict::Entry::BlueFuzz(value) => params.blue_fuzz = value,
535                dict::Entry::LanguageGroup(group) => params.language_group = group,
536                _ => {}
537            }
538        }
539        Ok((subfont, params))
540    }
541
542    /// Returns the offset to the local subroutine index from the start of the
543    /// CFF blob.
544    pub fn subrs_offset(&self) -> u32 {
545        self.subrs_offset
546    }
547
548    /// Returns the default advance width.
549    ///
550    /// The advance value for charstrings that do not contain a width.
551    pub fn default_width(&self) -> Option<Fixed> {
552        self.default_width
553    }
554
555    /// Returns the nominal advance width.
556    ///
557    /// The base advance value for charstrings that do contain a width. This
558    /// should be added to the width in the charstring.
559    pub fn nominal_width(&self) -> Fixed {
560        self.nominal_width
561    }
562
563    /// Returns the font matrix.
564    pub fn matrix(&self) -> Option<&ScaledFontMatrix> {
565        self.matrix.as_ref()
566    }
567
568    /// Returns the default variation store index.
569    pub fn vs_index(&self) -> u16 {
570        self.vs_index
571    }
572}
573
574/// Use in-band signaling for missing offsets to keep the struct size small.
575#[derive(Copy, Clone)]
576struct MaybeOffset(u32);
577
578impl MaybeOffset {
579    fn get(self) -> Option<usize> {
580        (self.0 != u32::MAX).then_some(self.0 as usize)
581    }
582}
583
584impl Default for MaybeOffset {
585    fn default() -> Self {
586        Self(u32::MAX)
587    }
588}
589
590#[derive(Clone)]
591struct TopDict<'a> {
592    charstrings: Index<'a>,
593    charset_offset: MaybeOffset,
594    encoding_offset: MaybeOffset,
595    matrix: Option<ScaledFontMatrix>,
596    var_store: Option<ItemVariationStore<'a>>,
597    kind: CffFontKind<'a>,
598}
599
600impl<'a> TopDict<'a> {
601    fn new(
602        cff_data: &'a [u8],
603        top_dict_data: &[u8],
604        strings: Index<'a>,
605        is_cff2: bool,
606    ) -> Result<Self, Error> {
607        let mut has_ros = false;
608        let mut charstrings = None;
609        let [mut charset_offset, mut encoding_offset] = if is_cff2 {
610            // CFF2 fonts use the cmap table but grab the encoding and
611            // charset if one is provided
612            [MaybeOffset::default(); 2]
613        } else {
614            // CFF fonts have a default charset and encoding offset of 0
615            // which selects the standard Adobe encoding
616            [MaybeOffset(0); 2]
617        };
618        let mut fd_array = None;
619        let mut fd_select = None;
620        let mut private_dict_range = 0..0;
621        let mut matrix = None;
622        let mut var_store = None;
623        for entry in dict::entries(top_dict_data, None).filter_map(|e| e.ok()) {
624            match entry {
625                dict::Entry::Ros { .. } => has_ros = true,
626                dict::Entry::CharstringsOffset(offset) => {
627                    charstrings = cff_data
628                        .get(offset..)
629                        .and_then(|data| Index::new(data, is_cff2).ok());
630                }
631                dict::Entry::Charset(offset) => charset_offset = MaybeOffset(offset as u32),
632                dict::Entry::Encoding(offset) => encoding_offset = MaybeOffset(offset as u32),
633                dict::Entry::FdArrayOffset(offset) => {
634                    fd_array = cff_data
635                        .get(offset..)
636                        .and_then(|data| Index::new(data, is_cff2).ok());
637                }
638                dict::Entry::FdSelectOffset(offset) => {
639                    fd_select = cff_data
640                        .get(offset..)
641                        .and_then(|data| FdSelect::read(data.into()).ok());
642                }
643                dict::Entry::PrivateDictRange(range) => {
644                    // Fail early if our private dictionary is out of bounds
645                    let _ = cff_data.get(range.clone()).ok_or(ReadError::OutOfBounds)?;
646                    private_dict_range = range;
647                }
648                dict::Entry::FontMatrix(font_matrix) => {
649                    // FreeType always normalizes this and the scaling factor
650                    // is dynamic so it won't make a difference to our users
651                    matrix = Some(font_matrix.normalize());
652                }
653                dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
654                    // IVS is preceded by a 2 byte length, but ensure that
655                    // we don't overflow
656                    // See <https://github.com/googlefonts/fontations/issues/1223>
657                    let offset = offset.checked_add(2).ok_or(ReadError::OutOfBounds)?;
658                    var_store = Some(ItemVariationStore::read(
659                        cff_data.get(offset..).unwrap_or_default().into(),
660                    )?);
661                }
662                _ => {}
663            }
664        }
665        let charstrings = charstrings.ok_or(Error::MissingCharstrings)?;
666        let kind = if let Some(fd_array) = fd_array {
667            if is_cff2 {
668                CffFontKind::Cff2 {
669                    fd_array,
670                    fd_select,
671                }
672            } else {
673                CffFontKind::Cid {
674                    fd_array,
675                    fd_select,
676                }
677            }
678        } else {
679            if has_ros || is_cff2 {
680                // The font dict array is required for CID-keyed and CFF2 fonts
681                return Err(Error::MissingFdArray);
682            }
683            CffFontKind::Sid {
684                strings,
685                private_dict: private_dict_range.start as u32..private_dict_range.end as u32,
686            }
687        };
688        Ok(Self {
689            charset_offset,
690            encoding_offset,
691            charstrings,
692            matrix,
693            kind,
694            var_store,
695        })
696    }
697}
698
699#[derive(Default)]
700struct FontDict {
701    private_dict_range: Range<usize>,
702    matrix: Option<ScaledFontMatrix>,
703}
704
705impl FontDict {
706    fn new(dict_data: &[u8]) -> Result<Self, Error> {
707        let mut range = None;
708        let mut matrix = None;
709        for entry in dict::entries(dict_data, None) {
710            match entry? {
711                dict::Entry::PrivateDictRange(dict_range) => {
712                    range = Some(dict_range);
713                }
714                dict::Entry::FontMatrix(font_matrix) => {
715                    matrix = Some(font_matrix);
716                }
717                _ => {}
718            }
719        }
720        Ok(Self {
721            private_dict_range: range.ok_or(Error::MissingPrivateDict)?,
722            matrix,
723        })
724    }
725}
726
727/// Extra metadata for a CFF font.
728///
729/// This is accessed separately because this information is redundant when a
730/// CFF blob is used in an OpenType font.
731#[derive(Clone, Debug)]
732pub struct Metadata<'a> {
733    name: Option<&'a str>,
734    full_name: Option<&'a str>,
735    family_name: Option<&'a str>,
736    weight: Option<&'a str>,
737    bbox: BoundingBox<Fixed>,
738    italic_angle: Fixed,
739    is_fixed_pitch: bool,
740    underline_position: Fixed,
741    underline_thickness: Fixed,
742}
743
744impl<'a> Metadata<'a> {
745    fn new(data: &'a [u8], top_dict_index: u16) -> Option<Self> {
746        let cff = cff::Cff::read(FontData::new(data)).ok()?;
747        let strings = cff.strings();
748        let get_str = |sid: Sid| {
749            let bytes = match sid.resolve_standard() {
750                Ok(bytes) => bytes,
751                Err(idx) => strings.get(idx).ok()?,
752            };
753            core::str::from_utf8(bytes).ok()
754        };
755        let top_dict_data = cff.top_dicts().get(top_dict_index as usize).ok()?;
756        let name = cff
757            .name(top_dict_index as usize)
758            .and_then(|bytes| core::str::from_utf8(bytes).ok());
759        let mut meta = Metadata {
760            name,
761            ..Default::default()
762        };
763        for entry in dict::entries(top_dict_data, None).filter_map(|e| e.ok()) {
764            match entry {
765                dict::Entry::FullName(sid) => meta.full_name = get_str(sid),
766                dict::Entry::FamilyName(sid) => meta.family_name = get_str(sid),
767                dict::Entry::Weight(sid) => meta.weight = get_str(sid),
768                dict::Entry::FontBbox([x_min, y_min, x_max, y_max]) => {
769                    meta.bbox = BoundingBox {
770                        x_min,
771                        x_max,
772                        y_min,
773                        y_max,
774                    }
775                }
776                dict::Entry::ItalicAngle(angle) => meta.italic_angle = angle,
777                dict::Entry::IsFixedPitch(fixed_pitch) => meta.is_fixed_pitch = fixed_pitch,
778                dict::Entry::UnderlinePosition(pos) => meta.underline_position = pos,
779                dict::Entry::UnderlineThickness(size) => meta.underline_thickness = size,
780                _ => {}
781            }
782        }
783        Some(meta)
784    }
785
786    /// Returns the PostScript name.
787    pub fn name(&self) -> Option<&'a str> {
788        self.name
789    }
790
791    /// Returns the full font name.
792    pub fn full_name(&self) -> Option<&'a str> {
793        self.full_name
794    }
795
796    /// Returns the font family name.
797    pub fn family_name(&self) -> Option<&'a str> {
798        self.family_name
799    }
800
801    /// Returns the weight or style name.
802    pub fn weight(&self) -> Option<&'a str> {
803        self.weight
804    }
805
806    /// Returns the italic angle.
807    pub fn italic_angle(&self) -> Fixed {
808        self.italic_angle
809    }
810
811    /// Returns true if the glyphs in this font have the same width.
812    pub fn is_fixed_pitch(&self) -> bool {
813        self.is_fixed_pitch
814    }
815
816    /// Returns the position of the top of an underline decoration.
817    pub fn underline_position(&self) -> Fixed {
818        self.underline_position
819    }
820
821    /// Returns the suggested size for an underline decoration.
822    pub fn underline_thickness(&self) -> Fixed {
823        self.underline_thickness
824    }
825
826    /// Returns the font bounding box.
827    pub fn bbox(&self) -> BoundingBox<Fixed> {
828        self.bbox
829    }
830}
831
832impl Default for Metadata<'_> {
833    fn default() -> Self {
834        Self {
835            name: None,
836            full_name: None,
837            family_name: None,
838            weight: None,
839            bbox: BoundingBox::default(),
840            italic_angle: Fixed::ZERO,
841            is_fixed_pitch: false,
842            underline_position: Fixed::from_i32(-100),
843            underline_thickness: Fixed::from_i32(50),
844        }
845    }
846}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851    use crate::{
852        ps::{hinting::Blues, transform::FontMatrix},
853        FontData, FontRef, TableProvider,
854    };
855    use cs::test_helpers::*;
856    use font_test_data::bebuffer::BeBuffer;
857
858    #[test]
859    fn read_cff_static() {
860        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
861        let cff =
862            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
863        assert_eq!(cff.version(), 1);
864        assert!(cff.var_store().is_none());
865        let CffFontKind::Sid { private_dict, .. } = &cff.top_dict.kind else {
866            panic!("this is an SID font");
867        };
868        assert!(!private_dict.is_empty());
869        assert_eq!(cff.num_glyphs(), 5);
870        assert_eq!(cff.num_subfonts(), 1);
871        assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
872        assert_eq!(cff.global_subrs.count(), 17);
873    }
874
875    #[test]
876    fn read_cff_metadata() {
877        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
878        let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
879        let meta = cff.metadata().unwrap();
880        assert_eq!(meta.name(), Some("NotoSerifDisplay-Regular"));
881        assert_eq!(meta.full_name(), Some("Noto Serif Display Regular"));
882        assert_eq!(meta.family_name(), Some("Noto Serif Display"));
883        assert_eq!(meta.weight(), None);
884        assert_eq!(
885            meta.bbox(),
886            BoundingBox {
887                x_min: Fixed::from_i32(-693),
888                y_min: Fixed::from_i32(-470),
889                x_max: Fixed::from_i32(2797),
890                y_max: Fixed::from_i32(1048)
891            }
892        );
893        assert_eq!(meta.italic_angle(), Fixed::ZERO);
894        assert!(!meta.is_fixed_pitch());
895        assert_eq!(meta.underline_position(), Fixed::from_i32(-100));
896        assert_eq!(meta.underline_thickness(), Fixed::from_i32(50));
897        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
898        let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
899        let meta = cff.metadata().unwrap();
900        assert_eq!(meta.name(), Some("GoogleMaterialIcons-Regular"));
901        assert_eq!(meta.full_name(), Some("GoogleMaterialIcons-Regular"));
902        assert_eq!(meta.family_name(), None);
903        assert_eq!(meta.weight(), None);
904        assert_eq!(
905            meta.bbox(),
906            BoundingBox {
907                x_min: Fixed::from_i32(-1),
908                y_min: Fixed::ZERO,
909                x_max: Fixed::from_i32(513),
910                y_max: Fixed::from_i32(512)
911            }
912        );
913        assert_eq!(meta.italic_angle(), Fixed::ZERO);
914        assert!(!meta.is_fixed_pitch());
915        assert_eq!(meta.underline_position(), Fixed::from_i32(-100));
916        assert_eq!(meta.underline_thickness(), Fixed::from_i32(50));
917    }
918
919    #[test]
920    fn read_cff2_static() {
921        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
922        let cff =
923            CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
924        assert_eq!(cff.version(), 2);
925        assert!(cff.var_store().is_some());
926        let CffFontKind::Cff2 { fd_array, .. } = &cff.top_dict.kind else {
927            panic!("this is a CFF2 font");
928        };
929        assert_eq!(fd_array.count(), 1);
930        assert_eq!(cff.num_glyphs(), 6);
931        assert_eq!(cff.num_subfonts(), 1);
932        assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
933        assert_eq!(cff.global_subrs.count(), 0);
934    }
935
936    #[test]
937    fn read_example_cff2_table() {
938        let cff = CffFontRef::new_cff2(font_test_data::cff2::EXAMPLE, None).unwrap();
939        assert_eq!(cff.version(), 2);
940        assert!(cff.var_store().is_some());
941        let CffFontKind::Cff2 { fd_array, .. } = &cff.top_dict.kind else {
942            panic!("this is a CFF2 font");
943        };
944        assert_eq!(fd_array.count(), 1);
945        assert_eq!(cff.num_glyphs(), 2);
946        assert_eq!(cff.num_subfonts(), 1);
947        assert_eq!(cff.subfont_index(GlyphId::new(1)), Some(0));
948        assert_eq!(cff.global_subrs.count(), 0);
949    }
950
951    #[test]
952    fn charset() {
953        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
954        let cff =
955            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
956        let charset = cff.charset().unwrap();
957        let glyph_names = charset
958            .iter()
959            .map(|(gid, sid)| {
960                (
961                    gid.to_u32(),
962                    std::str::from_utf8(sid.resolve_standard().unwrap()).unwrap(),
963                )
964            })
965            .collect::<Vec<_>>();
966        let expected = [(0, ".notdef"), (1, "i"), (2, "j"), (3, "k"), (4, "l")];
967        assert_eq!(glyph_names, expected)
968    }
969
970    // Fuzzer caught add with overflow when computing offset to
971    // var store.
972    // See <https://issues.oss-fuzz.com/issues/377574377>
973    #[test]
974    fn top_dict_ivs_offset_overflow() {
975        // A top DICT with a var store offset of -1 which will cause an
976        // overflow
977        let top_dict = BeBuffer::new()
978            .push(29u8) // integer operator
979            .push(-1i32) // integer value
980            .push(24u8) // var store offset operator
981            .to_vec();
982        // Just don't panic with overflow
983        assert!(TopDict::new(&[], &top_dict, Index::Empty, true).is_err());
984    }
985
986    /// Fuzzer caught add with overflow when computing subrs offset.
987    /// See <https://issues.oss-fuzz.com/issues/377965575>
988    #[test]
989    fn subrs_offset_overflow() {
990        // A private DICT with an overflowing subrs offset
991        let private_dict = BeBuffer::new()
992            .push(0u32) // pad so that range doesn't start with 0 and we overflow
993            .push(29u8) // integer operator
994            .push(-1i32) // integer value
995            .push(19u8) // subrs offset operator
996            .to_vec();
997        // Just don't panic with overflow
998        assert!(Subfont::new(&private_dict, 4..private_dict.len(), None, None).is_err());
999    }
1000
1001    /// Ensure we don't reject an empty Private DICT
1002    #[test]
1003    fn empty_private_dict() {
1004        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
1005        let cff =
1006            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1007        let CffFontKind::Sid { private_dict, .. } = &cff.top_dict.kind else {
1008            panic!("this is an SID font");
1009        };
1010        assert!(private_dict.is_empty());
1011    }
1012
1013    // We were overwriting family_other_blues with family_blues.
1014    #[test]
1015    fn capture_family_other_blues() {
1016        let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
1017        let store =
1018            ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
1019        let coords = &[F2Dot14::from_f32(0.0)];
1020        let blend_state = BlendState::new(store, coords, 0).unwrap();
1021        let (_subfont, hint_params) = Subfont::new_hinted(
1022            private_dict_data,
1023            0..private_dict_data.len(),
1024            Some(blend_state),
1025            None,
1026        )
1027        .unwrap();
1028        assert_eq!(
1029            hint_params.family_other_blues,
1030            Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
1031        )
1032    }
1033
1034    #[test]
1035    fn subfont_cff() {
1036        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
1037        let cff =
1038            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1039        let subfont = cff.subfont(0, &[]).unwrap();
1040        assert_eq!(subfont.default_width, None);
1041        assert_eq!(subfont.nominal_width, Fixed::from_i32(598));
1042        assert_eq!(subfont.vs_index, 0);
1043        assert_eq!(subfont.matrix, None);
1044    }
1045
1046    fn make_blues<const N: usize>(values: [i32; N]) -> Blues {
1047        Blues::new(values.map(Fixed::from_i32).into_iter())
1048    }
1049
1050    #[test]
1051    fn hinted_subfont_cff() {
1052        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
1053        let cff =
1054            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1055        let (subfont, hinting) = cff.subfont_hinted(0, &[]).unwrap();
1056        assert_eq!(subfont.default_width, None);
1057        assert_eq!(subfont.nominal_width, Fixed::from_i32(598));
1058        assert_eq!(subfont.vs_index, 0);
1059        assert_eq!(subfont.matrix, None);
1060        let expected_hinting = HintingParams {
1061            blues: make_blues([-15, 0, 536, 547, 571, 582, 714, 726, 760, 772]),
1062            family_blues: Blues::default(),
1063            other_blues: make_blues([-255, -240]),
1064            family_other_blues: Blues::default(),
1065            blue_scale: Fixed::from_f64(0.0500030517578125),
1066            blue_shift: Fixed::from_f64(7.0),
1067            blue_fuzz: Fixed::ZERO,
1068            language_group: 0,
1069        };
1070        assert_eq!(hinting, expected_hinting);
1071    }
1072
1073    #[test]
1074    fn subfont_cff2() {
1075        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1076        let cff =
1077            CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
1078        let subfont = cff.subfont(0, &[]).unwrap();
1079        assert_eq!(subfont.default_width, None);
1080        assert_eq!(subfont.nominal_width, Fixed::ZERO);
1081        assert_eq!(subfont.vs_index, 0);
1082        assert_eq!(subfont.matrix, None);
1083    }
1084
1085    #[test]
1086    fn hinted_subfont_cff2() {
1087        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1088        let cff =
1089            CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
1090        let (subfont, hinting) = cff.subfont_hinted(0, &[]).unwrap();
1091        assert_eq!(subfont.default_width, None);
1092        assert_eq!(subfont.nominal_width, Fixed::ZERO);
1093        assert_eq!(subfont.vs_index, 0);
1094        assert_eq!(subfont.matrix, None);
1095        let expected_hinting = HintingParams {
1096            blues: make_blues([-10, 0, 482, 492, 694, 704, 739, 749]),
1097            family_blues: Blues::default(),
1098            other_blues: make_blues([-227, -217]),
1099            family_other_blues: Blues::default(),
1100            blue_scale: Fixed::from_f64(0.0625),
1101            blue_shift: Fixed::from_f64(7.0),
1102            blue_fuzz: Fixed::ONE,
1103            language_group: 0,
1104        };
1105        assert_eq!(hinting, expected_hinting);
1106    }
1107
1108    #[test]
1109    fn subfont_matrix() {
1110        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
1111        let cff =
1112            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1113        let subfont = cff.subfont(0, &[]).unwrap();
1114        assert_eq!(subfont.default_width, None);
1115        assert_eq!(subfont.nominal_width, Fixed::ZERO);
1116        assert_eq!(subfont.vs_index, 0);
1117        let expected_matrix = FontMatrix::from_elements([
1118            Fixed::from_i32(5),
1119            Fixed::ZERO,
1120            Fixed::ZERO,
1121            Fixed::from_i32(5),
1122            Fixed::ZERO,
1123            Fixed::ZERO,
1124        ]);
1125        let expected_scale = 10;
1126        assert_eq!(
1127            subfont.matrix,
1128            Some(ScaledFontMatrix {
1129                matrix: expected_matrix,
1130                scale: expected_scale
1131            })
1132        );
1133    }
1134
1135    #[test]
1136    fn eval_charstring_cff() {
1137        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
1138        let cff =
1139            CffFontRef::new_cff(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1140        let mut sink = CharstringCommandCounter::default();
1141        let subfont = cff.subfont(0, &[]).unwrap();
1142        cff.evaluate_charstring(&subfont, GlyphId::new(2), &[], &mut sink)
1143            .unwrap();
1144        // Charstring eval is tested elsewhere so just make sure we're processing the
1145        // *correct* charstring.
1146        assert_eq!(sink.0, 18);
1147    }
1148
1149    #[test]
1150    fn eval_charstring_cff2() {
1151        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1152        let cff =
1153            CffFontRef::new_cff2(font.cff2().unwrap().offset_data().as_bytes(), None).unwrap();
1154        let mut sink = CharstringCommandCounter::default();
1155        let subfont = cff.subfont(0, &[]).unwrap();
1156        cff.evaluate_charstring(&subfont, GlyphId::new(2), &[], &mut sink)
1157            .unwrap();
1158        // Charstring eval is tested elsewhere so just make sure we're processing the
1159        // *correct* charstring.
1160        assert_eq!(sink.0, 11);
1161    }
1162
1163    #[test]
1164    fn select_version() {
1165        // CFF2 font
1166        assert_eq!(
1167            CffFontRef::new(
1168                FontRef::new(font_test_data::CANTARELL_VF_TRIMMED)
1169                    .unwrap()
1170                    .cff2()
1171                    .unwrap()
1172                    .offset_data()
1173                    .as_bytes(),
1174                0,
1175                None
1176            )
1177            .unwrap()
1178            .version(),
1179            2
1180        );
1181        // CFF font
1182        assert_eq!(
1183            CffFontRef::new(
1184                FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX)
1185                    .unwrap()
1186                    .cff()
1187                    .unwrap()
1188                    .offset_data()
1189                    .as_bytes(),
1190                0,
1191                None
1192            )
1193            .unwrap()
1194            .version(),
1195            1
1196        );
1197        // Not a CFF font
1198        assert!(CffFontRef::new(&[0, 1, 4, 5], 0, None).is_err());
1199        // Spoof version 1
1200        assert!(CffFontRef::new(&[1, 1, 4, 5], 0, None).is_err());
1201        // Spoof version 2
1202        assert!(CffFontRef::new(&[2, 1, 4, 5], 0, None).is_err());
1203    }
1204
1205    #[test]
1206    fn transform() {
1207        // font with gnarly nested matrices
1208        let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET_MATRIX).unwrap();
1209        // this font has a fun upem of 512
1210        let cff =
1211            CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, Some(512)).unwrap();
1212        let subfont = cff.subfont(0, &[]).unwrap();
1213        // Extracted from FreeType
1214        let expected_matrix = [65536, 0, 5604, 65536, 0, 0].map(Fixed::from_bits);
1215        // Unscaled
1216        let transform = cff.transform(&subfont, None);
1217        assert_eq!(transform.matrix.elements(), expected_matrix);
1218        assert_eq!(transform.scale, Some(Fixed::from_bits(32 << 16)));
1219        // Scaled at 16px
1220        let transform = cff.transform(&subfont, Some(16.0));
1221        assert_eq!(transform.matrix.elements(), expected_matrix);
1222        assert_eq!(transform.scale, Some(Fixed::from_bits(1 << 16)));
1223    }
1224
1225    #[test]
1226    fn cff_encoding() {
1227        let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
1228        let cff = CffFontRef::new(font.cff().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1229        let encoding = cff.encoding().unwrap();
1230        assert_eq!(encoding.predefined(), Some(PredefinedEncoding::Standard));
1231    }
1232
1233    #[test]
1234    fn cff2_lacks_encoding() {
1235        let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1236        let cff = CffFontRef::new(font.cff2().unwrap().offset_data().as_bytes(), 0, None).unwrap();
1237        assert!(cff.encoding().is_none());
1238    }
1239}