1use std::borrow::Cow;
4use std::collections::BTreeMap;
5
6use read_fonts::{FontRef, TableProvider};
7use types::{Tag, TT_SFNT_VERSION};
8
9#[cfg(feature = "tables")]
10use crate::error::BuilderError;
11use crate::search_range::SearchRange;
12
13include!("../generated/generated_font.rs");
14
15const TABLE_RECORD_LEN: usize = 16;
16const CFF: Tag = Tag::new(b"CFF ");
17const CFF2: Tag = Tag::new(b"CFF2");
18
19#[derive(Debug, Clone, Default)]
21pub struct FontBuilder<'a> {
22 tables: BTreeMap<Tag, Cow<'a, [u8]>>,
23}
24
25impl TableDirectory {
26 pub fn from_table_records(table_records: Vec<TableRecord>) -> TableDirectory {
27 assert!(table_records.len() <= u16::MAX as usize);
28 let computed = SearchRange::compute(table_records.len(), TABLE_RECORD_LEN);
30
31 let is_cff = table_records
32 .iter()
33 .any(|rec| [CFF, CFF2].contains(&rec.tag));
34 let sfnt = if is_cff {
35 CFF_SFNT_VERSION
36 } else {
37 TT_SFNT_VERSION
38 };
39
40 TableDirectory::new(
41 sfnt,
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 #[cfg(feature = "tables")]
96 pub fn add_table<T>(&mut self, table: &T) -> Result<&mut Self, BuilderError>
97 where
98 T: FontWrite + Validate + TopLevelTable,
99 {
100 let tag = T::TAG;
101 let bytes = crate::dump_table(table).map_err(|inner| BuilderError { inner, tag })?;
102 Ok(self.add_raw(tag, bytes))
103 }
104
105 pub fn add_raw(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &mut Self {
107 self.tables.insert(tag, data.into());
108 self
109 }
110
111 pub fn copy_missing_tables(&mut self, font: FontRef<'a>) -> &mut Self {
113 for record in font.table_directory().table_records() {
114 let tag = record.tag();
115 if !self.tables.contains_key(&tag) {
116 if let Some(data) = font.data_for_tag(tag) {
117 self.add_raw(tag, data);
118 } else {
119 log::warn!("data for '{tag}' is malformed");
120 }
121 }
122 }
123 self
124 }
125
126 pub fn contains(&self, tag: Tag) -> bool {
128 self.tables.contains_key(&tag)
129 }
130
131 pub fn ordered_tags(&self) -> Vec<Tag> {
142 let recommended_order: &[Tag] = if self.contains(Tag::new(b"CFF ")) {
143 &RECOMMENDED_TABLE_ORDER_CFF
144 } else {
145 &RECOMMENDED_TABLE_ORDER_TTF
146 };
147 let mut ordered_tags: Vec<Tag> = self.tables.keys().copied().collect();
152 let dsig = Tag::new(b"DSIG");
153 ordered_tags.sort_unstable_by_key(|rtag| {
154 let tag = *rtag;
155 if tag == dsig {
156 (2, 0, tag)
157 } else if let Some(idx) = recommended_order.iter().position(|t| t == rtag) {
158 (0, idx, tag)
159 } else {
160 (1, 0, tag)
161 }
162 });
163
164 ordered_tags
165 }
166
167 pub fn build(&mut self) -> Vec<u8> {
172 const HEAD_CHECKSUM_START: usize = 8;
174 const HEAD_CHECKSUM_END: usize = 12;
175
176 let header_len = std::mem::size_of::<u32>() + std::mem::size_of::<u16>() * 4 + self.tables.len() * TABLE_RECORD_LEN;
179
180 let table_order = self.ordered_tags();
183
184 let mut position = header_len as u32;
185 let mut checksums = Vec::new();
186 let head_tag = Tag::new(b"head");
187
188 let mut table_records = Vec::new();
189 for tag in table_order.iter() {
190 let data = self.tables.get_mut(tag).unwrap();
192 let offset = position;
193 let length = data.len() as u32;
194 position += length;
195 if *tag == head_tag && data.len() >= HEAD_CHECKSUM_END {
196 let head = data.to_mut();
201 head[HEAD_CHECKSUM_START..HEAD_CHECKSUM_END].copy_from_slice(&[0, 0, 0, 0]);
202 }
203 let (checksum, padding) = checksum_and_padding(data);
204 checksums.push(checksum);
205 position += padding;
206 table_records.push(TableRecord::new(*tag, checksum, offset, length));
207 }
208 table_records.sort_unstable_by_key(|record| record.tag);
209
210 let directory = TableDirectory::from_table_records(table_records);
211
212 let mut writer = TableWriter::default();
213 directory.write_into(&mut writer);
214 let mut data = writer.into_data().bytes;
215 checksums.push(read_fonts::tables::compute_checksum(&data));
216
217 let checksum = checksums.into_iter().fold(0u32, u32::wrapping_add);
222 let checksum_adjustment = 0xB1B0_AFBAu32.wrapping_sub(checksum);
223
224 for tag in table_order {
225 let table = self.tables.remove(&tag).unwrap();
226 if tag == head_tag && table.len() >= HEAD_CHECKSUM_END {
227 data.extend_from_slice(&table[..HEAD_CHECKSUM_START]);
229 data.extend_from_slice(&checksum_adjustment.to_be_bytes());
230 data.extend_from_slice(&table[HEAD_CHECKSUM_END..]);
231 } else {
232 data.extend_from_slice(&table);
233 }
234 let rem = round4(table.len()) - table.len();
235 let padding = [0u8; 4];
236 data.extend_from_slice(&padding[..rem]);
237 }
238 data
239 }
240}
241
242fn round4(sz: usize) -> usize {
244 (sz + 3) & !3
245}
246
247fn checksum_and_padding(table: &[u8]) -> (u32, u32) {
248 let checksum = read_fonts::tables::compute_checksum(table);
249 let padding = round4(table.len()) - table.len();
250 (checksum, padding as u32)
251}
252
253impl TTCHeader {
254 fn compute_version(&self) -> MajorMinor {
255 panic!("TTCHeader writing not supported (yet)")
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::{RECOMMENDED_TABLE_ORDER_CFF, RECOMMENDED_TABLE_ORDER_TTF};
262 use font_types::Tag;
263 use read_fonts::FontRef;
264
265 use crate::{font_builder::checksum_and_padding, FontBuilder};
266 use rand::seq::SliceRandom;
267 use rand::Rng;
268 use rstest::rstest;
269
270 #[test]
271 fn sets_binary_search_assists() {
272 let data = b"doesn't matter".to_vec();
274 let mut builder = FontBuilder::default();
275 (0..0x16u32).for_each(|i| {
276 builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data);
277 });
278 let bytes = builder.build();
279 let font = FontRef::new(&bytes).unwrap();
280 let td = font.table_directory();
281 assert_eq!(
282 (256, 4, 96),
283 (td.search_range(), td.entry_selector(), td.range_shift())
284 );
285 }
286
287 #[test]
288 fn survives_no_tables() {
289 FontBuilder::default().build();
290 }
291
292 #[test]
293 fn pad4() {
294 for i in 0..10 {
295 let pad = checksum_and_padding(&vec![0; i]).1;
296 assert!(pad < 4);
297 assert!((i + pad as usize) % 4 == 0, "pad {i} +{pad} bytes");
298 }
299 }
300
301 #[test]
302 fn validate_font_checksum() {
303 let head_size = 54;
308 let mut rng = rand::thread_rng();
309 let mut builder = FontBuilder::default();
310 for tag in [Tag::new(b"head"), Tag::new(b"FOO "), Tag::new(b"BAR ")] {
311 let data: Vec<u8> = (0..=head_size).map(|_| rng.r#gen()).collect();
312 builder.add_raw(tag, data);
313 }
314 let font_data = builder.build();
315 assert_eq!(read_fonts::tables::compute_checksum(&font_data), 0xB1B0AFBA);
316 }
317
318 #[test]
319 fn minimum_head_size_for_checksum_rewrite() {
320 let mut builder = FontBuilder::default();
321 builder.add_raw(
322 Tag::new(b"head"),
323 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
324 );
325
326 let font_data = builder.build();
327 let font = FontRef::new(&font_data).unwrap();
328 let head = font.table_data(Tag::new(b"head")).unwrap();
329
330 assert_eq!(
331 head.as_bytes(),
332 &vec![0, 1, 2, 3, 4, 5, 6, 7, 65, 61, 62, 10]
333 );
334 }
335
336 #[test]
337 fn doesnt_overflow_head() {
338 let mut builder = FontBuilder::default();
339 builder.add_raw(Tag::new(b"head"), vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
340
341 let font_data = builder.build();
342 let font = FontRef::new(&font_data).unwrap();
343 let head = font.table_data(Tag::new(b"head")).unwrap();
344
345 assert_eq!(head.as_bytes(), &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
346 }
347
348 #[rstest]
349 #[case::ttf(&RECOMMENDED_TABLE_ORDER_TTF)]
350 #[case::cff(&RECOMMENDED_TABLE_ORDER_CFF)]
351 fn recommended_table_order(#[case] recommended_order: &[Tag]) {
352 let dsig = Tag::new(b"DSIG");
353 let mut builder = FontBuilder::default();
354 builder.add_raw(dsig, vec![0]);
355 let mut tags = recommended_order.to_vec();
356 tags.shuffle(&mut rand::thread_rng());
357 for tag in tags {
358 builder.add_raw(tag, vec![0]);
359 }
360 builder.add_raw(Tag::new(b"ZZZZ"), vec![0]);
361 builder.add_raw(Tag::new(b"AAAA"), vec![0]);
362
363 let mut expected = recommended_order.to_vec();
365 expected.push(Tag::new(b"AAAA"));
366 expected.push(Tag::new(b"ZZZZ"));
367 expected.push(dsig);
368
369 assert_eq!(builder.ordered_tags(), expected);
370 }
371}