1use std::collections::BTreeMap;
4use std::{borrow::Cow, fmt::Display};
5
6use read_fonts::{FontRef, TableProvider};
7use types::{Tag, TT_SFNT_VERSION};
8
9use crate::util::SearchRange;
10
11include!("../generated/generated_font.rs");
12
13const TABLE_RECORD_LEN: usize = 16;
14
15#[derive(Debug, Clone, Default)]
17pub struct FontBuilder<'a> {
18 tables: BTreeMap<Tag, Cow<'a, [u8]>>,
19}
20
21#[derive(Clone, Debug)]
26#[non_exhaustive]
27pub struct BuilderError {
28 pub tag: Tag,
30 pub inner: crate::error::Error,
32}
33
34impl TableDirectory {
35 pub fn from_table_records(table_records: Vec<TableRecord>) -> TableDirectory {
36 assert!(table_records.len() <= u16::MAX as usize);
37 let computed = SearchRange::compute(table_records.len(), TABLE_RECORD_LEN);
39
40 TableDirectory::new(
41 TT_SFNT_VERSION,
42 computed.search_range,
43 computed.entry_selector,
44 computed.range_shift,
45 table_records,
46 )
47 }
48}
49
50const RECOMMENDED_TABLE_ORDER_TTF: [Tag; 19] = [
52 Tag::new(b"head"),
53 Tag::new(b"hhea"),
54 Tag::new(b"maxp"),
55 Tag::new(b"OS/2"),
56 Tag::new(b"hmtx"),
57 Tag::new(b"LTSH"),
58 Tag::new(b"VDMX"),
59 Tag::new(b"hdmx"),
60 Tag::new(b"cmap"),
61 Tag::new(b"fpgm"),
62 Tag::new(b"prep"),
63 Tag::new(b"cvt "),
64 Tag::new(b"loca"),
65 Tag::new(b"glyf"),
66 Tag::new(b"kern"),
67 Tag::new(b"name"),
68 Tag::new(b"post"),
69 Tag::new(b"gasp"),
70 Tag::new(b"PCLT"),
71];
72
73const RECOMMENDED_TABLE_ORDER_CFF: [Tag; 8] = [
74 Tag::new(b"head"),
75 Tag::new(b"hhea"),
76 Tag::new(b"maxp"),
77 Tag::new(b"OS/2"),
78 Tag::new(b"name"),
79 Tag::new(b"cmap"),
80 Tag::new(b"post"),
81 Tag::new(b"CFF "),
82];
83
84impl<'a> FontBuilder<'a> {
85 pub fn new() -> Self {
87 Self::default()
88 }
89
90 pub fn add_table<T>(&mut self, table: &T) -> Result<&mut Self, BuilderError>
96 where
97 T: FontWrite + Validate + TopLevelTable,
98 {
99 let tag = T::TAG;
100 let bytes = crate::dump_table(table).map_err(|inner| BuilderError { inner, tag })?;
101 Ok(self.add_raw(tag, bytes))
102 }
103
104 pub fn add_raw(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &mut Self {
106 self.tables.insert(tag, data.into());
107 self
108 }
109
110 pub fn copy_missing_tables(&mut self, font: FontRef<'a>) -> &mut Self {
112 for record in font.table_directory().table_records() {
113 let tag = record.tag();
114 if !self.tables.contains_key(&tag) {
115 if let Some(data) = font.data_for_tag(tag) {
116 self.add_raw(tag, data);
117 } else {
118 log::warn!("data for '{tag}' is malformed");
119 }
120 }
121 }
122 self
123 }
124
125 pub fn contains(&self, tag: Tag) -> bool {
127 self.tables.contains_key(&tag)
128 }
129
130 pub fn ordered_tags(&self) -> Vec<Tag> {
141 let recommended_order: &[Tag] = if self.contains(Tag::new(b"CFF ")) {
142 &RECOMMENDED_TABLE_ORDER_CFF
143 } else {
144 &RECOMMENDED_TABLE_ORDER_TTF
145 };
146 let mut ordered_tags: Vec<Tag> = self.tables.keys().copied().collect();
151 let dsig = Tag::new(b"DSIG");
152 ordered_tags.sort_unstable_by_key(|rtag| {
153 let tag = *rtag;
154 if tag == dsig {
155 (2, 0, tag)
156 } else if let Some(idx) = recommended_order.iter().position(|t| t == rtag) {
157 (0, idx, tag)
158 } else {
159 (1, 0, tag)
160 }
161 });
162
163 ordered_tags
164 }
165
166 pub fn build(&mut self) -> Vec<u8> {
171 const HEAD_CHECKSUM_START: usize = 8;
173 const HEAD_CHECKSUM_END: usize = 12;
174
175 let header_len = std::mem::size_of::<u32>() + std::mem::size_of::<u16>() * 4 + self.tables.len() * TABLE_RECORD_LEN;
178
179 let table_order = self.ordered_tags();
182
183 let mut position = header_len as u32;
184 let mut checksums = Vec::new();
185 let head_tag = Tag::new(b"head");
186
187 let mut table_records = Vec::new();
188 for tag in table_order.iter() {
189 let data = self.tables.get_mut(tag).unwrap();
191 let offset = position;
192 let length = data.len() as u32;
193 position += length;
194 if *tag == head_tag && data.len() >= HEAD_CHECKSUM_END {
195 let head = data.to_mut();
200 head[HEAD_CHECKSUM_START..HEAD_CHECKSUM_END].copy_from_slice(&[0, 0, 0, 0]);
201 }
202 let (checksum, padding) = checksum_and_padding(data);
203 checksums.push(checksum);
204 position += padding;
205 table_records.push(TableRecord::new(*tag, checksum, offset, length));
206 }
207 table_records.sort_unstable_by_key(|record| record.tag);
208
209 let directory = TableDirectory::from_table_records(table_records);
210
211 let mut writer = TableWriter::default();
212 directory.write_into(&mut writer);
213 let mut data = writer.into_data().bytes;
214 checksums.push(read_fonts::tables::compute_checksum(&data));
215
216 let checksum = checksums.into_iter().fold(0u32, u32::wrapping_add);
221 let checksum_adjustment = 0xB1B0_AFBAu32.wrapping_sub(checksum);
222
223 for tag in table_order {
224 let table = self.tables.remove(&tag).unwrap();
225 if tag == head_tag && table.len() >= HEAD_CHECKSUM_END {
226 data.extend_from_slice(&table[..HEAD_CHECKSUM_START]);
228 data.extend_from_slice(&checksum_adjustment.to_be_bytes());
229 data.extend_from_slice(&table[HEAD_CHECKSUM_END..]);
230 } else {
231 data.extend_from_slice(&table);
232 }
233 let rem = round4(table.len()) - table.len();
234 let padding = [0u8; 4];
235 data.extend_from_slice(&padding[..rem]);
236 }
237 data
238 }
239}
240
241fn round4(sz: usize) -> usize {
243 (sz + 3) & !3
244}
245
246fn checksum_and_padding(table: &[u8]) -> (u32, u32) {
247 let checksum = read_fonts::tables::compute_checksum(table);
248 let padding = round4(table.len()) - table.len();
249 (checksum, padding as u32)
250}
251
252impl TTCHeader {
253 fn compute_version(&self) -> MajorMinor {
254 panic!("TTCHeader writing not supported (yet)")
255 }
256}
257
258impl Display for BuilderError {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 write!(f, "failed to build '{}' table: '{}'", self.tag, self.inner)
261 }
262}
263
264impl std::error::Error for BuilderError {
265 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
266 Some(&self.inner)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::{RECOMMENDED_TABLE_ORDER_CFF, RECOMMENDED_TABLE_ORDER_TTF};
273 use font_types::Tag;
274 use read_fonts::FontRef;
275
276 use crate::{font_builder::checksum_and_padding, FontBuilder};
277 use rand::seq::SliceRandom;
278 use rand::Rng;
279 use rstest::rstest;
280
281 #[test]
282 fn sets_binary_search_assists() {
283 let data = b"doesn't matter".to_vec();
285 let mut builder = FontBuilder::default();
286 (0..0x16u32).for_each(|i| {
287 builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data);
288 });
289 let bytes = builder.build();
290 let font = FontRef::new(&bytes).unwrap();
291 let td = font.table_directory();
292 assert_eq!(
293 (256, 4, 96),
294 (td.search_range(), td.entry_selector(), td.range_shift())
295 );
296 }
297
298 #[test]
299 fn survives_no_tables() {
300 FontBuilder::default().build();
301 }
302
303 #[test]
304 fn pad4() {
305 for i in 0..10 {
306 let pad = checksum_and_padding(&vec![0; i]).1;
307 assert!(pad < 4);
308 assert!((i + pad as usize) % 4 == 0, "pad {i} +{pad} bytes");
309 }
310 }
311
312 #[test]
313 fn validate_font_checksum() {
314 let head_size = 54;
319 let mut rng = rand::thread_rng();
320 let mut builder = FontBuilder::default();
321 for tag in [Tag::new(b"head"), Tag::new(b"FOO "), Tag::new(b"BAR ")] {
322 let data: Vec<u8> = (0..=head_size).map(|_| rng.gen()).collect();
323 builder.add_raw(tag, data);
324 }
325 let font_data = builder.build();
326 assert_eq!(read_fonts::tables::compute_checksum(&font_data), 0xB1B0AFBA);
327 }
328
329 #[test]
330 fn minimum_head_size_for_checksum_rewrite() {
331 let mut builder = FontBuilder::default();
332 builder.add_raw(
333 Tag::new(b"head"),
334 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
335 );
336
337 let font_data = builder.build();
338 let font = FontRef::new(&font_data).unwrap();
339 let head = font.table_data(Tag::new(b"head")).unwrap();
340
341 assert_eq!(
342 head.as_bytes(),
343 &vec![0, 1, 2, 3, 4, 5, 6, 7, 65, 61, 62, 10]
344 );
345 }
346
347 #[test]
348 fn doesnt_overflow_head() {
349 let mut builder = FontBuilder::default();
350 builder.add_raw(Tag::new(b"head"), vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
351
352 let font_data = builder.build();
353 let font = FontRef::new(&font_data).unwrap();
354 let head = font.table_data(Tag::new(b"head")).unwrap();
355
356 assert_eq!(head.as_bytes(), &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
357 }
358
359 #[rstest]
360 #[case::ttf(&RECOMMENDED_TABLE_ORDER_TTF)]
361 #[case::cff(&RECOMMENDED_TABLE_ORDER_CFF)]
362 fn recommended_table_order(#[case] recommended_order: &[Tag]) {
363 let dsig = Tag::new(b"DSIG");
364 let mut builder = FontBuilder::default();
365 builder.add_raw(dsig, vec![0]);
366 let mut tags = recommended_order.to_vec();
367 tags.shuffle(&mut rand::thread_rng());
368 for tag in tags {
369 builder.add_raw(tag, vec![0]);
370 }
371 builder.add_raw(Tag::new(b"ZZZZ"), vec![0]);
372 builder.add_raw(Tag::new(b"AAAA"), vec![0]);
373
374 let mut expected = recommended_order.to_vec();
376 expected.push(Tag::new(b"AAAA"));
377 expected.push(Tag::new(b"ZZZZ"));
378 expected.push(dsig);
379
380 assert_eq!(builder.ordered_tags(), expected);
381 }
382}