Skip to main content

woff2/
decode.rs

1//! Interface for decoding WOFF2 files
2
3use alloc::string::{String, ToString};
4use alloc::vec::Vec;
5use bytes::Buf;
6use thiserror::Error;
7
8use crate::brotli::decompress_to_vec;
9use crate::checksum::ChecksumError;
10use crate::checksum::{calculate_font_checksum_adjustment, set_checksum_adjustment};
11use crate::magic::*;
12use crate::sfnt::{calculate_header_size, TableDirectory};
13use crate::woff2::collection::{CollectionHeader, CollectionHeaderError};
14use crate::woff2::header::{Woff2Header, Woff2HeaderError};
15use crate::woff2::tables::{TableDirectoryError, Woff2TableDirectory};
16use crate::woff2::tables::{WriteTablesError, HEAD_TAG};
17
18#[derive(Error, Debug)]
19pub enum DecodeError {
20    #[error("Invalid Woff2 File {0}")]
21    Invalid(String),
22    #[error("Unsupported feature {0}")]
23    Unsupported(&'static str),
24}
25
26impl From<ChecksumError> for DecodeError {
27    fn from(e: ChecksumError) -> Self {
28        DecodeError::invalid(e)
29    }
30}
31
32impl From<CollectionHeaderError> for DecodeError {
33    fn from(e: CollectionHeaderError) -> Self {
34        DecodeError::invalid(e)
35    }
36}
37
38impl From<TableDirectoryError> for DecodeError {
39    fn from(e: TableDirectoryError) -> Self {
40        DecodeError::invalid(e)
41    }
42}
43
44impl From<Woff2HeaderError> for DecodeError {
45    fn from(e: Woff2HeaderError) -> Self {
46        DecodeError::invalid(e)
47    }
48}
49
50impl From<WriteTablesError> for DecodeError {
51    fn from(e: WriteTablesError) -> Self {
52        match e {
53            WriteTablesError::Unsupported(e) => DecodeError::Unsupported(e),
54            _ => DecodeError::invalid(e),
55        }
56    }
57}
58
59#[cfg(feature = "std")]
60impl From<std::io::Error> for DecodeError {
61    fn from(error: std::io::Error) -> Self {
62        DecodeError::invalid(error)
63    }
64}
65
66impl From<&'static str> for DecodeError {
67    fn from(e: &'static str) -> Self {
68        DecodeError::Invalid(e.into())
69    }
70}
71
72impl DecodeError {
73    fn invalid<E: ToString>(error: E) -> Self {
74        Self::Invalid(error.to_string())
75    }
76}
77
78/// Returns whether the buffer starts with the WOFF2 magic number.
79pub fn is_woff2(input_buffer: &[u8]) -> bool {
80    input_buffer.starts_with(&WOFF2_SIGNATURE.0)
81}
82
83/// Converts a WOFF2 font in `input_buffer` into a TTF format font.
84pub fn convert_woff2_to_ttf(input_buffer: &mut impl Buf) -> Result<Vec<u8>, DecodeError> {
85    let header = Woff2Header::from_buf(input_buffer)?;
86    header.is_valid_header()?;
87
88    if !matches!(
89        header.flavor,
90        TTF_COLLECTION_FLAVOR | TTF_CFF_FLAVOR | TTF_TRUE_TYPE_FLAVOR
91    ) {
92        Err(DecodeError::Invalid("Invalid font flavor".into()))?;
93    }
94
95    let table_directory = Woff2TableDirectory::from_buf(input_buffer, header.num_tables)?;
96
97    let mut collection_header = if header.flavor == TTF_COLLECTION_FLAVOR {
98        Some(CollectionHeader::from_buf(input_buffer, header.num_tables)?)
99    } else {
100        None
101    };
102
103    let compressed_stream_size = usize::try_from(header.total_compressed_size).unwrap();
104    if input_buffer.remaining() < compressed_stream_size {
105        Err(DecodeError::Invalid("Truncated compressed stream".into()))?;
106    }
107
108    let mut compressed_stream = (&mut *input_buffer).take(compressed_stream_size);
109    let mut decompressed_tables = Vec::with_capacity(table_directory.uncompressed_length as usize);
110    decompress_to_vec(&mut compressed_stream, &mut decompressed_tables)?;
111
112    if compressed_stream.remaining() != 0 {
113        Err(DecodeError::Invalid(
114            "Compressed stream size does not match header".into(),
115        ))?;
116    }
117
118    let mut out_buffer = Vec::with_capacity(header.total_sfnt_size as usize);
119    let header_end = if let Some(collection_header) = &collection_header {
120        collection_header.calculate_header_size()
121    } else {
122        calculate_header_size(table_directory.tables.len())
123    };
124    out_buffer.resize(header_end, 0);
125    let ttf_tables = table_directory.write_to_buf(&mut out_buffer, &decompressed_tables)?;
126
127    let mut header_buffer = &mut out_buffer[..header_end];
128    if let Some(collection_header) = &mut collection_header {
129        for font in &mut collection_header.fonts {
130            font.table_indices
131                .sort_unstable_by_key(|&idx| ttf_tables[idx as usize].tag.0);
132        }
133        collection_header.write_to_buf(&mut header_buffer, &ttf_tables);
134    } else {
135        let ttf_header = TableDirectory::new(header.flavor, ttf_tables);
136        ttf_header.write_to_buf(&mut header_buffer);
137        let head_table_record = ttf_header
138            .find_table(HEAD_TAG)
139            .ok_or_else(|| DecodeError::Invalid("Missing `head` table".into()))?;
140        let checksum_adjustment = calculate_font_checksum_adjustment(&out_buffer);
141        let head_table = &mut out_buffer[head_table_record.get_range()];
142        set_checksum_adjustment(head_table, checksum_adjustment)?;
143    }
144
145    Ok(out_buffer)
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::test_data::{FONTAWESOME_REGULAR_400, LATO_V22_LATIN_REGULAR};
151
152    use super::convert_woff2_to_ttf;
153
154    #[test]
155    fn read_sample_font() {
156        let buffer = LATO_V22_LATIN_REGULAR;
157        let ttf = convert_woff2_to_ttf(&mut &buffer[..]).unwrap();
158        assert_eq!(None, ttf_parser::fonts_in_collection(&ttf));
159        let _parsed_ttf = ttf_parser::Face::parse(&ttf, 0).unwrap();
160    }
161
162    #[test]
163    fn read_loca_is_not_after_glyf_font() {
164        let buffer = FONTAWESOME_REGULAR_400;
165        let ttf = convert_woff2_to_ttf(&mut &buffer[..]).unwrap();
166        assert_eq!(None, ttf_parser::fonts_in_collection(&ttf));
167        let _parsed_ttf = ttf_parser::Face::parse(&ttf, 0).unwrap();
168    }
169
170    #[test]
171    fn sample_font_is_woff2() {
172        assert!(super::is_woff2(LATO_V22_LATIN_REGULAR));
173    }
174}