yeslogic / allsorts

Font parser, shaping engine, and subsetter implemented in Rust
https://yeslogic.com/blog/allsorts-rust-font-shaping-engine/
Apache License 2.0
721 stars 21 forks source link

Allow upfront decoding of the entire font to enable parallel text shaping of words / text blocks #41

Open fschutt opened 3 years ago

fschutt commented 3 years ago

One of the main features missing is the ability to shape / decode the font once on load - right now the entire crate is riddled with Rc<RefCell<...>>, which makes it impossible to shape words in parallel. I'd like to use this crate for my graphics framework, but shaping words synchronously is very slow. Ideally it should be possible to parse the entire font upfront (even if that takes hundreds of milliseconds) instead of decoding on-the-fly. The gpos::apply() and gsub::apply() functions currently take types like LayoutCache that don't implement Send, so it's not possible to shape words in parallel.

Right now the only thing I can do is to parse the glyf table upfront in parallel:

struct ParsedFont {
    glyph_records_decoded: BTreeMap<usize, OwnedGlyph>,
}

impl ParsedFont {

    fn new(font_bytes: &[u8], font_index: usize) -> Self {

        let scope = ReadScope::new(font_bytes);
        let font_file = scope.read::<FontData<'_>>().ok()?;
        let provider = font_file.table_provider(font_index).ok()?;
        let glyf_data = provider.table_data(tag::GLYF).ok()??.into_owned();
        let glyf_table = ReadScope::new(&glyf_data).read_dep::<GlyfTable<'_>>(&loca_table).ok()?;

        let glyph_records_decoded = glyf_table.records
        .into_par_iter() // <- decode in parallel
        .enumerate()
        .filter_map(|(glyph_index, mut glyph_record)| {
            glyph_record.parse().ok()?;
            match glyph_record {
                GlyfRecord::Empty | GlyfRecord::Present(_) => None,
                GlyfRecord::Parsed(g) => {
                    Some((glyph_index, OwnedGlyph::from_glyph_data(g)))
                }
            }
        }).collect::<Vec<_>>();

        ParsedFont {
            glyph_records_decoded: glyph_records_decoded.into_iter().collect(),
        }
    }

    // this function is now much faster (no need to decode the glyph_record on the fly)
    pub fn get_glyph_size(&self, glyph_index: u16) -> Option<(i32, i32)> {
        let g = self.glyph_records_decoded.get(&(glyph_index as usize))?;
        let glyph_width = g.bounding_box.x_max as i32 - g.bounding_box.x_min as i32; // width
        let glyph_height = g.bounding_box.y_max as i32 - g.bounding_box.y_min as i32; // height
        Some((glyph_width, glyph_height))
    }
}

The only problem is that I can't do this with font shaping, i.e. this doesn't work:

let shaped_words = words
    .par_iter()
    .map(|w| upfront_parsed_font.shape(word.chars[..], script, lang))
    .collect();

In GUI frameworks it's often necessary to shape characters many times, so it would be nice to decode the kerning / positioning / substitution tables upfront. Right now, font shaping is the heaviest part of the entire layout step (4 out of 5 ms for layout are spent on text shaping alone), so parallelizing text shaping on a per-word basis would be great.

LoganDark commented 2 years ago

I don't understand the reasoning behind this crate's dependency on Rc/RefCell. What is it used for?

wezm commented 2 years ago

I don't understand the reasoning behind this crate's dependency on Rc/RefCell. What is it used for?

It's used for caching during shaping: https://github.com/yeslogic/allsorts/blob/6f44fc4fefdb94f3441c10054449c98deea89e39/src/layout.rs#L2964