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 #[allow(dead_code)] fn compute_version(&self) -> MajorMinor {
256 panic!("TTCHeader writing not supported (yet)")
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::{RECOMMENDED_TABLE_ORDER_CFF, RECOMMENDED_TABLE_ORDER_TTF};
263 use font_types::Tag;
264 use read_fonts::FontRef;
265
266 use crate::{font_builder::checksum_and_padding, FontBuilder};
267 use rand::seq::SliceRandom;
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 use rand::RngExt;
304
305 let head_size = 54;
310 let mut rng = rand::rng();
311 let mut builder = FontBuilder::default();
312 for tag in [Tag::new(b"head"), Tag::new(b"FOO "), Tag::new(b"BAR ")] {
313 let data: Vec<u8> = (0..=head_size).map(|_| rng.r#random()).collect();
314 builder.add_raw(tag, data);
315 }
316 let font_data = builder.build();
317 assert_eq!(read_fonts::tables::compute_checksum(&font_data), 0xB1B0AFBA);
318 }
319
320 #[test]
321 fn minimum_head_size_for_checksum_rewrite() {
322 let mut builder = FontBuilder::default();
323 builder.add_raw(
324 Tag::new(b"head"),
325 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
326 );
327
328 let font_data = builder.build();
329 let font = FontRef::new(&font_data).unwrap();
330 let head = font.table_data(Tag::new(b"head")).unwrap();
331
332 assert_eq!(
333 head.as_bytes(),
334 &vec![0, 1, 2, 3, 4, 5, 6, 7, 65, 61, 62, 10]
335 );
336 }
337
338 #[test]
339 fn doesnt_overflow_head() {
340 let mut builder = FontBuilder::default();
341 builder.add_raw(Tag::new(b"head"), vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
342
343 let font_data = builder.build();
344 let font = FontRef::new(&font_data).unwrap();
345 let head = font.table_data(Tag::new(b"head")).unwrap();
346
347 assert_eq!(head.as_bytes(), &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
348 }
349
350 #[rstest]
351 #[case::ttf(&RECOMMENDED_TABLE_ORDER_TTF)]
352 #[case::cff(&RECOMMENDED_TABLE_ORDER_CFF)]
353 fn recommended_table_order(#[case] recommended_order: &[Tag]) {
354 let dsig = Tag::new(b"DSIG");
355 let mut builder = FontBuilder::default();
356 builder.add_raw(dsig, vec![0]);
357 let mut tags = recommended_order.to_vec();
358 tags.shuffle(&mut rand::rng());
359 for tag in tags {
360 builder.add_raw(tag, vec![0]);
361 }
362 builder.add_raw(Tag::new(b"ZZZZ"), vec![0]);
363 builder.add_raw(Tag::new(b"AAAA"), vec![0]);
364
365 let mut expected = recommended_order.to_vec();
367 expected.push(Tag::new(b"AAAA"));
368 expected.push(Tag::new(b"ZZZZ"));
369 expected.push(dsig);
370
371 assert_eq!(builder.ordered_tags(), expected);
372 }
373}