linebender / resvg

An SVG rendering library.
Apache License 2.0
2.85k stars 228 forks source link

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

Open dhardy opened 5 months ago

dhardy commented 5 months ago

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:

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

RazrFalcon commented 5 months ago

@LaurenzV

RazrFalcon commented 5 months ago

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

LaurenzV commented 5 months ago

I'll delegate to @laurmaedje. :p

dhardy commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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

laurmaedje commented 5 months ago

For context, the current implementation comes from https://github.com/RazrFalcon/resvg/pull/769. This implementation was preceded by the earlier PR https://github.com/RazrFalcon/resvg/pull/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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

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