···55// Imports for subsetting and woff2
66use fontcull_read_fonts as frf;
77use fontcull_read_fonts::collections::int_set::IntSet;
88-use kurbo::{Affine, BezPath, Shape};
99-use pinyin::ToPinyin;
88+use kurbo::BezPath;
109use read_fonts::{
1110 FileRef, FontRef, TableProvider,
1211 types::{GlyphId, Tag},
1312};
1313+1414+#[cfg(feature = "pinyin")]
1515+mod ruby;
1416use skrifa::{MetadataProvider, outline::OutlinePen};
1517use woofwoof;
1618use write_fonts::{
···3234const TAG_LOCA: Tag = Tag::new(b"loca");
3335const TAG_HEAD: Tag = Tag::new(b"head");
34363535-struct PathPen {
3636- path: BezPath,
3737+pub struct PathPen {
3838+ pub path: BezPath,
3739}
38403941impl PathPen {
4040- fn new() -> Self {
4242+ pub fn new() -> Self {
4143 Self {
4244 path: BezPath::new(),
4345 }
···105107 process_single_font(font_ref, pinyin_font_ref)
106108}
107109108108-fn process_single_font(font: FontRef, pinyin_font: Option<FontRef>) -> Result<Vec<u8>> {
110110+fn process_single_font(font: FontRef, _pinyin_font: Option<FontRef>) -> Result<Vec<u8>> {
109111 let font_file_data = font.table_directory.offset_data();
110112 let charmap = font.charmap();
111113 let hmtx = font.hmtx().context("Missing hmtx")?;
···114116 let num_glyphs = maxp.num_glyphs();
115117 let upem = font.head()?.units_per_em() as f64;
116118117117- // Providers for pinyin (default to main font)
118118- let p_font_ref = pinyin_font.as_ref().unwrap_or(&font);
119119- let p_charmap = p_font_ref.charmap();
120120- let p_outlines = p_font_ref.outline_glyphs();
121121- let p_hmtx = p_font_ref.hmtx().context("Missing pinyin font hmtx")?;
122122- let p_upem = p_font_ref.head()?.units_per_em() as f64;
123123-124124- // Normalizing scale factor:
125125- // We want pinyin to be 30% of main font's EM height.
126126- // Scale = (0.3 * MainUPEM) / PinyinUPEM
127127- let p_scale_factor = (0.3 * upem) / p_upem;
119119+ #[cfg(feature = "pinyin")]
120120+ let ruby_renderer: Option<Box<dyn crate::ruby::RubyRenderer>> = {
121121+ if let Some(pf) = _pinyin_font.as_ref() {
122122+ match crate::ruby::pinyin::PinyinRenderer::new(pf.clone(), 0.3, upem) {
123123+ Ok(r) => Some(Box::new(r)),
124124+ Err(err) => {
125125+ eprintln!("Warning: failed to initialize pinyin renderer: {:?}", err);
126126+ None
127127+ }
128128+ }
129129+ } else {
130130+ None
131131+ }
132132+ };
128133129134 let mut gid_to_char: HashMap<GlyphId, char> = HashMap::new();
130135···160165 }
161166162167 if let Some(&ch) = gid_to_char.get(&gid) {
163163- if let Some(p) = ch.to_pinyin() {
164164- let pinyin_text = p.with_tone();
165165- let mut pinyin_paths: Vec<(skrifa::GlyphId, BezPath)> = Vec::new();
166166- let mut all_found = true;
167167-168168- for pc in pinyin_text.chars() {
169169- match p_charmap.map(pc) {
170170- Some(pgid) if pgid.to_u32() != 0 => {
171171- if let Some(pglyph) = p_outlines.get(pgid) {
172172- let mut ppen = PathPen::new();
173173-174174- if pglyph
175175- .draw(skrifa::instance::Size::unscaled(), &mut ppen)
176176- .is_ok()
177177- {
178178- pinyin_paths.push((pgid, ppen.path));
179179- } else {
180180- all_found = false;
181181-182182- break;
183183- }
184184- } else {
185185- all_found = false;
186186-187187- break;
188188- }
189189- }
190190- _ => {
191191- all_found = false;
192192-193193- break;
194194- }
195195- }
196196- }
197197-198198- if all_found && !pinyin_paths.is_empty() {
199199- let orig_advance = hmtx
200200- .h_metrics()
201201- .get(gid.to_u32() as usize)
202202- .map(|m| m.advance.get())
203203- .unwrap_or(upem as u16) as f64;
204204-205205- let mut total_pinyin_width = 0.0;
206206-207207- for (pgid, _) in &pinyin_paths {
208208- let adv = p_hmtx
209209- .h_metrics()
210210- .get(pgid.to_u32() as usize)
211211- .map(|m| m.advance.get())
212212- .unwrap_or(p_upem as u16) as f64;
213213-214214- total_pinyin_width += adv * p_scale_factor;
215215- }
216216-217217- let bbox = final_path.bounding_box();
218218- let target_y = bbox.y1 + (upem * 0.1); // 10% EM padding
219219- let mut current_x = (orig_advance - total_pinyin_width) / 2.0;
220220-221221- for (pgid, mut p_path) in pinyin_paths {
222222- let adv = p_hmtx
223223- .h_metrics()
224224- .get(pgid.to_u32() as usize)
225225- .map(|m| m.advance.get())
226226- .unwrap_or(p_upem as u16) as f64;
227227- let xform = Affine::translate((current_x, target_y))
228228- * Affine::scale(p_scale_factor);
229229-230230- p_path.apply_affine(xform);
231231-232232- for el in p_path.elements() {
233233- match el {
234234- kurbo::PathEl::MoveTo(p) => final_path.move_to(*p),
235235- kurbo::PathEl::LineTo(p) => final_path.line_to(*p),
236236- kurbo::PathEl::QuadTo(p1, p2) => final_path.quad_to(*p1, *p2),
237237- kurbo::PathEl::CurveTo(p1, p2, p3) => {
238238- final_path.curve_to(*p1, *p2, *p3)
239239- }
240240- kurbo::PathEl::ClosePath => final_path.close_path(),
241241- }
242242- }
243243-244244- current_x += adv * p_scale_factor;
245245- }
246246- }
168168+ #[cfg(feature = "pinyin")]
169169+ if let Some(renderer) = &ruby_renderer {
170170+ let orig_advance = hmtx
171171+ .h_metrics()
172172+ .get(gid.to_u32() as usize)
173173+ .map(|m| m.advance.get())
174174+ .unwrap_or(upem as u16) as f64;
175175+ let _ = renderer.annotate(ch, &mut final_path, orig_advance, upem);
247176 }
248177 }
249178
+7-10
src/main.rs
···1212 /// Output font file path
1313 output: PathBuf,
14141515- /// Optional font file to use for pinyin characters
1515+ /// Optional font file to use for ruby characters
1616 #[arg(long)]
1717- pinyin_font: Option<PathBuf>,
1717+ ruby_font: Option<PathBuf>,
18181919 /// Subset the font to include only CJK and Pinyin characters
2020 #[arg(long)]
···2727 let font_data = fs::read(&cli.input)
2828 .with_context(|| format!("Failed to read input file: {:?}", cli.input))?;
29293030- let pinyin_font_data = if let Some(path) = &cli.pinyin_font {
3131- Some(
3232- fs::read(path)
3333- .with_context(|| format!("Failed to read pinyin font file: {:?}", path))?,
3434- )
3030+ let ruby_font_data = if let Some(path) = &cli.ruby_font {
3131+ Some(fs::read(path).with_context(|| format!("Failed to read ruby font file: {:?}", path))?)
3532 } else {
3633 None
3734 };
38353936 println!("Processing font...");
4040- let mut new_font_data = pinyinify::process_font_file(&font_data, pinyin_font_data.as_deref())?;
3737+ let mut new_font_data = rubify::process_font_file(&font_data, ruby_font_data.as_deref())?;
41384239 if cli.subset {
4340 println!("Subsetting font...");
4444- new_font_data = pinyinify::subset_cjk(&new_font_data)?;
4141+ new_font_data = rubify::subset_cjk(&new_font_data)?;
4542 }
46434744 // Infer format from output extension
···53505451 if let Some("woff2") = extension.as_deref() {
5552 println!("Converting to WOFF2...");
5656- new_font_data = pinyinify::convert_to_woff2(&new_font_data)?;
5353+ new_font_data = rubify::convert_to_woff2(&new_font_data)?;
5754 }
58555956 fs::write(&cli.output, new_font_data)
+19
src/ruby/mod.rs
···11+#[cfg(feature = "pinyin")]
22+pub mod pinyin;
33+44+use anyhow::Result;
55+use kurbo::BezPath;
66+77+/// A pluggable renderer that can add "ruby" annotations (small text above characters).
88+/// Implementations (such as pinyin) will be provided behind features.
99+pub trait RubyRenderer: Send + Sync {
1010+ /// Given a base character `ch`, add annotation paths (if any) into `final_path`.
1111+ /// `orig_advance` is the glyph advance in font units; `main_upem` is the main font UPEM.
1212+ fn annotate(
1313+ &self,
1414+ ch: char,
1515+ final_path: &mut BezPath,
1616+ orig_advance: f64,
1717+ main_upem: f64,
1818+ ) -> Result<()>;
1919+}
+122
src/ruby/pinyin.rs
···11+use ::pinyin::ToPinyin;
22+use anyhow::{Context, Result};
33+use kurbo::{Affine, BezPath, Shape};
44+use read_fonts::{FontRef, TableProvider};
55+use skrifa::MetadataProvider;
66+77+use crate::{PathPen, ruby::RubyRenderer};
88+99+pub struct PinyinRenderer<'a> {
1010+ font: FontRef<'a>,
1111+ upem: f64,
1212+ scale_ratio: f64,
1313+}
1414+1515+impl<'a> PinyinRenderer<'a> {
1616+ pub fn new(font: FontRef<'a>, scale_ratio: f64, _main_upem: f64) -> Result<Self> {
1717+ let upem = font.head()?.units_per_em() as f64;
1818+1919+ Ok(Self {
2020+ font,
2121+ upem,
2222+ scale_ratio,
2323+ })
2424+ }
2525+}
2626+2727+impl<'a> RubyRenderer for PinyinRenderer<'a> {
2828+ fn annotate(
2929+ &self,
3030+ ch: char,
3131+ final_path: &mut BezPath,
3232+ orig_advance: f64,
3333+ main_upem: f64,
3434+ ) -> Result<()> {
3535+ if let Some(p) = ch.to_pinyin() {
3636+ let pinyin_text = p.with_tone();
3737+ let mut pinyin_paths: Vec<(skrifa::GlyphId, BezPath)> = Vec::new();
3838+ let mut all_found = true;
3939+4040+ let cmap = self.font.charmap();
4141+ let outlines = self.font.outline_glyphs();
4242+ let hmtx = self.font.hmtx().context("Missing pinyin font hmtx")?;
4343+4444+ for pc in pinyin_text.chars() {
4545+ match cmap.map(pc) {
4646+ Some(pgid) if pgid.to_u32() != 0 => {
4747+ if let Some(pglyph) = outlines.get(pgid) {
4848+ let mut ppen = PathPen::new();
4949+5050+ if pglyph
5151+ .draw(skrifa::instance::Size::unscaled(), &mut ppen)
5252+ .is_ok()
5353+ {
5454+ pinyin_paths.push((pgid, ppen.path));
5555+ } else {
5656+ all_found = false;
5757+ break;
5858+ }
5959+ } else {
6060+ all_found = false;
6161+ break;
6262+ }
6363+ }
6464+ _ => {
6565+ all_found = false;
6666+ break;
6767+ }
6868+ }
6969+ }
7070+7171+ if all_found && !pinyin_paths.is_empty() {
7272+ // scale factor relative to the pinyin font's UPEM
7373+ let p_scale_factor = (self.scale_ratio * main_upem) / self.upem;
7474+7575+ let mut total_pinyin_width = 0.0;
7676+7777+ for (pgid, _) in &pinyin_paths {
7878+ let adv = hmtx
7979+ .h_metrics()
8080+ .get(pgid.to_u32() as usize)
8181+ .map(|m| m.advance.get())
8282+ .unwrap_or(self.upem as u16) as f64;
8383+8484+ total_pinyin_width += adv * p_scale_factor;
8585+ }
8686+8787+ let bbox = final_path.bounding_box();
8888+ let target_y = bbox.y1 + (main_upem * 0.1); // 10% EM padding
8989+ let mut current_x = (orig_advance - total_pinyin_width) / 2.0;
9090+9191+ for (pgid, mut p_path) in pinyin_paths {
9292+ let adv = hmtx
9393+ .h_metrics()
9494+ .get(pgid.to_u32() as usize)
9595+ .map(|m| m.advance.get())
9696+ .unwrap_or(self.upem as u16) as f64;
9797+9898+ let xform =
9999+ Affine::translate((current_x, target_y)) * Affine::scale(p_scale_factor);
100100+101101+ p_path.apply_affine(xform);
102102+103103+ for el in p_path.elements() {
104104+ match el {
105105+ kurbo::PathEl::MoveTo(p) => final_path.move_to(*p),
106106+ kurbo::PathEl::LineTo(p) => final_path.line_to(*p),
107107+ kurbo::PathEl::QuadTo(p1, p2) => final_path.quad_to(*p1, *p2),
108108+ kurbo::PathEl::CurveTo(p1, p2, p3) => {
109109+ final_path.curve_to(*p1, *p2, *p3)
110110+ }
111111+ kurbo::PathEl::ClosePath => final_path.close_path(),
112112+ }
113113+ }
114114+115115+ current_x += adv * p_scale_factor;
116116+ }
117117+ }
118118+ }
119119+120120+ Ok(())
121121+ }
122122+}