Skip to main content

FontShaper

Struct FontShaper 

Source
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.json pins — not a port. Where the two disagree, this is what the font draws;
  • there is no trace or shape_detailed: the font’s lookups are not the crate’s rules. The glyph-level view is FontShaper::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 an Mvs, as in the rule engine;
  • a ZWJ between two letters the font ligates comes out of HarfBuzz after the ligature. FontShaper::shape_raw keeps it there; shape drops it (see ShapedGlyph::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

Source

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?
examples/compare.rs (line 265)
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}
Source

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?
examples/compare.rs (line 270)
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}
Source

pub fn shape_str(&self, text: &str) -> Result<String, Error>

FontShaper::shape joined with + (S+A+I+I+A), the CLI’s output format.

Source

pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error>

Do a and b render the same glyph sequence?

Source

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.

Trait Implementations§

Source§

impl Default for FontShaper

Source§

fn default() -> FontShaper

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.