Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
fschutt opened this issue Jan 15, 2021 · 2 comments

Comments

@fschutt
Copy link
Contributor

fschutt commented Jan 15, 2021

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
Copy link
Contributor

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

@wezm
Copy link
Contributor

wezm commented Jan 20, 2022

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:

pub fn new_layout_cache<T: LayoutTableType>(layout_table: LayoutTable<T>) -> LayoutCache<T> {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants