woff2_patched/
decode.rs

1//! Interface for decoding WOFF2 files
2
3use bytes::Buf;
4use thiserror::Error;
5
6use crate::{
7    checksum::{calculate_font_checksum_adjustment, set_checksum_adjustment, ChecksumError},
8    magic_numbers::{TTF_CFF_FLAVOR, TTF_COLLECTION_FLAVOR, TTF_TRUE_TYPE_FLAVOR, WOFF2_SIGNATURE},
9    ttf_header::{calculate_header_size, TableDirectory},
10    woff2::{
11        collection_directory::{CollectionHeader, CollectionHeaderError},
12        header::{Woff2Header, Woff2HeaderError},
13        table_directory::{TableDirectoryError, Woff2TableDirectory, WriteTablesError, HEAD_TAG},
14    },
15};
16
17#[derive(Error, Debug)]
18pub enum DecodeError {
19    #[error("Invalid Woff2 File {0}")]
20    Invalid(String),
21    #[error("Unsupported feature {0}")]
22    Unsupported(&'static str),
23}
24
25impl From<ChecksumError> for DecodeError {
26    fn from(e: ChecksumError) -> Self {
27        DecodeError::Invalid(e.to_string())
28    }
29}
30
31impl From<CollectionHeaderError> for DecodeError {
32    fn from(e: CollectionHeaderError) -> Self {
33        DecodeError::Invalid(e.to_string())
34    }
35}
36
37impl From<TableDirectoryError> for DecodeError {
38    fn from(e: TableDirectoryError) -> Self {
39        DecodeError::Invalid(e.to_string())
40    }
41}
42
43impl From<Woff2HeaderError> for DecodeError {
44    fn from(e: Woff2HeaderError) -> Self {
45        DecodeError::Invalid(e.to_string())
46    }
47}
48
49impl From<WriteTablesError> for DecodeError {
50    fn from(e: WriteTablesError) -> Self {
51        match e {
52            WriteTablesError::Unsupported(e) => DecodeError::Unsupported(e),
53            _ => DecodeError::Invalid(e.to_string()),
54        }
55    }
56}
57
58impl From<std::io::Error> for DecodeError {
59    fn from(e: std::io::Error) -> Self {
60        DecodeError::Invalid(e.to_string())
61    }
62}
63
64/// Returns whether the buffer starts with the WOFF2 magic number.
65pub fn is_woff2(input_buffer: &[u8]) -> bool {
66    input_buffer.starts_with(&WOFF2_SIGNATURE.0)
67}
68
69/// Converts a WOFF2 font in `input_buffer` into a TTF format font.
70pub fn convert_woff2_to_ttf(input_buffer: &mut impl Buf) -> Result<Vec<u8>, DecodeError> {
71    let header = Woff2Header::from_buf(input_buffer)?;
72    header.is_valid_header()?;
73
74    if !matches!(
75        header.flavor,
76        TTF_COLLECTION_FLAVOR | TTF_CFF_FLAVOR | TTF_TRUE_TYPE_FLAVOR
77    ) {
78        Err(DecodeError::Invalid("Invalid font flavor".to_string()))?;
79    }
80
81    let table_directory = Woff2TableDirectory::from_buf(input_buffer, header.num_tables)?;
82
83    let mut collection_header = if header.flavor == TTF_COLLECTION_FLAVOR {
84        Some(CollectionHeader::from_buf(input_buffer, header.num_tables)?)
85    } else {
86        None
87    };
88
89    // for checking the compressed size
90    // let stream_start_remaining = input_buffer.remaining();
91
92    let mut decompressed_tables =
93        Vec::with_capacity(table_directory.uncompressed_length.try_into().unwrap());
94
95    brotli::BrotliDecompress(&mut input_buffer.reader(), &mut decompressed_tables)?;
96
97    // let compressed_size = stream_start_remaining - input_buffer.remaining();
98
99    // if compressed_size != usize::try_from(header.total_compressed_size).unwrap() + 1 {
100    //     Err(DecodeError::Invalid(
101    //         "Compressed stream size does not match header".to_string(),
102    //     ))?;
103    // }
104
105    let mut out_buffer = Vec::with_capacity(header.total_sfnt_size as usize);
106    // space for headers; we'll fill this in later once we've calculated table locations and
107    // checksums
108    let header_end = if let Some(collection_header) = &collection_header {
109        collection_header.calculate_header_size()
110    } else {
111        calculate_header_size(table_directory.tables.len())
112    };
113    out_buffer.resize(header_end, 0);
114    let ttf_tables = table_directory.write_to_buf(&mut out_buffer, &decompressed_tables)?;
115
116    let mut header_buffer = &mut out_buffer[..header_end];
117    if let Some(collection_header) = &mut collection_header {
118        // sort tables for each font
119        for font in &mut collection_header.fonts {
120            font.table_indices
121                .sort_unstable_by_key(|&idx| ttf_tables[idx as usize].tag.0);
122        }
123        collection_header.write_to_buf(&mut header_buffer, &ttf_tables);
124    } else {
125        let ttf_header = TableDirectory::new(header.flavor, ttf_tables);
126        ttf_header.write_to_buf(&mut header_buffer);
127        // calculate font checksum and store it at the appropriate location
128        let head_table_record = ttf_header
129            .find_table(HEAD_TAG)
130            .ok_or_else(|| DecodeError::Invalid("Missing `head` table".into()))?;
131        let checksum_adjustment = calculate_font_checksum_adjustment(&out_buffer);
132        let head_table = &mut out_buffer[head_table_record.get_range()];
133        set_checksum_adjustment(head_table, checksum_adjustment)?;
134    }
135
136    Ok(out_buffer)
137}
138
139#[cfg(test)]
140mod tests {
141    use std::io::Cursor;
142
143    use crate::test_resources::{FONTAWESOME_REGULAR_400, LATO_V22_LATIN_REGULAR};
144
145    use super::convert_woff2_to_ttf;
146
147    #[test]
148    fn read_sample_font() {
149        let buffer = LATO_V22_LATIN_REGULAR;
150        let ttf = convert_woff2_to_ttf(&mut Cursor::new(buffer)).unwrap();
151        assert_eq!(None, ttf_parser::fonts_in_collection(&ttf));
152        let _parsed_ttf = ttf_parser::Face::from_slice(&ttf, 1).unwrap();
153    }
154    #[test]
155    // Spec: https://www.w3.org/TR/WOFF2/#table_order
156    // The loca table MUST follow the glyf table in the table directory. When WOFF2 file contains individually encoded font file, the table directory MAY contain other tables inserted between glyf and loca tables; however when WOFF2 contains a font collection file each loca table MUST immediately follow its corresponding glyf table. For example, the following order of tables: 'cmap', 'glyf', 'hhea', 'hmtx', 'loca', 'maxp' ... is acceptable for individually encoded font files;
157    fn read_loca_is_not_after_glyf_font() {
158        // In this test font, the loca table does not follow the glyf table.
159        let buffer = FONTAWESOME_REGULAR_400;
160        let ttf = convert_woff2_to_ttf(&mut Cursor::new(buffer)).unwrap();
161        assert_eq!(None, ttf_parser::fonts_in_collection(&ttf));
162        let _parsed_ttf = ttf_parser::Face::from_slice(&ttf, 1).unwrap();
163    }
164
165    #[test]
166    fn sample_font_is_woff2() {
167        assert!(super::is_woff2(LATO_V22_LATIN_REGULAR));
168    }
169}