pub struct FontShaper { /* private fields */ }Expand description
Shapes Hudum with the bundled mongfontbuilder font through HarfBuzz rules — the font backend
of the crate (Cargo feature font), an alternative to the rule engine
Shaper.
harfrust (the Rust port of HarfBuzz) runs the Hudum font that mongfontbuilder — the
reference implementation of UTN #57 — composes, with the OpenType rules the composer
generates. The crate reads the written units off the glyphs the font chose. The font is
bundled without its outlines (fonts/hudum.otf, 30 KB: GSUB, GDEF, cmap and the sfnt
skeleton), and the glyph → written-unit table is generated from its glyph names.
Compared with the rule engine:
- the rules are the font’s, at the mongfontbuilder commit
fonts/mongfontbuilder.jsonpins — not a port. Where the two disagree, this is what the font draws; - there is no
traceorshape_detailed: the font’s lookups are not the crate’s rules. The glyph-level view isFontShaper::shape_glyphs; - there is no
normalize: the normalizer is built on the rule engine; - it costs harfrust and its crates, Rust 1.85, and a few microseconds per word.
§Output contract
FontShaper::shape is the same kind of sequence as Shaper::shape:
Mvs and Nirugu verbatim, a Zwj only where it changes the ink, the duplicate encodings
unified — the two post-passes are shared with the rule engine — so the two backends can
be compared unit for unit. How the glyphs are read:
- an FVS, valid or not, is never a unit. The font draws an invalid one as a visible placeholder; the crate follows the rule engine and the GB/T 25914-2023 unit streams and reports nothing;
- an MVS that separates nothing is drawn as a space plus a silent glyph: one
Mvs. An NNBSP is anMvs, as in the rule engine; - a ZWJ between two letters the font ligates comes out of HarfBuzz after the ligature.
FontShaper::shape_rawkeeps it there;shapedrops it (seeShapedGlyph::skipped).
§Errors
Error::NonMongolianChar on anything but Mongolian letters, FVS, MVS, NNBSP, nirugu and
ZWJ — the rule engine’s rule; Error::UnsupportedChar on a character of the block the
font has no glyph for: a letter of another writing system (Todo, Sibe, Manchu, Ali Gali),
an Ali Gali sign, an unassigned code point. The rule engine shapes those to nothing; the
font would draw its .notdef box, so this backend refuses them instead.
§Example
use mongol_norm::{FontShaper, Locale, Shaper};
let font = FontShaper::new();
let rules = Shaper::new(Locale::Mng);
assert_eq!(font.shape_str("ᠰᠠᠢᠨ")?, "S+A+I+I+A");
assert_eq!(font.shape("ᠰᠠᠢᠨ")?, rules.shape("ᠰᠠᠢᠨ")?);Implementations§
Source§impl FontShaper
impl FontShaper
Sourcepub fn new() -> FontShaper
pub fn new() -> FontShaper
A shaper over the bundled font.
The font is parsed once per process and shared; a shaper holds its shape plan, so it is cheap to keep and shapes without further setup.
Examples found in repository?
264fn compare(options: &Options, words: &[String]) -> Report {
265 let font = FontShaper::new();
266 let engine = Shaper::new(Locale::Mng);
267 let mut differences = Vec::new();
268 let mut rejected = Vec::new();
269 for word in words {
270 match (font.shape(word), engine.shape(word)) {
271 (Ok(a), Ok(b)) if a == b => {}
272 (Ok(a), Ok(b)) => {
273 let (x, y) = middle(&a, &b);
274 differences.push(Difference {
275 word: word.clone(),
276 font: joined(&a),
277 engine: joined(&b),
278 group: (joined(x), joined(y)),
279 });
280 }
281 (Err(error), Ok(_)) => rejected.push(Rejected {
282 word: word.clone(),
283 side: "font",
284 error: error.to_string(),
285 }),
286 (Ok(_), Err(error)) => rejected.push(Rejected {
287 word: word.clone(),
288 side: "engine",
289 error: error.to_string(),
290 }),
291 (Err(_), Err(_)) => {}
292 }
293 }
294
295 let mut counts: HashMap<&(String, String), (usize, &str)> = HashMap::new();
296 for difference in &differences {
297 counts
298 .entry(&difference.group)
299 .or_insert((0, &difference.word))
300 .0 += 1;
301 }
302 let mut groups: Vec<Group> = counts
303 .into_iter()
304 .map(|((font, engine), (count, example))| Group {
305 font: font.clone(),
306 engine: engine.clone(),
307 count,
308 example: example.to_owned(),
309 })
310 .collect();
311 groups.sort_by(|a, b| {
312 b.count
313 .cmp(&a.count)
314 .then_with(|| (&a.font, &a.engine).cmp(&(&b.font, &b.engine)))
315 });
316
317 let input = match options.random {
318 Some(count) => format!(
319 "{count} random strings (seed {}, length 1-{})",
320 options.seed, options.max_len
321 ),
322 None => format!("{} distinct words", words.len()),
323 };
324 Report {
325 input,
326 total: words.len(),
327 differences,
328 rejected,
329 groups,
330 }
331}Sourcepub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error>
pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error>
Shape text into its written-unit sequence, on the same contract as
Shaper::shape: Mvs and Nirugu verbatim, a Zwj only where
it changes the ink, the duplicate encodings unified.
Examples found in repository?
264fn compare(options: &Options, words: &[String]) -> Report {
265 let font = FontShaper::new();
266 let engine = Shaper::new(Locale::Mng);
267 let mut differences = Vec::new();
268 let mut rejected = Vec::new();
269 for word in words {
270 match (font.shape(word), engine.shape(word)) {
271 (Ok(a), Ok(b)) if a == b => {}
272 (Ok(a), Ok(b)) => {
273 let (x, y) = middle(&a, &b);
274 differences.push(Difference {
275 word: word.clone(),
276 font: joined(&a),
277 engine: joined(&b),
278 group: (joined(x), joined(y)),
279 });
280 }
281 (Err(error), Ok(_)) => rejected.push(Rejected {
282 word: word.clone(),
283 side: "font",
284 error: error.to_string(),
285 }),
286 (Ok(_), Err(error)) => rejected.push(Rejected {
287 word: word.clone(),
288 side: "engine",
289 error: error.to_string(),
290 }),
291 (Err(_), Err(_)) => {}
292 }
293 }
294
295 let mut counts: HashMap<&(String, String), (usize, &str)> = HashMap::new();
296 for difference in &differences {
297 counts
298 .entry(&difference.group)
299 .or_insert((0, &difference.word))
300 .0 += 1;
301 }
302 let mut groups: Vec<Group> = counts
303 .into_iter()
304 .map(|((font, engine), (count, example))| Group {
305 font: font.clone(),
306 engine: engine.clone(),
307 count,
308 example: example.to_owned(),
309 })
310 .collect();
311 groups.sort_by(|a, b| {
312 b.count
313 .cmp(&a.count)
314 .then_with(|| (&a.font, &a.engine).cmp(&(&b.font, &b.engine)))
315 });
316
317 let input = match options.random {
318 Some(count) => format!(
319 "{count} random strings (seed {}, length 1-{})",
320 options.seed, options.max_len
321 ),
322 None => format!("{} distinct words", words.len()),
323 };
324 Report {
325 input,
326 total: words.len(),
327 differences,
328 rejected,
329 groups,
330 }
331}Sourcepub fn shape_str(&self, text: &str) -> Result<String, Error>
pub fn shape_str(&self, text: &str) -> Result<String, Error>
FontShaper::shape joined with + (S+A+I+I+A), the CLI’s output format.
Sourcepub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error>
pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error>
Do a and b render the same glyph sequence?
Sourcepub fn shape_glyphs(&self, text: &str) -> Result<Vec<ShapedGlyph>, Error>
pub fn shape_glyphs(&self, text: &str) -> Result<Vec<ShapedGlyph>, Error>
The glyphs the font chose for text, in the order HarfBuzz returns them, each with its
written units and positions — the debugging view of this backend.