Skip to main content

fmd_font/
subset.rs

1//! Diagnosable, strict subset APIs. Legacy Option APIs retain their tolerance.
2use crate::{Font, be_u16, find_table_full, outline::OutlineError};
3
4/// Font program encoding; never infer a PDF dictionary from the filename.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum EmbeddingFormat {
7    /// sfnt with glyf outlines: PDF FontFile2 / CIDFontType2.
8    TrueType,
9    /// OTTO sfnt with CFF outlines: PDF FontFile3 / Subtype OpenType;
10    /// this is a name-keyed CFF program, not a CID-keyed program.
11    /// Consumers must choose a compatible simple-font encoding/dictionary;
12    /// do not emit FontFile2 or assume a CIDToGIDMap applies.
13    OpenTypeCff,
14}
15/// A compact font and its original-glyph to subset-glyph mapping.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Subset {
18    pub bytes: Vec<u8>,
19    /// Absent glyphs use [`crate::MISSING_GLYPH_REMAP`].
20    pub glyph_map: Vec<u16>,
21    pub format: EmbeddingFormat,
22}
23/// Stable categories independent of host paths and diagnostic prose.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SubsetErrorKind {
26    UnsupportedFormat,
27    UnsupportedOperator,
28    MissingTable,
29    InvalidGlyph,
30    Malformed,
31    BudgetExceeded,
32    Capacity,
33}
34/// Bounded failure context. Offsets identify the failing table structure
35/// (or glyph charstring when `glyph` is present), never a host file path.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct SubsetError {
38    pub kind: SubsetErrorKind,
39    pub table: [u8; 4],
40    pub glyph: Option<u16>,
41    pub offset: Option<usize>,
42}
43impl SubsetError {
44    pub(crate) const fn new(
45        kind: SubsetErrorKind,
46        table: [u8; 4],
47        glyph: Option<u16>,
48        offset: Option<usize>,
49    ) -> Self {
50        Self {
51            kind,
52            table,
53            glyph,
54            offset,
55        }
56    }
57}
58impl core::fmt::Display for SubsetError {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        write!(
61            f,
62            "font subset {:?} in {:?}, glyph {:?}, offset {:?}",
63            self.kind, self.table, self.glyph, self.offset
64        )
65    }
66}
67impl std::error::Error for SubsetError {}
68
69impl Font {
70    /// Strict subset for browser embedding. Unlike the legacy Option API,
71    /// invalid glyphs and malformed composites are refused, never repaired.
72    pub fn try_subset(&self, keep: &[char]) -> Result<Subset, SubsetError> {
73        let glyphs: Vec<_> = keep.iter().map(|&ch| self.glyph_index(ch)).collect();
74        self.try_subset_impl(&glyphs, keep, true)
75    }
76    /// Strict subset of a pre-shaped glyph set, with explicit embedding format.
77    pub fn try_subset_glyphs(
78        &self,
79        glyphs: &[u16],
80        cmap_chars: &[char],
81    ) -> Result<Subset, SubsetError> {
82        self.try_subset_impl(glyphs, cmap_chars, false)
83    }
84    /// Dense-map spelling for callers migrating from the legacy lookup API.
85    pub fn try_subset_glyphs_with_lookup(
86        &self,
87        glyphs: &[u16],
88        cmap_chars: &[char],
89    ) -> Result<Subset, SubsetError> {
90        self.try_subset_glyphs(glyphs, cmap_chars)
91    }
92    fn try_subset_impl(
93        &self,
94        glyphs: &[u16],
95        chars: &[char],
96        web: bool,
97    ) -> Result<Subset, SubsetError> {
98        if !self.has_glyf_outlines() {
99            return crate::cff::subset(self, glyphs, chars);
100        }
101        self.validate_font_metrics()?;
102        let (_, len) = find_table_full(&self.data, b"loca").ok_or(SubsetError::new(
103            SubsetErrorKind::MissingTable,
104            *b"loca",
105            None,
106            None,
107        ))?;
108        if len < (usize::from(self.num_glyphs) + 1) * if self.loca_long { 4 } else { 2 } {
109            return Err(SubsetError::new(
110                SubsetErrorKind::Malformed,
111                *b"loca",
112                None,
113                Some(len),
114            ));
115        }
116        let (bytes, glyph_map) = self.subset_core(glyphs, chars, web, true)?;
117        Ok(Subset {
118            bytes,
119            glyph_map,
120            format: EmbeddingFormat::TrueType,
121        })
122    }
123    pub(crate) fn validate_font_metrics(&self) -> Result<(), SubsetError> {
124        if self.units_per_em == 0 || self.num_glyphs == 0 {
125            return Err(SubsetError::new(
126                SubsetErrorKind::Malformed,
127                *b"head",
128                None,
129                None,
130            ));
131        }
132        for (tag, minimum) in [
133            (b"head", 54),
134            (b"hhea", 36),
135            (b"maxp", 6),
136            (
137                b"hmtx",
138                usize::from(self.num_h_metrics) * 4
139                    + usize::from(self.num_glyphs.saturating_sub(self.num_h_metrics)) * 2,
140            ),
141        ] {
142            let (offset, len) = find_table_full(&self.data, tag).ok_or(SubsetError::new(
143                SubsetErrorKind::MissingTable,
144                *tag,
145                None,
146                None,
147            ))?;
148            if len < minimum
149                || offset
150                    .checked_add(len)
151                    .is_none_or(|end| end > self.data.len())
152            {
153                return Err(SubsetError::new(
154                    SubsetErrorKind::Malformed,
155                    *tag,
156                    None,
157                    Some(len),
158                ));
159            }
160        }
161        if self.num_h_metrics == 0 || self.num_h_metrics > self.num_glyphs {
162            return Err(SubsetError::new(
163                SubsetErrorKind::Malformed,
164                *b"hhea",
165                None,
166                Some(34),
167            ));
168        }
169        Ok(())
170    }
171    pub(crate) fn validate_subset_glyph(&self, gid: u16) -> Result<(), SubsetError> {
172        let error = |kind, offset| SubsetError::new(kind, *b"glyf", Some(gid), offset);
173        if gid >= self.num_glyphs {
174            return Err(error(SubsetErrorKind::InvalidGlyph, None));
175        }
176        self.glyph_outline(gid).map_err(|e| {
177            error(
178                match e {
179                    OutlineError::BudgetExceeded => SubsetErrorKind::BudgetExceeded,
180                    _ => SubsetErrorKind::Malformed,
181                },
182                None,
183            )
184        })?;
185        let d = self
186            .glyph_data(gid)
187            .ok_or(error(SubsetErrorKind::Malformed, None))?;
188        if !self.is_composite(gid) {
189            return Ok(());
190        }
191        let mut p = 10;
192        loop {
193            let bad = error(SubsetErrorKind::Malformed, Some(p));
194            let flags = be_u16(d, p).ok_or(bad)?;
195            let child = be_u16(d, p + 2).ok_or(bad)?;
196            if child >= self.num_glyphs {
197                return Err(error(SubsetErrorKind::InvalidGlyph, Some(p + 2)));
198            }
199            p += 4 + if flags & 1 != 0 { 4 } else { 2 };
200            p += if flags & 8 != 0 {
201                2
202            } else if flags & 64 != 0 {
203                4
204            } else if flags & 128 != 0 {
205                8
206            } else {
207                0
208            };
209            if p > d.len() {
210                return Err(bad);
211            }
212            if flags & 32 == 0 {
213                if flags & 256 != 0 {
214                    let n = usize::from(be_u16(d, p).ok_or(bad)?);
215                    if p + 2 + n > d.len() {
216                        return Err(bad);
217                    }
218                }
219                return Ok(());
220            }
221        }
222    }
223}