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