font_subset/font/
woff2.rs

1//! WOFF2 font parsing.
2
3use super::{Cursor, Font};
4use crate::{alloc::Vec, utils::brotli, ParseError, ParseErrorKind, TableTag};
5
6impl Cursor<'_> {
7    fn read_u8(&mut self) -> Result<u8, ParseError> {
8        let [a, rest @ ..] = self.bytes else {
9            return Err(self.err(ParseErrorKind::UnexpectedEof));
10        };
11        self.bytes = rest;
12        self.offset += 1;
13        Ok(*a)
14    }
15
16    // visible for testing
17    pub(crate) fn read_uint_base128(&mut self) -> Result<u32, ParseError> {
18        let offset = self.offset;
19        let mut val = 0_u32;
20        for _ in 0..5 {
21            let byte = self.read_u8()?;
22            val = (val << 7) + u32::from(byte & 0x7f);
23            if byte < 0x80 {
24                // This is the terminal byte
25                return Ok(val);
26            }
27        }
28        Err(ParseError {
29            kind: ParseErrorKind::UintBase128,
30            table: self.table,
31            offset,
32        })
33    }
34}
35
36impl TableTag {
37    pub(crate) const NULL_TRANSFORM_MASK: u8 = 0b_1100_0000;
38
39    fn parse_woff2(cursor: &mut Cursor<'_>) -> Result<Self, ParseError> {
40        // Stash the cursor for error handling.
41        let start_cursor = *cursor;
42
43        let raw_tag = cursor.read_u8()?;
44        let tag = Self::from_u8(raw_tag);
45        let tag = if let Some(tag) = tag {
46            tag
47        } else {
48            Self(cursor.read_byte_array::<4>()?)
49        };
50
51        let transform_bits = raw_tag & Self::NULL_TRANSFORM_MASK;
52        let expected_transform = match tag {
53            Self::GLYF | Self::LOCA => Self::NULL_TRANSFORM_MASK,
54            _ => 0,
55        };
56        if transform_bits != expected_transform {
57            return Err(start_cursor.err(ParseErrorKind::UnsupportedWoff2Table {
58                tag,
59                transform_bits: transform_bits >> 6,
60            }));
61        }
62
63        Ok(tag)
64    }
65}
66
67#[derive(Debug, Clone, Copy)]
68struct Woff2TableRecord {
69    tag: TableTag,
70    len: u32,
71}
72
73impl Woff2TableRecord {
74    fn parse(cursor: &mut Cursor<'_>) -> Result<Self, ParseError> {
75        let tag = TableTag::parse_woff2(cursor)?;
76        let len = cursor.read_uint_base128()?;
77        // Since we don't support non-null transforms, we don't need to read the transformed table length.
78        #[cfg(feature = "tracing")]
79        tracing::debug!(?tag, len, "parsed table record");
80        Ok(Self { tag, len })
81    }
82}
83
84impl Font<'_> {
85    pub(crate) const WOFF2_SIGNATURE: u32 = 0x_774f_4632;
86}
87
88/// Reader for files in the WOFF2 format.
89///
90/// Unlike [`OpenTypeReader`](super::OpenTypeReader), this reader owns the table data since it needs
91/// to be decompressed. As a result, [`Self::read()`] will borrow the data from the reader itself,
92/// not from the original font bytes.
93#[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
94#[derive(Debug, Clone)]
95pub struct Woff2Reader {
96    table_records: Vec<Woff2TableRecord>,
97    table_data: Vec<u8>,
98}
99
100impl Woff2Reader {
101    /// Creates a reader from the specified raw bytes.
102    ///
103    /// This will parse the WOFF2 header and table records and decompress the table data.
104    ///
105    /// # Errors
106    ///
107    /// Returns parsing / decompression errors if any are encountered.
108    #[allow(clippy::missing_panics_doc)] // false positive
109    #[cfg_attr(
110        feature = "tracing",
111        tracing::instrument(
112            level = "debug",
113            name = "Woff2Reader::new",
114            err,
115            skip_all,
116            fields(bytes.len = bytes.len()),
117        )
118    )]
119    pub fn new(bytes: &[u8]) -> Result<Self, ParseError> {
120        let mut header_cursor = Cursor::new(bytes);
121        let bytes_len = u32::try_from(bytes.len())
122            .map_err(|_| header_cursor.err(ParseErrorKind::TooLargeFont(bytes.len())))?;
123
124        header_cursor
125            .read_u32_checked(|signature| check_exact!(signature, Font::WOFF2_SIGNATURE))?;
126        header_cursor.read_u32_checked(|version| check_exact!(version, Font::SFNT_VERSION))?;
127
128        header_cursor.read_u32_checked(|file_len| check_exact!(file_len, bytes_len))?;
129        let table_count = header_cursor.read_u16()?;
130        header_cursor.skip(6)?; // reserved, decompressed_len
131        let compressed_data_len = header_cursor.read_u32()?;
132        let compressed_data_len = usize::try_from(compressed_data_len).unwrap();
133        header_cursor.skip(24)?; // WOFF version ..= private block length
134
135        #[cfg(feature = "tracing")]
136        tracing::debug!(table_count, compressed_data_len, "parsed header");
137
138        let table_records = (0..table_count)
139            .map(|_| Woff2TableRecord::parse(&mut header_cursor))
140            .collect::<Result<Vec<_>, _>>()?;
141
142        let data_cursor = header_cursor.read_range(0..compressed_data_len)?;
143        #[cfg(feature = "tracing")]
144        tracing::debug!(range = ?data_cursor.range(), "decompressing table data");
145        let table_data = brotli::decompress(data_cursor.bytes())
146            .map_err(|()| data_cursor.err(ParseErrorKind::BrotliDecompression))?;
147        #[cfg(feature = "tracing")]
148        tracing::debug!(table_data.len = table_data.len(), "decompressed table data");
149
150        Ok(Self {
151            table_records,
152            table_data,
153        })
154    }
155
156    /// Returns the byte size of the equivalent OpenType font file.
157    pub fn opentype_len(&self) -> usize {
158        let table_size = self
159            .iter()
160            .map(|(_, cursor)| cursor.bytes().len().div_ceil(4) * 4 + Font::TABLE_RECORD_LEN)
161            .sum::<usize>();
162        table_size + Font::SFNT_HEADER_LEN
163    }
164
165    // visible for testing
166    pub(crate) fn iter(&self) -> impl ExactSizeIterator<Item = (TableTag, Cursor<'_>)> + '_ {
167        let mut offset = 0_usize;
168        self.table_records.iter().map(move |record| {
169            let table_offset = offset;
170            offset += usize::try_from(record.len).unwrap();
171            let tag = record.tag;
172            let table_data = &self.table_data[table_offset..offset];
173            let table_cursor = Cursor::for_table(table_data, table_offset, tag);
174            (tag, table_cursor)
175        })
176    }
177
178    /// Iterates over all tables in the file (including ones that are not processed by [`Font`]).
179    pub fn raw_tables(&self) -> impl ExactSizeIterator<Item = (TableTag, &[u8])> + '_ {
180        self.iter().map(|(tag, cursor)| (tag, cursor.bytes()))
181    }
182
183    /// Reads a [`Font`] from this reader. The font will borrow data from the reader.
184    ///
185    /// # Errors
186    ///
187    /// Returns parsing errors (e.g., on missing required tables).
188    pub fn read(&self) -> Result<Font<'_>, ParseError> {
189        Font::from_tables(self.iter())
190    }
191
192    pub(super) fn into_table_data(self) -> Vec<u8> {
193        self.table_data
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn all_woff2_tables_are_covered() {
203        for val in 0_u8..=62 {
204            let table = TableTag::from_u8(val).unwrap();
205            assert_eq!(table, TableTag::from_u8(val + 64).unwrap());
206            assert_eq!(table, TableTag::from_u8(val + 128).unwrap());
207            assert_eq!(table, TableTag::from_u8(val + 192).unwrap());
208        }
209    }
210}