1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//!  A builder for top-level font objects

use std::collections::BTreeMap;
use std::{borrow::Cow, fmt::Display};

use read_fonts::{FontRef, TableProvider};
use types::{Tag, TT_SFNT_VERSION};

include!("../generated/generated_font.rs");

const TABLE_RECORD_LEN: usize = 16;

/// Build a font from some set of tables.
#[derive(Debug, Clone, Default)]
pub struct FontBuilder<'a> {
    tables: BTreeMap<Tag, Cow<'a, [u8]>>,
}

/// An error returned when attempting to add a table to the builder.
///
/// This wraps a compilation error, adding the tag of the table where it was
/// encountered.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BuilderError {
    /// The tag of the root table where the error occured
    pub tag: Tag,
    /// The underlying error
    pub inner: crate::error::Error,
}

impl TableDirectory {
    pub fn from_table_records(table_records: Vec<TableRecord>) -> TableDirectory {
        assert!(table_records.len() <= u16::MAX as usize);

        // See https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
        // Computation works at the largest allowable num tables so don't stress the as u16's
        let entry_selector = (table_records.len() as f64).log2().floor() as u16;
        let search_range = (2.0_f64.powi(entry_selector as i32) * 16.0) as u16;
        // The result doesn't really make sense with 0 tables but ... let's at least not fail
        let range_shift = (table_records.len() * 16).saturating_sub(search_range as usize) as u16;

        TableDirectory::new(
            TT_SFNT_VERSION,
            search_range,
            entry_selector,
            range_shift,
            table_records,
        )
    }
}

impl<'a> FontBuilder<'a> {
    /// Create a new builder to compile a binary font
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a table to the builder.
    ///
    /// The table can be any top-level table defined in this crate. This function
    /// will attempt to compile the table and then add it to the builder if
    /// successful, returning an error otherwise.
    pub fn add_table<T>(&mut self, table: &T) -> Result<&mut Self, BuilderError>
    where
        T: FontWrite + Validate + TopLevelTable,
    {
        let tag = T::TAG;
        let bytes = crate::dump_table(table).map_err(|inner| BuilderError { inner, tag })?;
        Ok(self.add_raw(tag, bytes))
    }

    /// A builder method to add raw data for the provided tag
    pub fn add_raw(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &mut Self {
        self.tables.insert(tag, data.into());
        self
    }

    /// Copy each table from the source font if it does not already exist
    pub fn copy_missing_tables(&mut self, font: FontRef<'a>) -> &mut Self {
        for record in font.table_directory.table_records() {
            let tag = record.tag();
            if !self.tables.contains_key(&tag) {
                if let Some(data) = font.data_for_tag(tag) {
                    self.add_raw(tag, data);
                } else {
                    log::warn!("data for '{tag}' is malformed");
                }
            }
        }
        self
    }

    /// Returns `true` if the builder contains a table with this tag.
    pub fn contains(&self, tag: Tag) -> bool {
        self.tables.contains_key(&tag)
    }

    /// Assemble all the tables into a binary font file with a [Table Directory].
    ///
    /// [Table Directory]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
    pub fn build(&mut self) -> Vec<u8> {
        let header_len = std::mem::size_of::<u32>() // sfnt
            + std::mem::size_of::<u16>() * 4 // num_tables to range_shift
            + self.tables.len() * TABLE_RECORD_LEN;

        let mut position = header_len as u32;
        let table_records: Vec<_> = self
            .tables
            .iter_mut()
            .map(|(tag, data)| {
                let offset = position;
                let length = data.len() as u32;
                position += length;
                let (checksum, padding) = checksum_and_padding(data);
                position += padding;
                TableRecord::new(*tag, checksum, offset, length)
            })
            .collect();

        let directory = TableDirectory::from_table_records(table_records);

        let mut writer = TableWriter::default();
        directory.write_into(&mut writer);
        let mut data = writer.into_data().bytes;
        for table in self.tables.values() {
            data.extend_from_slice(table);
            let rem = round4(table.len()) - table.len();
            let padding = [0u8; 4];
            data.extend_from_slice(&padding[..rem]);
        }
        data
    }
}

/// <https://github.com/google/woff2/blob/a0d0ed7da27b708c0a4e96ad7a998bddc933c06e/src/round.h#L19>
fn round4(sz: usize) -> usize {
    (sz + 3) & !3
}

fn checksum_and_padding(table: &[u8]) -> (u32, u32) {
    let padding = round4(table.len()) - table.len();
    let mut sum = 0u32;
    let mut iter = table.chunks_exact(4);
    for quad in &mut iter {
        // this can't fail, and we trust the compiler to avoid a branch
        let array: [u8; 4] = quad.try_into().unwrap_or_default();
        sum = sum.wrapping_add(u32::from_be_bytes(array));
    }

    let rem = match *iter.remainder() {
        [a] => u32::from_be_bytes([a, 0, 0, 0]),
        [a, b] => u32::from_be_bytes([a, b, 0, 0]),
        [a, b, c] => u32::from_be_bytes([a, b, c, 0]),
        _ => 0,
    };

    (sum.wrapping_add(rem), padding as u32)
}

impl TTCHeader {
    fn compute_version(&self) -> MajorMinor {
        panic!("TTCHeader writing not supported (yet)")
    }
}

impl Display for BuilderError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "failed to build '{}' table: '{}'", self.tag, self.inner)
    }
}

impl std::error::Error for BuilderError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.inner)
    }
}

#[cfg(test)]
mod tests {
    use font_types::Tag;
    use read_fonts::FontRef;

    use crate::{font_builder::checksum_and_padding, FontBuilder};

    #[test]
    fn sets_binary_search_assists() {
        // Based on Roboto's num tables
        let data = b"doesn't matter".to_vec();
        let mut builder = FontBuilder::default();
        (0..0x16u32).for_each(|i| {
            builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data);
        });
        let bytes = builder.build();
        let font = FontRef::new(&bytes).unwrap();
        let td = font.table_directory;
        assert_eq!(
            (256, 4, 96),
            (td.search_range(), td.entry_selector(), td.range_shift())
        );
    }

    #[test]
    fn survives_no_tables() {
        FontBuilder::default().build();
    }

    #[test]
    fn pad4() {
        for i in 0..10 {
            let pad = checksum_and_padding(&vec![0; i]).1;
            assert!(pad < 4);
            assert!((i + pad as usize) % 4 == 0, "pad {i} +{pad} bytes");
        }
    }
}