Annotate fonts with ruby (pinyin/romaji) and produce modified TTF/WOFF2 outputs.
1use anyhow::{Context, Result};
2use fontcull_read_fonts::{FontRef, TopLevelTable, tables::cff::Cff, types::Tag};
3use fontcull_write_fonts::tables::{glyf::Glyf, loca::Loca};
4use rustc_hash::FxHashMap;
5
6pub fn build_collection(fonts: &[FontRef]) -> Result<Vec<u8>> {
7 let mut out = Vec::new();
8
9 // TTC header
10 out.extend_from_slice(b"ttcf"); // Tag
11 out.extend_from_slice(&1u16.to_be_bytes()); // Major
12 out.extend_from_slice(&0u16.to_be_bytes()); // Minor
13 out.extend_from_slice(&(fonts.len() as u32).to_be_bytes());
14
15 let offset_table_start = out.len();
16
17 for _ in 0..fonts.len() {
18 out.extend_from_slice(&0u32.to_be_bytes());
19 }
20
21 let mut font_offsets = Vec::new();
22 let mut table_cache: FxHashMap<(Tag, Vec<u8>), u32> = FxHashMap::default();
23 let mut table_data_block = Vec::new();
24
25 // Process and rewrite each font
26 for font in fonts {
27 font_offsets.push(out.len() as u32);
28 let records = font.table_directory().table_records();
29 let num_tables = records.len() as u16;
30
31 // Write OffsetTable header
32 out.extend_from_slice(&0x00010000u32.to_be_bytes()); // sfntVersion
33 out.extend_from_slice(&num_tables.to_be_bytes());
34 let entry_selector = (num_tables as f32).log2().floor() as u16;
35 let search_range = (2u16.pow(entry_selector as u32)) * 16;
36 out.extend_from_slice(&search_range.to_be_bytes());
37 out.extend_from_slice(&entry_selector.to_be_bytes());
38 out.extend_from_slice(&(num_tables * 16 - search_range).to_be_bytes());
39
40 for record in records {
41 let tag = record.tag();
42 let table_data = font
43 .table_data(tag)
44 .context("Table missing")?
45 .as_ref()
46 .to_vec();
47
48 // Only share tables that are usually safe and heavy
49 let can_share = matches!(tag, Glyf::TAG | Cff::TAG | Loca::TAG);
50
51 let rel_offset = if can_share {
52 if let Some(&off) = table_cache.get(&(tag, table_data.clone())) {
53 off
54 } else {
55 while table_data_block.len() % 4 != 0 {
56 table_data_block.push(0);
57 }
58
59 let off = table_data_block.len() as u32;
60 table_cache.insert((tag, table_data.clone()), off);
61 table_data_block.extend(&table_data);
62
63 off
64 }
65 } else {
66 while table_data_block.len() % 4 != 0 {
67 table_data_block.push(0);
68 }
69
70 let off = table_data_block.len() as u32;
71 table_data_block.extend(&table_data);
72
73 off
74 };
75
76 out.extend_from_slice(&tag.to_be_bytes());
77 out.extend_from_slice(&record.checksum().to_be_bytes());
78 out.extend_from_slice(&rel_offset.to_be_bytes());
79 out.extend_from_slice(&(table_data.len() as u32).to_be_bytes());
80 }
81 }
82
83 // Fix up absolute offsets
84
85 let data_block_start = out.len() as u32;
86
87 for (i, &off) in font_offsets.iter().enumerate() {
88 let pos = offset_table_start + (i * 4);
89 out[pos..pos + 4].copy_from_slice(&off.to_be_bytes());
90 }
91
92 for &f_off in &font_offsets {
93 let num_tables =
94 u16::from_be_bytes(out[f_off as usize + 4..f_off as usize + 6].try_into()?);
95
96 for i in 0..num_tables {
97 let off_pos = (f_off as usize + 12) + (i as usize * 16) + 8;
98 let rel = u32::from_be_bytes(out[off_pos..off_pos + 4].try_into()?);
99 out[off_pos..off_pos + 4].copy_from_slice(&(data_block_start + rel).to_be_bytes());
100 }
101 }
102
103 out.extend(table_data_block);
104
105 Ok(out)
106}