Annotate fonts with ruby (pinyin/romaji) and produce modified TTF/WOFF2 outputs.
1pub mod pen;
2pub mod renderer;
3pub mod ttc;
4
5use anyhow::{Context, Result};
6use fontcull_font_types::NameId;
7use fontcull_klippa::{Plan, SubsetFlags, subset_font};
8use fontcull_read_fonts::{
9 FileRef, FontRef, TableProvider, TopLevelTable,
10 collections::IntSet,
11 types::{GlyphId, Tag},
12};
13use fontcull_skrifa::MetadataProvider;
14use fontcull_write_fonts::{
15 FontBuilder,
16 from_obj::ToOwnedObj,
17 tables::{
18 glyf::{Glyf, GlyfLocaBuilder, Glyph, SimpleGlyph},
19 head::Head,
20 loca::Loca,
21 },
22};
23use indicatif::ProgressStyle;
24use kurbo::BezPath;
25use rayon::iter::{ParallelBridge, ParallelIterator};
26use rustc_hash::FxHashMap;
27use tracing::{info, info_span};
28use tracing_indicatif::span_ext::IndicatifSpanExt;
29
30use crate::{pen::PathPen, renderer::RubyRenderer};
31
32pub struct ProcessedFont {
33 pub data: Vec<u8>,
34 pub file_name: Option<String>,
35}
36
37pub fn process_font_file(
38 file: FileRef,
39 renderer: &Box<dyn RubyRenderer>,
40 subset: bool,
41 split: bool,
42) -> Result<Vec<ProcessedFont>> {
43 match file {
44 FileRef::Font(font) => {
45 let data = process_font_ref(&font, &renderer)?;
46 let data = if subset {
47 info!("Subsetting font");
48
49 subset_by_renderers(&data, &renderer)?
50 } else {
51 data
52 };
53
54 Ok(vec![ProcessedFont {
55 data,
56 file_name: None,
57 }])
58 }
59 FileRef::Collection(collection) => {
60 if split {
61 // Split mode: write each font as a separate TTF file
62 let collection_span = info_span!("split_fonts_in_collection");
63 collection_span.pb_set_style(
64 &ProgressStyle::with_template("{msg} [{wide_bar:.green/cyan}] {pos}/{len}")
65 .unwrap(),
66 );
67 collection_span.pb_set_length(collection.len() as u64);
68 collection_span.pb_set_message("Splitting collection");
69
70 let split_span_enter = collection_span.enter();
71
72 let fonts = collection
73 .iter()
74 .enumerate()
75 .map(|(idx, font)| {
76 collection_span.pb_inc(1);
77
78 let font = font.context("Failed to read font")?;
79 let mut data = process_font_ref(&font, &renderer)?;
80
81 if subset {
82 collection_span.pb_set_message("Subsetting font");
83 data = subset_by_renderers(&data, &renderer)?;
84 }
85
86 // Generate output filename
87 let file_name = if let Ok(name_table) = font.name() {
88 // Try to get family name from name table
89 name_table
90 .name_record()
91 .iter()
92 .find(|n| n.name_id() == NameId::POSTSCRIPT_NAME)
93 .and_then(|rec| rec.string(name_table.string_data()).ok())
94 .map(|name| format!("{name}.ttf"))
95 .unwrap_or_else(|| format!("font-{idx}.ttf"))
96 } else {
97 format!("font-{idx}.ttf")
98 };
99
100 Ok(ProcessedFont {
101 data,
102 file_name: Some(file_name),
103 })
104 })
105 .collect::<Result<Vec<ProcessedFont>>>();
106
107 drop(split_span_enter);
108 drop(collection_span);
109
110 fonts
111 } else {
112 let collection_span = info_span!("process_fonts_in_collection");
113 collection_span.pb_set_style(
114 &ProgressStyle::with_template("{msg} [{wide_bar:.green/cyan}] {pos}/{len}")
115 .unwrap(),
116 );
117 collection_span.pb_set_length(collection.len() as u64);
118 collection_span.pb_set_message("Processing collection");
119
120 let process_span_enter = collection_span.enter();
121
122 let fonts = collection
123 .iter()
124 .par_bridge()
125 .map(|font| {
126 collection_span.pb_inc(1);
127 collection_span.pb_set_message("Processing font");
128
129 let font = font.context("Failed to read font")?;
130
131 let mut data = process_font_ref(&font, &renderer)?;
132
133 if subset {
134 collection_span.pb_set_message("Subsetting font");
135 data = subset_by_renderers(&data, &renderer)?;
136 }
137
138 let data = Box::leak(data.into_boxed_slice());
139
140 FontRef::new(data).context("Failed to create font ref")
141 })
142 .collect::<Result<Vec<FontRef>>>()?;
143
144 drop(process_span_enter);
145
146 info_span!("Building TTC");
147
148 let data = ttc::build_collection(&fonts).context("Failed to build TTC")?;
149
150 Ok(vec![ProcessedFont {
151 data,
152 file_name: None,
153 }])
154 }
155 }
156 }
157}
158
159pub fn process_font_ref(font: &FontRef, renderer: &Box<dyn RubyRenderer>) -> Result<Vec<u8>> {
160 let font_file_data = font.table_directory.offset_data();
161 let charmap = font.charmap();
162 let hmtx = font.hmtx()?;
163 let maxp = font.maxp()?;
164 let outlines = font.outline_glyphs();
165 let upem = font.head()?.units_per_em() as f64;
166
167 let gid_char_map = renderer
168 .ranges()
169 .iter()
170 .cloned()
171 .flat_map(|range| {
172 range.filter_map(|c_u32| {
173 std::char::from_u32(c_u32).and_then(|c| {
174 charmap
175 .map(c)
176 .and_then(|gid| (gid != GlyphId::NOTDEF).then_some((gid, c)))
177 })
178 })
179 })
180 .collect::<FxHashMap<GlyphId, char>>();
181
182 // let glyphs = if subset {
183 // gid_char_map.keys().copied().collect::<Vec<GlyphId>>()
184 // } else {
185 // (0..(maxp.num_glyphs() as u32))
186 // .map(GlyphId::new)
187 // .collect::<Vec<GlyphId>>()
188 // };
189
190 let glyphs = (0..(maxp.num_glyphs() as u32))
191 .map(GlyphId::new)
192 .collect::<Vec<GlyphId>>();
193
194 let progress_style = ProgressStyle::with_template(
195 "{spinner:.green} {msg} {wide_bar:.cyan/blue} {pos:>7}/{len:7}",
196 )?
197 .progress_chars("##-");
198
199 let glyphs_span = info_span!("process_glyphs");
200 glyphs_span.pb_set_style(&progress_style);
201 glyphs_span.pb_set_length(glyphs.len() as u64);
202
203 if let Some(ttc_index) = font.ttc_index() {
204 glyphs_span.pb_set_message(&format!("Processing glyphs ({})", ttc_index));
205 } else {
206 glyphs_span.pb_set_message("Processing glyphs");
207 }
208
209 let glyphs_span_enter = glyphs_span.enter();
210
211 let mut glyf_loca_builder = GlyfLocaBuilder::new();
212
213 for gid in glyphs {
214 glyphs_span.pb_inc(1);
215
216 let mut final_path = BezPath::new();
217 let mut has_content = false;
218
219 if let Some(glyph) = outlines.get(fontcull_skrifa::GlyphId::new(gid.to_u32())) {
220 let mut pen = PathPen::new();
221
222 match glyph.draw(fontcull_skrifa::instance::Size::unscaled(), &mut pen) {
223 Ok(_) => {
224 final_path = pen.path;
225 has_content = true;
226 }
227 Err(_) => {}
228 }
229 }
230
231 if let Some(&ch) = gid_char_map.get(&gid) {
232 let orig_advance = hmtx
233 .h_metrics()
234 .get(gid.to_u32() as usize)
235 .map(|m| m.advance.get())
236 .unwrap_or(upem as u16) as f64;
237
238 renderer
239 .annotate(ch, &mut final_path, orig_advance, upem)
240 .context("Failed to annotate")?;
241 }
242
243 let write_glyph = if !has_content && final_path.elements().is_empty() {
244 Glyph::Empty
245 } else {
246 match SimpleGlyph::from_bezpath(&final_path) {
247 Ok(s) => Glyph::Simple(s),
248 Err(_) => Glyph::Empty,
249 }
250 };
251
252 glyf_loca_builder.add_glyph(&write_glyph)?;
253 }
254
255 drop(glyphs_span_enter);
256 drop(glyphs_span);
257
258 let (glyf_data, loca_data, loca_fmt) = glyf_loca_builder.build();
259
260 let mut font_builder = FontBuilder::new();
261
262 for record in font.table_directory.table_records() {
263 let tag = record.tag();
264
265 // Skip glyf/loca - we'll insert rebuilt data later
266 if tag == Glyf::TAG || tag == Loca::TAG {
267 continue;
268 }
269
270 if tag == Head::TAG {
271 if let Ok(head) = font.head() {
272 let mut head: Head = head.to_owned_obj(font_file_data);
273
274 head.index_to_loc_format = loca_fmt as i16;
275 head.checksum_adjustment = 0;
276
277 font_builder
278 .add_table(&head)
279 .context("Failed to add head table")?;
280 }
281
282 continue;
283 }
284
285 if let Some(data) = font.data_for_tag(tag) {
286 font_builder.add_raw(tag, data.as_bytes().to_vec());
287 }
288 }
289
290 font_builder
291 .add_table(&glyf_data)
292 .context("Failed to add glyf table")?
293 .add_table(&loca_data)
294 .context("Failed to add loca table")?;
295
296 Ok(font_builder.build())
297}
298
299pub fn subset_by_renderers(font_data: &[u8], renderer: &Box<dyn RubyRenderer>) -> Result<Vec<u8>> {
300 let font = FontRef::new(font_data).context("Failed to parse font for subsetting")?;
301
302 // Build unicodes set based on provided character sets
303 let mut unicodes = IntSet::<u32>::empty();
304
305 for range in renderer.ranges() {
306 for c in range.clone() {
307 unicodes.insert(c);
308 }
309 }
310
311 let glyph_ids = IntSet::<GlyphId>::empty();
312 let drop_tables = IntSet::<Tag>::empty();
313 let no_subset_tables = IntSet::<Tag>::empty();
314 let passthrough_tables = IntSet::<Tag>::empty();
315 let name_ids = IntSet::<NameId>::empty();
316 let name_languages = IntSet::<u16>::empty();
317
318 let plan = Plan::new(
319 &glyph_ids,
320 &unicodes,
321 &font,
322 SubsetFlags::default(),
323 &drop_tables,
324 &no_subset_tables,
325 &passthrough_tables,
326 &name_ids,
327 &name_languages,
328 );
329
330 subset_font(&font, &plan).context("Subset error")
331}
332
333#[cfg(feature = "woff2")]
334pub fn convert_to_woff2(font_data: &[u8]) -> Result<Vec<u8>> {
335 woofwoof::compress(font_data, &[], 11, true).context("WOFF2 compression failed")
336}