Annotate fonts with ruby (pinyin/romaji) and produce modified TTF/WOFF2 outputs.
at main 228 lines 7.4 kB view raw
1use std::sync::atomic::Ordering; 2 3use atomic_float::AtomicF64; 4use fontcull_read_fonts::FontRef; 5use fontcull_skrifa::{GlyphId, MetadataProvider, instance::Size}; 6use kurbo::{BezPath, Shape}; 7 8use crate::renderer::RubyPosition; 9 10pub type GlyphPaths = Vec<(GlyphId, BezPath)>; 11 12/// Collect glyph paths; returns None if any glyph cannot be found or drawn. 13pub fn collect_glyph_paths(font: &FontRef, text: String) -> Option<GlyphPaths> { 14 let cmap = font.charmap(); 15 let outlines = font.outline_glyphs(); 16 17 let mut glyph_paths: Vec<(GlyphId, BezPath)> = Vec::new(); 18 19 for pc in text.chars() { 20 match cmap.map(pc) { 21 Some(pgid) if pgid.to_u32() != 0 => { 22 if let Some(pglyph) = outlines.get(pgid) { 23 let mut ppen = crate::PathPen::new(); 24 let res = pglyph.draw(Size::unscaled(), &mut ppen); 25 26 if res.is_ok() { 27 glyph_paths.push((pgid, ppen.path)); 28 } else { 29 return None; 30 } 31 } else { 32 return None; 33 } 34 } 35 _ => return None, 36 } 37 } 38 39 Some(glyph_paths) 40} 41 42/// Compute widths for each text given a closure to get advance (in font units). 43pub fn compute_glyph_widths( 44 glyph_paths: &GlyphPaths, 45 p_scale_factor: f64, 46 mut get_adv: impl FnMut(GlyphId) -> f64, 47) -> Vec<f64> { 48 let mut text_widths: Vec<f64> = Vec::new(); 49 50 for (pgid, _) in glyph_paths { 51 let mut text_width = 0.0; 52 let adv = get_adv(*pgid); 53 text_width += adv * p_scale_factor; 54 text_widths.push(text_width); 55 } 56 57 text_widths 58} 59 60/// Render top/bottom annotated text into `final_path`. 61#[allow(clippy::too_many_arguments)] 62pub fn render_top_bottom( 63 final_path: &mut BezPath, 64 glyph_paths: GlyphPaths, 65 text_widths: &[f64], 66 p_scale_factor: f64, 67 main_upem: f64, 68 orig_advance: f64, 69 position: RubyPosition, 70 gutter_em: f64, 71 baseline_offset_em: f64, 72 tight: bool, 73 cached_top: &AtomicF64, 74 cached_bottom: &AtomicF64, 75 mut get_adv: impl FnMut(GlyphId) -> f64, 76) { 77 let total_width = text_widths.iter().sum::<f64>(); 78 79 let bbox = final_path.bounding_box(); 80 let gutter_units = gutter_em * main_upem; 81 let approx_height = main_upem * (p_scale_factor * (1.0 / (p_scale_factor.max(0.00001)))) * 0.8; // conservative 82 83 let baseline_offset_units = baseline_offset_em * main_upem; 84 85 // Measure min/max y of the pinyin glyphs in unscaled font units 86 87 let mut min_y: f64 = f64::INFINITY; 88 let mut max_y: f64 = f64::NEG_INFINITY; 89 90 for (_pgid, p_path) in &glyph_paths { 91 for el in p_path.elements() { 92 match el { 93 kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) => { 94 min_y = min_y.min(p.y); 95 max_y = max_y.max(p.y); 96 } 97 kurbo::PathEl::QuadTo(p1, p2) => { 98 min_y = min_y.min(p1.y).min(p2.y); 99 max_y = max_y.max(p1.y).max(p2.y); 100 } 101 kurbo::PathEl::CurveTo(p1, p2, p3) => { 102 min_y = min_y.min(p1.y).min(p2.y).min(p3.y); 103 max_y = max_y.max(p1.y).max(p2.y).max(p3.y); 104 } 105 kurbo::PathEl::ClosePath => {} 106 } 107 } 108 } 109 110 if !min_y.is_finite() { 111 min_y = 0.0; 112 } 113 if !max_y.is_finite() { 114 max_y = approx_height / p_scale_factor; 115 } 116 117 let min_y_scaled = min_y * p_scale_factor; 118 let max_y_scaled = max_y * p_scale_factor; 119 120 let required_top_target = bbox.y1 + gutter_units + baseline_offset_units - min_y_scaled; 121 let required_bottom_target = bbox.y0 - gutter_units - baseline_offset_units - max_y_scaled; 122 123 let target_y = if tight { 124 if position == RubyPosition::Top { 125 bbox.y1 + gutter_units 126 } else { 127 bbox.y0 - gutter_units - approx_height 128 } 129 } else { 130 if position == RubyPosition::Top { 131 cached_top 132 .fetch_max(required_top_target, Ordering::Relaxed) 133 .max(required_top_target) 134 } else { 135 cached_bottom 136 .fetch_min(required_bottom_target, Ordering::Relaxed) 137 .min(required_bottom_target) 138 } 139 }; 140 141 let mut current_x = (orig_advance - total_width) / 2.0; 142 143 for (pgid, mut p_path) in glyph_paths.into_iter() { 144 let xform = 145 kurbo::Affine::translate((current_x, target_y)) * kurbo::Affine::scale(p_scale_factor); 146 147 p_path.apply_affine(xform); 148 149 for el in p_path.elements() { 150 match el { 151 kurbo::PathEl::MoveTo(p) => final_path.move_to(*p), 152 kurbo::PathEl::LineTo(p) => final_path.line_to(*p), 153 kurbo::PathEl::QuadTo(p1, p2) => final_path.quad_to(*p1, *p2), 154 kurbo::PathEl::CurveTo(p1, p2, p3) => final_path.curve_to(*p1, *p2, *p3), 155 kurbo::PathEl::ClosePath => final_path.close_path(), 156 } 157 } 158 159 let adv = get_adv(pgid); 160 current_x += adv * p_scale_factor; 161 } 162} 163 164/// Render side-positioned annotations (left/right, up/down stacking) 165#[allow(clippy::too_many_arguments)] 166pub fn render_side( 167 final_path: &mut BezPath, 168 glyph_paths: &GlyphPaths, 169 p_scale_factor: f64, 170 main_upem: f64, 171 orig_advance: f64, 172 position: RubyPosition, 173 gutter_em: f64, 174 bbox_center_y: f64, 175 get_adv: &mut impl FnMut(GlyphId) -> f64, 176) { 177 let mut glyph_list: Vec<(f64, BezPath)> = Vec::new(); 178 179 for (pgid, p_path) in glyph_paths { 180 let adv = get_adv(*pgid); 181 glyph_list.push((adv * p_scale_factor, p_path.clone())); 182 } 183 184 if glyph_list.is_empty() { 185 return; 186 } 187 188 let max_glyph_width = glyph_list.iter().map(|(w, _)| *w).fold(0.0f64, f64::max); 189 let vertical_step = main_upem * p_scale_factor * 0.8; 190 let gutter_units = gutter_em * main_upem; 191 192 let start_x = match position { 193 RubyPosition::LeftDown | RubyPosition::LeftUp => -(max_glyph_width + gutter_units), 194 _ => orig_advance + gutter_units, 195 }; 196 197 let n = glyph_list.len() as f64; 198 let mut current_y = match position { 199 RubyPosition::LeftDown | RubyPosition::RightDown => { 200 bbox_center_y + ((n - 1.0) / 2.0) * vertical_step 201 } 202 _ => bbox_center_y - ((n - 1.0) / 2.0) * vertical_step, 203 }; 204 205 for (w, mut p_path) in glyph_list { 206 let tx = start_x + (max_glyph_width - w) / 2.0; 207 208 let xform = 209 kurbo::Affine::translate((tx, current_y)) * kurbo::Affine::scale(p_scale_factor); 210 211 p_path.apply_affine(xform); 212 213 for el in p_path.elements() { 214 match el { 215 kurbo::PathEl::MoveTo(p) => final_path.move_to(*p), 216 kurbo::PathEl::LineTo(p) => final_path.line_to(*p), 217 kurbo::PathEl::QuadTo(p1, p2) => final_path.quad_to(*p1, *p2), 218 kurbo::PathEl::CurveTo(p1, p2, p3) => final_path.curve_to(*p1, *p2, *p3), 219 kurbo::PathEl::ClosePath => final_path.close_path(), 220 } 221 } 222 223 match position { 224 RubyPosition::LeftDown | RubyPosition::RightDown => current_y -= vertical_step, 225 _ => current_y += vertical_step, 226 } 227 } 228}