Skip to main content

caj2pdf_core/pdf/
font.rs

1// SPDX-License-Identifier: MIT
2
3//! Ranged metadata access for explicitly supplied static OpenType fonts:
4//! TrueType or CFF outlines, standalone or in a collection.
5
6mod bands;
7mod cff;
8mod encoding;
9mod full;
10mod subset;
11pub(crate) use encoding::{CharacterMap, Characters, GlyphCharacters};
12pub(crate) use subset::{FontProgram, SubsetOutput};
13
14use crate::fallible::reserve_exact;
15use crate::{Cancellation, Error, Limits, RangedSource, Result, read_exact_at};
16use sha2::Digest;
17use std::rc::Rc;
18use xberg_ttf_parser::{Face, RawFaceTables};
19
20const MAX_TABLES: usize = 128;
21/// Maximum combined retained font metadata, independent of outline size.
22pub const MAX_FONT_METADATA_BYTES: u64 = 1024 * 1024;
23const METADATA_TAGS: [[u8; 4]; 8] = [
24    *b"head", *b"hhea", *b"maxp", *b"cmap", *b"hmtx", *b"OS/2", *b"post", *b"name",
25];
26/// Error reason when a font read again differs from its first read.
27pub(crate) const CHANGED: &str = "font source changed after its metadata was read";
28
29/// Whether BMP `code` is set in a 65,536-bit character bitmap.
30pub(crate) fn has_code(bitmap: &[u8], code: usize) -> bool {
31    bitmap[code / 8] & (1 << (code % 8)) != 0
32}
33
34/// Set BMP `code` in a 65,536-bit character bitmap.
35pub(crate) fn mark_code(bitmap: &mut [u8], code: usize) {
36    bitmap[code / 8] |= 1 << (code % 8);
37}
38
39/// Tables whose bytes stay in the source and are read again when subsetting.
40/// A font has either `glyf` and `loca` (with optional hinting tables) or
41/// `CFF `.
42const OUTLINE_TAGS: [[u8; 4]; 6] = [*b"glyf", *b"loca", *b"cvt ", *b"fpgm", *b"prep", *b"CFF "];
43const CFF: usize = 5;
44
45/// Font programs embedded for glyphs actually drawn in the document.
46#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
47pub enum FontEmbedding {
48    /// Embed only used glyphs (and their composite components).
49    #[default]
50    Subset,
51    /// Preserve complete TrueType programs, including unused glyphs and tables.
52    /// CFF outlines are refused when used. Unused resources remain subsets.
53    Full,
54}
55
56/// A Unicode glyph's ID and horizontal advance in font units.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct FontGlyph {
59    pub id: u16,
60    pub advance: u16,
61}
62
63/// Whether an OS/2 table of `version` with `fs_type` permits embedding a
64/// subset of the outlines. When a font sets several licensing bits, the
65/// least restrictive applies, as OS/2 versions 0-2 specify and readers
66/// apply to later versions too: a restricted license (bit 1) alone forbids
67/// embedding. The loader requires subset eligibility, so from version 2, which defines
68/// them, the no-subsetting (bit 8) and bitmap-only (bit 9) bits forbid it as
69/// well; versions 0 and 1 reserve those bits.
70fn permits_subset_embedding(version: u16, fs_type: u16) -> bool {
71    let licensing = fs_type & 0xf;
72    (licensing == 0 || licensing & 0xc != 0) && (version < 2 || fs_type & 0x300 == 0)
73}
74
75/// Metadata and a borrowed source for one face of a static OpenType font or
76/// collection, with TrueType (`glyf`/`loca`) or CFF outlines.
77///
78/// Only small metric/character tables and the CFF structures are retained;
79/// outline bytes remain in the ranged source. Subset embedding reads drawn
80/// glyphs; complete embedding streams the program. This validates metadata, not every glyph
81/// outline. Variable fonts and `CFF2` are not supported.
82pub struct OpenTypeFont<'a, S> {
83    pub(super) source: &'a mut S,
84    face: u32,
85    /// Bounded face directory retained for complete-program embedding.
86    header: [u8; 12],
87    directory: [[u8; 16]; MAX_TABLES],
88    table_count: usize,
89    face_base: u64,
90    collection: bool,
91    tables: [Vec<u8>; 8],
92    /// `(offset, length)` of each present [`OUTLINE_TAGS`] table.
93    outlines: [Option<(u64, u64)>; 6],
94    /// Parsed CFF structures of a CFF-flavoured OpenType font.
95    cff: Option<Rc<cff::Cff>>,
96    /// Font program bytes read again for embedding, counted toward input limits.
97    subset_bytes_read: u64,
98}
99
100impl<'a, S: RangedSource> OpenTypeFont<'a, S> {
101    /// Read face `face` of a TrueType font or TrueType collection (`ttcf`).
102    /// A standalone font has only face 0. Collection table offsets are
103    /// file-relative, so faces may share tables.
104    pub fn read<C: Cancellation>(
105        source: &'a mut S,
106        face: u32,
107        limits: &Limits,
108        cancellation: &C,
109    ) -> Result<Self> {
110        limits.validate()?;
111        limits.check_input_size(source.size())?;
112        let mut header = [0; 12];
113        read(source, 0, &mut header, limits, cancellation)?;
114        // The collection header and this face's directory hold no tables.
115        let (base, collection_end) = if let Some((faces, dsig)) = collection(&header)? {
116            if face >= faces {
117                return Err(invalid("TrueType collection face index is out of range"));
118            }
119            let mut offset = [0; 4];
120            read(
121                source,
122                12 + 4 * u64::from(face),
123                &mut offset,
124                limits,
125                cancellation,
126            )?;
127            let base = u64::from(u32::from_be_bytes(offset));
128            read(source, base, &mut header, limits, cancellation)?;
129            (base, 12 + 4 * u64::from(faces) + dsig)
130        } else if face != 0 {
131            return Err(invalid("a standalone font has only face 0"));
132        } else {
133            (0, 0)
134        };
135        if !is_font_tag(&header) {
136            return Err(invalid("font must be an OpenType font or collection face"));
137        }
138        let count = usize::from(u16::from_be_bytes([header[4], header[5]]));
139        if count == 0 || count > MAX_TABLES {
140            return Err(invalid("unsupported TrueType table count"));
141        }
142        let directory_end = base + 12 + 16 * count as u64;
143        let mut directory = [[0_u8; 16]; MAX_TABLES];
144        let mut metadata_bytes = 0_u64;
145        let mut outlines = [None; 6];
146        for i in 0..count {
147            read(
148                source,
149                base + 12 + 16 * i as u64,
150                &mut directory[i],
151                limits,
152                cancellation,
153            )?;
154            let entry = directory[i];
155            let tag = &entry[..4];
156            if i != 0 && directory[i - 1][..4] >= *tag {
157                return Err(invalid("TrueType table tags must be unique and sorted"));
158            }
159            if tag == b"fvar" || tag == b"CFF2" {
160                return Err(invalid("variable fonts are not supported"));
161            }
162            let (offset, length) = span(&entry);
163            // Tables should be 4-byte aligned, but installed fonts such as
164            // WenQuanYi Zen Hei are not; readers accept any offset.
165            if (offset < directory_end && base < offset + length.max(1))
166                || offset < collection_end
167                || offset + length > source.size()
168            {
169                return Err(invalid("TrueType table range is outside the source"));
170            }
171            for previous in &directory[..i] {
172                let (start, len) = span(previous);
173                if length != 0 && len != 0 && offset < start + len && start < offset + length {
174                    return Err(invalid("TrueType table ranges overlap"));
175                }
176            }
177            if let Some(slot) = OUTLINE_TAGS.iter().position(|wanted| wanted == tag) {
178                outlines[slot] = Some((offset, length));
179            }
180            if METADATA_TAGS.iter().any(|wanted| wanted == tag) {
181                metadata_bytes += length;
182            }
183        }
184        let truetype = outlines[0].is_some() && outlines[1].is_some();
185        if truetype == outlines[CFF].is_some() {
186            return Err(invalid("font needs either glyf and loca or CFF outlines"));
187        }
188        if metadata_bytes > MAX_FONT_METADATA_BYTES {
189            return Err(Error::limit(
190                "font metadata bytes",
191                MAX_FONT_METADATA_BYTES,
192                metadata_bytes,
193            ));
194        }
195        limits.check_allocation(metadata_bytes)?;
196        let mut tables: [Vec<u8>; 8] = Default::default();
197        for (tag, table) in METADATA_TAGS.iter().zip(&mut tables) {
198            let entry = directory[..count]
199                .iter()
200                .find(|entry| entry[..4] == *tag)
201                .ok_or(invalid("required TrueType metadata table is missing"))?;
202            let (offset, length) = span(entry);
203            let refused = limits.allocation_refused("font metadata bytes", length);
204            reserve_exact(table, length as usize, refused)?;
205            table.resize(length as usize, 0);
206            read(source, offset, table, limits, cancellation)?;
207        }
208        let mut font = Self {
209            source,
210            face,
211            header,
212            directory,
213            table_count: count,
214            face_base: base,
215            collection: collection_end != 0,
216            tables,
217            outlines,
218            cff: None,
219            subset_bytes_read: 0,
220        };
221        let face = font.face()?;
222        if face.tables().cmap.is_none()
223            || face.tables().hmtx.is_none()
224            || face.tables().os2.is_none()
225            || face.tables().post.is_none()
226        {
227            return Err(invalid("invalid required TrueType metadata table"));
228        }
229        // A BMP mapping pass performs a fixed number of character lookups.
230        // Bound their per-character table search independently of byte size.
231        let maps = face
232            .tables()
233            .cmap
234            .as_ref()
235            .map_or(0, |cmap| cmap.subtables.len());
236        if maps > 16 {
237            return Err(Error::limit("font character maps", 16, u64::from(maps)));
238        }
239        // `version` and `fsType` of the required OS/2 table.
240        let field = |at: usize| {
241            font.tables[5]
242                .get(at..at + 2)
243                .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]]))
244        };
245        let permitted = field(0)
246            .zip(field(8))
247            .is_some_and(|(version, fs_type)| permits_subset_embedding(version, fs_type));
248        if !permitted {
249            return Err(invalid("font metadata does not permit subset embedding"));
250        }
251        if let Some(span) = font.outlines[CFF] {
252            let glyphs = face.number_of_glyphs();
253            font.cff = Some(Rc::new(cff::Cff::read(
254                font.source,
255                span,
256                glyphs,
257                limits,
258                cancellation,
259            )?));
260        }
261        Ok(font)
262    }
263
264    /// The number of faces in `source`: a collection's declared face count,
265    /// or 1 for a standalone font. Only the 12-byte header is read, so the
266    /// faces themselves are not validated; [`Self::read`] validates one.
267    pub fn face_count<C: Cancellation>(
268        source: &mut S,
269        limits: &Limits,
270        cancellation: &C,
271    ) -> Result<u32> {
272        limits.validate()?;
273        limits.check_input_size(source.size())?;
274        let mut header = [0; 12];
275        read(source, 0, &mut header, limits, cancellation)?;
276        match collection(&header)? {
277            Some((faces, _)) => Ok(faces),
278            None if is_font_tag(&header) => Ok(1),
279            None => Err(invalid("font must be an OpenType font or collection face")),
280        }
281    }
282
283    pub fn units_per_em(&self) -> Result<u16> {
284        Ok(self.face()?.units_per_em())
285    }
286
287    /// Reject missing glyphs instead of silently selecting `.notdef`.
288    pub fn glyph(&self, character: char) -> Result<FontGlyph> {
289        let face = self.face()?;
290        let id = face
291            .glyph_index(character)
292            .filter(|id| id.0 != 0 && id.0 < face.number_of_glyphs())
293            .ok_or(invalid("font has no glyph for the requested character"))?;
294        let advance = face
295            .glyph_hor_advance(id)
296            .ok_or(invalid("font glyph has no horizontal advance"))?;
297        Ok(FontGlyph { id: id.0, advance })
298    }
299
300    pub(super) fn face(&self) -> Result<Face<'_>> {
301        Face::from_raw_tables(RawFaceTables {
302            head: &self.tables[0],
303            hhea: &self.tables[1],
304            maxp: &self.tables[2],
305            cmap: Some(&self.tables[3]),
306            hmtx: Some(&self.tables[4]),
307            os2: Some(&self.tables[5]),
308            post: Some(&self.tables[6]),
309            name: Some(&self.tables[7]),
310            ..Default::default()
311        })
312        .map_err(|_| invalid("invalid required TrueType face metadata"))
313    }
314
315    /// The `head` table's checkSumAdjustment, from the retained metadata.
316    pub fn checksum_adjustment(&self) -> Result<u32> {
317        self.tables[0]
318            .get(8..12)
319            .map(|bytes| u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
320            .ok_or(invalid("invalid required TrueType face metadata"))
321    }
322
323    /// Whether the font has CFF rather than TrueType outlines.
324    pub(crate) fn is_cff(&self) -> bool {
325        self.cff.is_some()
326    }
327
328    /// Digest of the directory, retained metadata and outline table ranges, identifying
329    /// the font when its source is read again.
330    pub(crate) fn fingerprint(&self) -> [u8; 32] {
331        let mut hash = sha2::Sha256::new().chain_update(self.face.to_be_bytes());
332        hash.update(self.header);
333        for entry in &self.directory[..self.table_count] {
334            hash.update(entry);
335        }
336        for table in &self.tables {
337            hash.update((table.len() as u64).to_be_bytes());
338            hash.update(table);
339        }
340        for (offset, length) in self.outlines.iter().flatten() {
341            hash.update(offset.to_be_bytes());
342            hash.update(length.to_be_bytes());
343        }
344        hash.finalize().into()
345    }
346
347    /// The `(glyph, CID)` of each used character, in CID order.
348    /// A character the font no longer maps means its source
349    /// changed after the metadata was read.
350    pub(super) fn used_glyphs(
351        &self,
352        used: Characters<'_>,
353        limits: &Limits,
354    ) -> Result<Vec<(u16, u16)>> {
355        let face = self.face()?;
356        let count = face.number_of_glyphs();
357        let length = used.codes().count();
358        let bytes = length as u64 * size_of::<(u16, u16)>() as u64;
359        limits.check_allocation(bytes)?;
360        let mut glyphs = Vec::new();
361        reserve_exact(
362            &mut glyphs,
363            length,
364            limits.allocation_refused("font used glyphs", bytes),
365        )?;
366        for code in used.codes() {
367            let glyph = used
368                .glyph(usize::from(code))
369                .and_then(|character| face.glyph_index(character))
370                .filter(|id| id.0 != 0 && id.0 < count)
371                .map(|id| (id.0, code))
372                .ok_or(invalid(CHANGED))?;
373            glyphs.push(glyph);
374        }
375        Ok(glyphs)
376    }
377
378    /// The face's PostScript name (name ID 6), validated as 1 to 63
379    /// printable ASCII characters without PDF delimiters.
380    pub fn postscript_name(&self) -> Result<String> {
381        let face = self.face()?;
382        let name = face
383            .names()
384            .into_iter()
385            .find(|name| name.name_id == 6 && name.is_unicode())
386            .ok_or(invalid("font requires a Unicode PostScript name"))?;
387        if name.name.is_empty() || name.name.len() > 126 || name.name.len() % 2 != 0 {
388            return Err(invalid(
389                "font PostScript name must have 1 to 63 ASCII characters",
390            ));
391        }
392        let name = name
393            .to_string()
394            .ok_or(invalid("font PostScript name is invalid UTF-16"))?;
395        if !is_valid_postscript_name(&name) {
396            return Err(invalid("font PostScript name contains invalid characters"));
397        }
398        Ok(name)
399    }
400}
401
402/// 1 to 63 printable ASCII characters without PDF delimiters.
403pub(crate) fn is_valid_postscript_name(name: &str) -> bool {
404    (1..=63).contains(&name.len())
405        && name
406            .bytes()
407            .all(|byte| (33..=126).contains(&byte) && !b"[](){}<>/%".contains(&byte))
408}
409
410/// The face count and DSIG header bytes of a TrueType collection header, or
411/// `None` for any other font header.
412fn collection(header: &[u8; 12]) -> Result<Option<(u32, u64)>> {
413    if header[..4] != *b"ttcf" {
414        return Ok(None);
415    }
416    // Version 2 adds three 32-bit DSIG fields to the header.
417    let dsig = match u16::from_be_bytes([header[4], header[5]]) {
418        1 => 0,
419        2 => 12,
420        _ => return Err(invalid("unsupported TrueType collection version")),
421    };
422    Ok(Some((
423        u32::from_be_bytes(header[8..12].try_into().unwrap()),
424        dsig,
425    )))
426}
427
428/// Whether a font header starts with a TrueType or CFF OpenType tag.
429fn is_font_tag(header: &[u8; 12]) -> bool {
430    header[..4] == [0, 1, 0, 0] || header[..4] == *b"true" || header[..4] == *b"OTTO"
431}
432
433fn span(entry: &[u8; 16]) -> (u64, u64) {
434    (
435        u64::from(u32::from_be_bytes(entry[8..12].try_into().unwrap())),
436        u64::from(u32::from_be_bytes(entry[12..16].try_into().unwrap())),
437    )
438}
439
440fn invalid(reason: &'static str) -> Error {
441    Error::invalid(reason)
442}
443
444fn read<S: RangedSource, C: Cancellation>(
445    source: &mut S,
446    offset: u64,
447    bytes: &mut [u8],
448    limits: &Limits,
449    cancellation: &C,
450) -> Result<()> {
451    for (index, chunk) in bytes.chunks_mut(limits.io_chunk_bytes).enumerate() {
452        read_exact_at(
453            source,
454            offset + (index * limits.io_chunk_bytes) as u64,
455            chunk,
456            limits,
457            cancellation,
458        )?;
459    }
460    Ok(())
461}
462
463#[cfg(test)]
464pub(crate) mod tests;