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 checksums: BTreeMap<Tag, u32>,
24}
25
26impl TableDirectory {
27 pub fn from_table_records(table_records: Vec<TableRecord>) -> TableDirectory {
28 assert!(table_records.len() <= u16::MAX as usize);
29 let computed = SearchRange::compute(table_records.len(), TABLE_RECORD_LEN);
31
32 let is_cff = table_records
33 .iter()
34 .any(|rec| [CFF, CFF2].contains(&rec.tag));
35 let sfnt = if is_cff {
36 CFF_SFNT_VERSION
37 } else {
38 TT_SFNT_VERSION
39 };
40
41 TableDirectory::new(
42 sfnt,
43 computed.search_range,
44 computed.entry_selector,
45 computed.range_shift,
46 table_records,
47 )
48 }
49}
50
51const RECOMMENDED_TABLE_ORDER_TTF: [Tag; 19] = [
53 Tag::new(b"head"),
54 Tag::new(b"hhea"),
55 Tag::new(b"maxp"),
56 Tag::new(b"OS/2"),
57 Tag::new(b"hmtx"),
58 Tag::new(b"LTSH"),
59 Tag::new(b"VDMX"),
60 Tag::new(b"hdmx"),
61 Tag::new(b"cmap"),
62 Tag::new(b"fpgm"),
63 Tag::new(b"prep"),
64 Tag::new(b"cvt "),
65 Tag::new(b"loca"),
66 Tag::new(b"glyf"),
67 Tag::new(b"kern"),
68 Tag::new(b"name"),
69 Tag::new(b"post"),
70 Tag::new(b"gasp"),
71 Tag::new(b"PCLT"),
72];
73
74const RECOMMENDED_TABLE_ORDER_CFF: [Tag; 8] = [
75 Tag::new(b"head"),
76 Tag::new(b"hhea"),
77 Tag::new(b"maxp"),
78 Tag::new(b"OS/2"),
79 Tag::new(b"name"),
80 Tag::new(b"cmap"),
81 Tag::new(b"post"),
82 Tag::new(b"CFF "),
83];
84
85impl<'a> FontBuilder<'a> {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 #[cfg(feature = "tables")]
97 pub fn add_table<T>(&mut self, table: &T) -> Result<&mut Self, BuilderError>
98 where
99 T: FontWrite + Validate + TopLevelTable,
100 {
101 let tag = T::TAG;
102 let bytes = crate::dump_table(table).map_err(|inner| BuilderError { inner, tag })?;
103 Ok(self.add_raw(tag, bytes))
104 }
105
106 pub fn add_raw(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &mut Self {
108 self.tables.insert(tag, data.into());
109 self.checksums.remove(&tag);
110 self
111 }
112
113 pub fn add_raw_with_checksum(
118 &mut self,
119 tag: Tag,
120 data: impl Into<Cow<'a, [u8]>>,
121 checksum: u32,
122 ) -> &mut Self {
123 self.tables.insert(tag, data.into());
124 self.checksums.insert(tag, checksum);
125 self
126 }
127
128 pub fn copy_missing_tables(&mut self, font: FontRef<'a>) -> &mut Self {
130 for record in font.table_directory().table_records() {
131 let tag = record.tag();
132 if !self.tables.contains_key(&tag) {
133 if let Some(data) = font.data_for_tag(tag) {
134 self.add_raw(tag, data);
135 } else {
136 log::warn!("data for '{tag}' is malformed");
137 }
138 }
139 }
140 self
141 }
142
143 pub fn contains(&self, tag: Tag) -> bool {
145 self.tables.contains_key(&tag)
146 }
147
148 pub fn ordered_tags(&self) -> Vec<Tag> {
159 let recommended_order: &[Tag] = if self.contains(Tag::new(b"CFF ")) {
160 &RECOMMENDED_TABLE_ORDER_CFF
161 } else {
162 &RECOMMENDED_TABLE_ORDER_TTF
163 };
164 let mut ordered_tags: Vec<Tag> = self.tables.keys().copied().collect();
169 let dsig = Tag::new(b"DSIG");
170 ordered_tags.sort_unstable_by_key(|rtag| {
171 let tag = *rtag;
172 if tag == dsig {
173 (2, 0, tag)
174 } else if let Some(idx) = recommended_order.iter().position(|t| t == rtag) {
175 (0, idx, tag)
176 } else {
177 (1, 0, tag)
178 }
179 });
180
181 ordered_tags
182 }
183
184 pub fn build(&mut self) -> Vec<u8> {
189 const HEAD_CHECKSUM_START: usize = 8;
191 const HEAD_CHECKSUM_END: usize = 12;
192
193 let header_len = std::mem::size_of::<u32>() + std::mem::size_of::<u16>() * 4 + self.tables.len() * TABLE_RECORD_LEN;
196
197 let table_order = self.ordered_tags();
200
201 let mut position = header_len as u32;
202 let mut checksums = Vec::with_capacity(self.tables.len() + 1);
203 let head_tag = Tag::new(b"head");
204
205 let mut table_records = Vec::with_capacity(self.tables.len());
206 for tag in table_order.iter() {
207 let data = self.tables.get_mut(tag).unwrap();
209 let offset = position;
210 let length = data.len() as u32;
211 position += length;
212 if *tag == head_tag && data.len() >= HEAD_CHECKSUM_END {
213 let head = data.to_mut();
218 head[HEAD_CHECKSUM_START..HEAD_CHECKSUM_END].copy_from_slice(&[0, 0, 0, 0]);
219 }
220 let padding = round4(data.len()) - data.len();
222 let checksum = if *tag == head_tag {
223 read_fonts::tables::compute_checksum(data)
224 } else {
225 self.checksums
226 .get(tag)
227 .copied()
228 .unwrap_or_else(|| read_fonts::tables::compute_checksum(data))
229 };
230 checksums.push(checksum);
231 position += padding as u32;
232 table_records.push(TableRecord::new(*tag, checksum, offset, length));
233 }
234 table_records.sort_unstable_by_key(|record| record.tag);
235
236 let directory = TableDirectory::from_table_records(table_records);
237
238 let mut writer = TableWriter::default();
239 directory.write_into(&mut writer);
240 let mut data = writer.into_data().bytes;
241 data.reserve_exact((position as usize).saturating_sub(data.len()));
242 let expected_data_len = data.capacity();
243 checksums.push(read_fonts::tables::compute_checksum(&data));
244
245 let checksum = checksums.into_iter().fold(0u32, u32::wrapping_add);
250 let checksum_adjustment = 0xB1B0_AFBAu32.wrapping_sub(checksum);
251
252 for tag in table_order {
253 let table = self.tables.remove(&tag).unwrap();
254 self.checksums.remove(&tag);
255 if tag == head_tag && table.len() >= HEAD_CHECKSUM_END {
256 data.extend_from_slice(&table[..HEAD_CHECKSUM_START]);
258 data.extend_from_slice(&checksum_adjustment.to_be_bytes());
259 data.extend_from_slice(&table[HEAD_CHECKSUM_END..]);
260 } else {
261 data.extend_from_slice(&table);
262 }
263 let rem = round4(table.len()) - table.len();
264 let padding = [0u8; 4];
265 data.extend_from_slice(&padding[..rem]);
266 }
267 if expected_data_len != data.len() {
268 log::warn!(
269 "suboptimal: allocated {expected_data_len} bytes but needed {actual_len}",
270 actual_len = data.len()
271 );
272 }
273 data
274 }
275}
276
277fn round4(sz: usize) -> usize {
279 (sz + 3) & !3
280}
281
282#[cfg(test)]
283fn checksum_and_padding(table: &[u8]) -> (u32, u32) {
284 let checksum = read_fonts::tables::compute_checksum(table);
285 let padding = round4(table.len()) - table.len();
286 (checksum, padding as u32)
287}
288
289impl TTCHeader {
290 #[allow(dead_code)] fn compute_version(&self) -> MajorMinor {
292 panic!("TTCHeader writing not supported (yet)")
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::{RECOMMENDED_TABLE_ORDER_CFF, RECOMMENDED_TABLE_ORDER_TTF};
299 use font_types::Tag;
300 use read_fonts::FontRef;
301
302 use crate::{font_builder::checksum_and_padding, FontBuilder};
303 use rand::seq::SliceRandom;
304 use rstest::rstest;
305
306 #[test]
307 fn sets_binary_search_assists() {
308 let data = b"doesn't matter".to_vec();
310 let mut builder = FontBuilder::default();
311 (0..0x16u32).for_each(|i| {
312 builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data);
313 });
314 let bytes = builder.build();
315 let font = FontRef::new(&bytes).unwrap();
316 let td = font.table_directory();
317 assert_eq!(
318 (256, 4, 96),
319 (td.search_range(), td.entry_selector(), td.range_shift())
320 );
321 }
322
323 #[test]
324 fn survives_no_tables() {
325 FontBuilder::default().build();
326 }
327
328 #[test]
329 fn pad4() {
330 for i in 0..10 {
331 let pad = checksum_and_padding(&vec![0; i]).1;
332 assert!(pad < 4);
333 assert!((i + pad as usize) % 4 == 0, "pad {i} +{pad} bytes");
334 }
335 }
336
337 #[test]
338 fn uses_and_invalidates_precomputed_checksums() {
339 let tag = Tag::new(b"TEST");
340 let mut builder = FontBuilder::default();
341 builder.add_raw_with_checksum(tag, [1, 2, 3].as_slice(), 0xDEAD_BEEF);
342 let bytes = builder.build();
343 let font = FontRef::new(&bytes).unwrap();
344 let record = font
345 .table_directory()
346 .table_records()
347 .iter()
348 .find(|record| record.tag() == tag)
349 .unwrap();
350 assert_eq!(record.checksum(), 0xDEAD_BEEF);
351
352 builder.add_raw_with_checksum(tag, [0; 4].as_slice(), 0xDEAD_BEEF);
353 builder.add_raw(tag, [1, 2, 3].as_slice());
354 let bytes = builder.build();
355 let font = FontRef::new(&bytes).unwrap();
356 let record = font
357 .table_directory()
358 .table_records()
359 .iter()
360 .find(|record| record.tag() == tag)
361 .unwrap();
362 assert_eq!(record.checksum(), 0x0102_0300);
363 }
364
365 #[test]
366 fn validate_font_checksum() {
367 use rand::RngExt;
368
369 let head_size = 54;
374 let mut rng = rand::rng();
375 let mut builder = FontBuilder::default();
376 for tag in [Tag::new(b"head"), Tag::new(b"FOO "), Tag::new(b"BAR ")] {
377 let data: Vec<u8> = (0..=head_size).map(|_| rng.r#random()).collect();
378 builder.add_raw(tag, data);
379 }
380 let font_data = builder.build();
381 assert_eq!(read_fonts::tables::compute_checksum(&font_data), 0xB1B0AFBA);
382 }
383
384 #[test]
385 fn minimum_head_size_for_checksum_rewrite() {
386 let mut builder = FontBuilder::default();
387 builder.add_raw(
388 Tag::new(b"head"),
389 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
390 );
391
392 let font_data = builder.build();
393 let font = FontRef::new(&font_data).unwrap();
394 let head = font.table_data(Tag::new(b"head")).unwrap();
395
396 assert_eq!(
397 head.as_bytes(),
398 &vec![0, 1, 2, 3, 4, 5, 6, 7, 65, 61, 62, 10]
399 );
400 }
401
402 #[test]
403 fn doesnt_overflow_head() {
404 let mut builder = FontBuilder::default();
405 builder.add_raw(Tag::new(b"head"), vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
406
407 let font_data = builder.build();
408 let font = FontRef::new(&font_data).unwrap();
409 let head = font.table_data(Tag::new(b"head")).unwrap();
410
411 assert_eq!(head.as_bytes(), &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
412 }
413
414 #[rstest]
415 #[case::ttf(&RECOMMENDED_TABLE_ORDER_TTF)]
416 #[case::cff(&RECOMMENDED_TABLE_ORDER_CFF)]
417 fn recommended_table_order(#[case] recommended_order: &[Tag]) {
418 let dsig = Tag::new(b"DSIG");
419 let mut builder = FontBuilder::default();
420 builder.add_raw(dsig, vec![0]);
421 let mut tags = recommended_order.to_vec();
422 tags.shuffle(&mut rand::rng());
423 for tag in tags {
424 builder.add_raw(tag, vec![0]);
425 }
426 builder.add_raw(Tag::new(b"ZZZZ"), vec![0]);
427 builder.add_raw(Tag::new(b"AAAA"), vec![0]);
428
429 let mut expected = recommended_order.to_vec();
431 expected.push(Tag::new(b"AAAA"));
432 expected.push(Tag::new(b"ZZZZ"));
433 expected.push(dsig);
434
435 assert_eq!(builder.ordered_tags(), expected);
436 }
437}