Open dhardy opened 5 months ago
@LaurenzV
Do I understand correctly that your approach doesn't allow sharing fontdb between threads? That's one of the requirements.
I'll delegate to @laurmaedje. :p
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.
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.
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
.
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.
Ok, will wait then. I do not want to break typst again.
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).
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.
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
andselect_fallback
are&self
, we'd have to resort to interior mutability on theDatabase
,
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(())
}
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.
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.
What if An Options
contains an Arc<dyn FontResolver>
instead (where FontResolver
is some trait impl'd by fontdb::Database
)?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.
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.
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.
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.
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.)
Summary
usvg::FontResolver
is changed from a struct to a trait, with a default implementationDefaultFontResolver
.usvg::Options
andusvg::Tree
no longer containfontdb
; instead thenFontResolver
provides access as required.Motivation
The existing model:
fontdb
be shared under anArc
, which shouldn't be necessaryfontdb
before modifyingfontdb
accessible throughusvg::Tree
, which doesn't appear to be the right place for itInstead, the user is expected to provide a
&dyn FontResolver
instance in order to process any SVG involving text. As a consequence:fontdb
instance (without cloning) and may even insert additional fonts directly into this DB (viaRefCell
,Mutex
or similar)fontdb
instance used withoutArc
font_resolver
is not declared beforeusvg::Options
).