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

Make FontResolver a trait and use to provide access to a fontdb #784

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

dhardy
Copy link
Contributor

@dhardy dhardy commented Jun 27, 2024

Summary

usvg::FontResolver is changed from a struct to a trait, with a default implementation DefaultFontResolver.

usvg::Options and usvg::Tree no longer contain fontdb; instead then FontResolver provides access as required.

Motivation

The existing model:

  • Requires that the fontdb be shared under an Arc, which shouldn't be necessary
  • Clones the fontdb before modifying
  • Makes the modified fontdb accessible through usvg::Tree, which doesn't appear to be the right place for it

Instead, the user is expected to provide a &dyn FontResolver instance in order to process any SVG involving text. As a consequence:

  • The user may use an existing fontdb instance (without cloning) and may even insert additional fonts directly into this DB (via RefCell, Mutex or similar)
  • The user may retrieve the original fontdb instance used without Arc
  • The minimal set-up with working text processing is slightly more complex (and unfortunately runs into a borrow-checker error if font_resolver is not declared before usvg::Options).

@RazrFalcon
Copy link
Collaborator

@LaurenzV

@RazrFalcon
Copy link
Collaborator

Do I understand correctly that your approach doesn't allow sharing fontdb between threads? That's one of the requirements.

@LaurenzV
Copy link
Contributor

I'll delegate to @laurmaedje. :p

@dhardy
Copy link
Contributor Author

dhardy commented Jun 27, 2024

Do I understand correctly that your approach doesn't allow sharing fontdb between threads? That's one of the requirements.

I think it should but haven't tested. Specifically, there is a Sync bound on FontResolver (to make&dyn FontResolver: Send) and the user may implement any type of lock they wish.

DefaultFontResolver is not really suitable for this, but it doesn't need to be.

@RazrFalcon
Copy link
Collaborator

DefaultFontResolver is not really suitable for this, but it doesn't need to be.

Both the current and the old implementations allowed sharing fontdb between threads.
But in this patch you're simply cloning fontdb in crates/resvg/tests/integration/main.rs This is far from ideal.

@dhardy
Copy link
Contributor Author

dhardy commented Jun 27, 2024

Fixed: there's no longer a need for DefaultFontResolver since we can just make Database implement FontResolver (avoiding the need for a variant over a &Database).

I think the previous code would have cloned the DB anyway when calling Options::fontdb_mut in order to call the resolvers (which needed &mut Database passed even when they only used &Database)?

Certainly now it shouldn't be cloning the Database.

@LaurenzV
Copy link
Contributor

Just so you know I briefly talked to @laurmaedje and he said he would comment why he implemented it the way he did, although I'm not sure when he will get to it. But might be good to wait a bit before merging.

@RazrFalcon
Copy link
Collaborator

Ok, will wait then. I do not want to break typst again.

@laurmaedje
Copy link
Contributor

laurmaedje commented Jun 28, 2024

For context, the current implementation comes from #769. This implementation was preceded by the earlier PR #754.

The design in this PR is closer to #754 than #769. The discussion of that PR provides some insights into problems, but I'll try to summarize things here.

The main conceptual problem, from my understanding, is how support for fonts in embedded in SVGs would work. In the current model, since usvg manages the database, it can mutate it itself. The Arc optimizes for the common case, but conceptually each SVG has its own database and that's also why it ends up in the tree.

On another note, if I'm not mistaken the implementation in this PR doesn't really allow for lazy font loading. Since both select_font and select_fallback are &self, we'd have to resort to interior mutability on the Database, but that's incompatible with the method fontdb(&self) -> &Database. There is some discussion about this problem in #754, but all workarounds I found are ugly.

If I may ask: Which concrete use case is the motivation for changing things again? The Arc is very cheap and the deep-clone of the Database only happens if you both provide fonts up-front and on-demand. And even then, cloning a Database isn't that costly since the font data itself is reference counted internally or memory-mapped on-demand. Moreover, while having the fontdb in the usvg::Tree seems a bit weird perhaps, it makes the user-facing API much simpler and harder to use in the wrong way.

The one somewhat costly thing I can see is that if you parse a bunch of SVGs and lazy load, the fontdb is populated over and over again. But you can cache things in the background and keep that relatively cheap (in the grand scheme of what usvg does I would be surprised if this was a large factor). Moreover, it helps a bit with correctness because in the current design it's harder to mess things up in a way where parsing of one SVG affects font selection of another SVG (because of an already populated database).

@RazrFalcon
Copy link
Collaborator

Moreover, while having the fontdb in the usvg::Tree seems a bit weird perhaps, it makes the user-facing API much simpler and harder to use in the wrong way.

Another reason for this is that in the future we will support fonts embedded in CSS, which must not be added to the "global" fontdb. That's why we need an ability to deeply clone the database on-demand.

@dhardy
Copy link
Contributor Author

dhardy commented Jun 29, 2024

On another note, if I'm not mistaken the implementation in this PR doesn't really allow for lazy font loading. Since both select_font and select_fallback are &self, we'd have to resort to interior mutability on the Database,

The intention is that a wrapper type be used to manage that lock... which also requires an associated type (something I forgot to add previously)...

pub trait FontResolver: Debug + Sync {
    type DbGuard<'a>: std::ops::Deref<Target = Database>;
    fn fontdb(&self) -> Self::DbGuard<'_>;
    // ...
}

#[derive(Debug)]
struct LockingFontResolver {
    db: std::sync::Mutex<Database>,
}
impl FontResolver for LockingFontResolver {
    type DbGuard<'a> = std::sync::MutexGuard<'a, Database>;
    fn fontdb(&self) -> Self::DbGuard<'_> {
        self.db.lock().unwrap()
    }
}

... and thus usage of dyn FontResolver becomes problematic. (We probably don't want to add generic parameters to all usages.)

A better option might be to do this:

pub trait FontResolver: Debug + Sync {
    fn with_fontdb_dyn(&self, f: Box<dyn FnOnce(&Database) + '_>);
    // ...
}

pub trait FontResolverExt: FontResolver {
    fn with_fontdb<R>(&self, f: impl (FnOnce(&Database) -> R)) -> Option<R> {
        let mut result = None;
        let out = &mut result;
        self.with_fontdb_dyn(Box::new(|db| *out = Some(f(db))));
        result
    }
}
impl<FR: FontResolver + ?Sized> FontResolverExt for FR {}

Then we can still use dyn FontResolver:

pub(crate) fn convert(text: &mut Text, resolver: &dyn FontResolver) -> Option<()> {
    let (text_fragments, bbox) = layout::layout_text(text, resolver)?;
    text.layouted = text_fragments;
    text.bounding_box = bbox.to_rect();
    text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect();

    let (group, stroke_bbox) = resolver.with_fontdb(|fontdb| flatten::flatten(text, fontdb))??;
    text.flattened = Box::new(group);
    text.stroke_bounding_box = stroke_bbox.to_rect();
    text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect();

    Some(())
}

@dhardy
Copy link
Contributor Author

dhardy commented Jun 29, 2024

Another reason for this is that in the future we will support fonts embedded in CSS, which must not be added to the "global" fontdb. That's why we need an ability to deeply clone the database on-demand.

Admittedly, this is a big motivation to keep the existing design (passing Arc<Database> in). Especially if you have another mechanism for caching loaded fonts.

@laurmaedje
Copy link
Contributor

The intention is that a wrapper type be used to manage that lock... which also requires an associated type (something I forgot to add previously)...

... and thus usage of dyn FontResolver becomes problematic. (We probably don't want to add generic parameters to all usages.)

I went down the same path of thoughts during #754 and ended up with a very similar approach here, but I think it's far from pretty.

@dhardy
Copy link
Contributor Author

dhardy commented Aug 28, 2024

What if Options contains an Arc<dyn FontResolver> instead (where FontResolver is some trait impl'd by fontdb::Database)? An Arc<fontdb::Database> will upcast appropriately, as will a user-defined type T implementing that trait.

Edit: doesn't work because you need clone support.

I'd also be satisfied with Arc<T> where T: Borrow<fontdb::Database> + Clone, but I understand not wanting to use generics.

@laurmaedje
Copy link
Contributor

Could you elaborate more on your motivation? The three bullet points from the original post all sound like conceptual concerns rather than motivating a specific use case that isn't currently achievable.

@dhardy
Copy link
Contributor Author

dhardy commented Aug 28, 2024

To further explain my motivation: kas_text::fonts::Database is a struct wrapping fontdb::Database with a set of font aliases. This is used to implement FontSelector::select, where FontSelector is roughly akin to usvg::Font (i.e. font family name, weight, style).

Ideally therefore I would implement usvg::FontSelectionFn as a wrapper around my FontSelector::select which requires access to the aliases, which, in my case, are stored alongside the fontdb::Database.

I could store the whole kas_text::fonts::Database in an Arc. But the current design would require that aliases be pushed into a static value. This may be feasible given that an app usually only uses one font configuration.

@laurmaedje
Copy link
Contributor

For what it's worth, Typst also has its own font loading and metadata stack and its implementation of FontSelectionFn can be found here. Might be interesting.

Since the font selection functions are only needed during parsing, I think you could maybe just borrow from your aliases in them? I personally also had to a use a Mutex to maintain a mapping between fontdb and Typst IDs. Might or might not be necessary in your case.

@dhardy
Copy link
Contributor Author

dhardy commented Aug 28, 2024

It seems this approach is viable. Work on the font management side is here: kas-gui/kas-text#87. (The SVG font resolver is WIP.)

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

Successfully merging this pull request may close these issues.

4 participants