font_subset/write/
mod.rs

1//! Logic for serializing `FontSubset`s in OpenType format.
2
3use core::iter;
4
5use crate::{
6    alloc::{vec, Vec},
7    font::Cursor,
8    Font, TableTag,
9};
10
11#[cfg(feature = "woff2")]
12mod brotli;
13#[cfg(test)]
14mod tests;
15#[cfg(feature = "woff2")]
16mod woff2;
17
18/// Writes a single font table to a byte buffer.
19pub(crate) trait WriteTable {
20    fn tag(&self) -> TableTag;
21
22    fn write_to_vec(&self, buffer: &mut Vec<u8>);
23}
24
25impl WriteTable for (TableTag, Cursor<'_>) {
26    fn tag(&self) -> TableTag {
27        self.0
28    }
29
30    fn write_to_vec(&self, buffer: &mut Vec<u8>) {
31        buffer.extend_from_slice(self.1.bytes());
32    }
33}
34
35/// Extension trait for `Vec<u8>` allowing to write various data to it.
36pub(crate) trait VecExt {
37    fn write_u16(&mut self, value: u16);
38
39    fn write_i16(&mut self, value: i16);
40
41    fn write_u32(&mut self, value: u32);
42
43    fn write_i32(&mut self, value: i32);
44
45    fn write_u64(&mut self, value: u64);
46
47    fn write_i64(&mut self, value: i64);
48}
49
50impl VecExt for Vec<u8> {
51    fn write_u16(&mut self, value: u16) {
52        self.extend_from_slice(&value.to_be_bytes());
53    }
54
55    fn write_i16(&mut self, value: i16) {
56        self.extend_from_slice(&value.to_be_bytes());
57    }
58
59    fn write_u32(&mut self, value: u32) {
60        self.extend_from_slice(&value.to_be_bytes());
61    }
62
63    fn write_i32(&mut self, value: i32) {
64        self.extend_from_slice(&value.to_be_bytes());
65    }
66
67    fn write_u64(&mut self, value: u64) {
68        self.extend_from_slice(&value.to_be_bytes());
69    }
70
71    fn write_i64(&mut self, value: i64) {
72        self.extend_from_slice(&value.to_be_bytes());
73    }
74}
75
76impl Font<'_> {
77    /// Serializes this subset to the OpenType format.
78    pub fn to_opentype(&self) -> Vec<u8> {
79        self.to_writer().into_opentype()
80    }
81
82    /// Serializes this subset to the WOFF2 format.
83    #[cfg(feature = "woff2")]
84    #[cfg_attr(docsrs, doc(cfg(feature = "woff2")))]
85    pub fn to_woff2(&self) -> Vec<u8> {
86        self.to_writer().into_woff2()
87    }
88
89    fn to_writer(&self) -> FontWriter {
90        let mut writer = FontWriter::default();
91        writer.write(&self.cmap);
92        if let Some(variable) = &self.variable {
93            writer.write(&variable.stat);
94            writer.write(&variable.fvar);
95            writer.write(&variable.gvar);
96        }
97        writer.write(&self.hmtx);
98        writer.write(&self.hhea);
99        writer.write(&self.maxp);
100        writer.write(&self.name);
101        writer.write(&self.os2);
102        writer.write(&self.post);
103        // Write `glyf` immediately after `loca` as per the WOFF2 spec.
104        writer.write(&self.glyf);
105        writer.write(&self.loca);
106        writer.write(&self.head);
107
108        for tag_and_cursor in &self.unparsed {
109            writer.write(tag_and_cursor);
110        }
111        writer
112    }
113}
114
115#[derive(Debug, Clone, Copy)]
116#[cfg_attr(test, derive(PartialEq))]
117struct TableRecord {
118    tag: TableTag,
119    checksum: u32,
120    /// Offset is initially recorded relative to the table data start. It's always 4-byte aligned.
121    offset: u32,
122    length: u32,
123}
124
125impl TableRecord {
126    fn write_opentype(&self, buffer: &mut Vec<u8>) {
127        buffer.extend_from_slice(&self.tag.0);
128        buffer.write_u32(self.checksum);
129        buffer.write_u32(self.offset);
130        buffer.write_u32(self.length);
131    }
132
133    fn self_checksum(&self) -> u32 {
134        u32::from_be_bytes(self.tag.0)
135            .wrapping_add(self.checksum)
136            .wrapping_add(self.offset)
137            .wrapping_add(self.length)
138    }
139}
140
141#[derive(Debug, Clone, Default)]
142struct FontWriter {
143    tables: Vec<TableRecord>,
144    /// Contains *aligned* table data
145    table_data: Vec<u8>,
146}
147
148impl FontWriter {
149    fn write_custom(&mut self, tag: TableTag, with: impl FnOnce(&mut Vec<u8>)) {
150        let offset = self.table_data.len();
151        debug_assert_eq!(offset % 4, 0, "unaligned offset: {offset}");
152
153        with(&mut self.table_data);
154        let length = self.table_data.len() - offset;
155        // Pad the table heap to a 4-byte boundary.
156        if length % 4 > 0 {
157            let zero_padding = 4 - length % 4;
158            self.table_data.extend(iter::repeat_n(0_u8, zero_padding));
159        }
160
161        let checksum = Font::checksum(&self.table_data[offset..]);
162        let record = TableRecord {
163            tag,
164            checksum,
165            offset: u32::try_from(offset).expect("table offset overflow"),
166            length: u32::try_from(length).expect("table length overflow"),
167        };
168        #[cfg(feature = "tracing")]
169        tracing::debug!(?record, "written table record");
170        self.tables.push(record);
171    }
172
173    fn write(&mut self, table: &impl WriteTable) {
174        self.write_custom(table.tag(), |buffer| table.write_to_vec(buffer));
175    }
176
177    fn write_sfnt_header(&self) -> Vec<u8> {
178        let mut buffer = vec![];
179        buffer.write_u32(Font::SFNT_VERSION);
180
181        // `unwrap()`s are safe: we don't have many tables written.
182        let table_count = u16::try_from(self.tables.len()).unwrap();
183        buffer.write_u16(table_count);
184        let entry_selector = u16::try_from(table_count.ilog2()).unwrap();
185        let search_range = 1 << (4 + entry_selector);
186        buffer.write_u16(search_range);
187        buffer.write_u16(entry_selector);
188        let range_shift = 16 * table_count - search_range;
189        buffer.write_u16(range_shift);
190
191        debug_assert_eq!(buffer.len(), Font::SFNT_HEADER_LEN);
192        buffer
193    }
194
195    /// Returns the starting offset of table data.
196    fn data_offset(&self) -> usize {
197        Font::SFNT_HEADER_LEN + self.tables.len() * Font::TABLE_RECORD_LEN
198    }
199
200    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip_all))]
201    fn into_opentype(mut self) -> Vec<u8> {
202        let mut buffer = self.write_sfnt_header();
203        self.adjust_data(Font::checksum(&buffer));
204
205        self.tables.sort_unstable_by_key(|record| record.tag.0);
206        for record in &self.tables {
207            record.write_opentype(&mut buffer);
208        }
209        buffer.extend(self.table_data);
210        buffer
211    }
212
213    fn adjust_data(&mut self, sfnt_header_checksum: u32) {
214        let data_offset = self.data_offset();
215        let data_offset_u32 = u32::try_from(data_offset).expect("data_offset overflow");
216
217        let mut file_checksum = sfnt_header_checksum;
218        for record in &mut self.tables {
219            record.offset += data_offset_u32;
220            file_checksum = file_checksum
221                .wrapping_add(record.self_checksum())
222                .wrapping_add(record.checksum);
223        }
224        self.patch_head_table(file_checksum, data_offset);
225    }
226
227    fn checksum_adjustment_offset(&self) -> usize {
228        let head_table = self
229            .tables
230            .iter()
231            .find(|record| record.tag == TableTag::HEAD)
232            .expect("head table is always present");
233        head_table.offset as usize + Font::HEAD_CHECKSUM_OFFSET
234    }
235
236    fn patch_head_table(&mut self, file_checksum: u32, data_offset: usize) {
237        let checksum_adjustment = Font::SFNT_CHECKSUM.wrapping_sub(file_checksum);
238
239        // At this point, the table offset already includes the heap offset, so we need to subtract it.
240        let offset = self.checksum_adjustment_offset() - data_offset;
241        #[cfg(feature = "tracing")]
242        tracing::debug!(
243            file_checksum,
244            checksum_adjustment,
245            offset,
246            "patching `head` table"
247        );
248        self.table_data[offset..offset + 4].copy_from_slice(&checksum_adjustment.to_be_bytes());
249    }
250}