Annotate fonts with ruby (pinyin/romaji) and produce modified TTF/WOFF2 outputs.
at main 336 lines 11 kB view raw
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}