fix(engine): fall through to system default font on unmatched family
Replaces the previous LogPlugin-filter approach (which suppresses the
warn message) with a fix at the source: a custom usvg FontResolver
that appends `sans-serif` and `serif` to every family-lookup query.
usvg's default selector queries fontdb with [SVG-requested families,
Serif] and emits `log::warn!("No match for '{family}'")` when the
query returns None. On systems without the SVG's named family (Arial
on Linux, etc.), every text node logs a warn even though the system
has perfectly good fonts available — the warn is a false negative
because fontdb's named-family lookup is exact-match only.
The new resolver appends both `Family::SansSerif` and `Family::Serif`
to the query, both resolved by fontdb (via fontconfig on Linux or
built-in defaults elsewhere) to whatever the system has installed.
The query now finds *some* face on any reasonably configured machine,
so `id.is_none()` is never true and the warn branch never fires. The
visible behaviour: SVGs that request unavailable named families now
silently use the system's default sans-serif font.
Reverts the LogPlugin filter from main.rs — silencing warns at the
log level was the wrong layer; fixing the lookup is.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,12 @@ impl AssetLoader for SvgLoader {
|
||||
pub fn rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, SvgLoaderError> {
|
||||
let opt = usvg::Options {
|
||||
fontdb: shared_fontdb(),
|
||||
// Default for SVG elements without an explicit `font-family` —
|
||||
// resolved by fontdb's generic-family alias to whatever
|
||||
// sans-serif the system has installed (DejaVu Sans on most
|
||||
// Linux installs, Helvetica on macOS, Arial on Windows).
|
||||
font_family: "sans-serif".to_string(),
|
||||
font_resolver: lenient_font_resolver(),
|
||||
..Default::default()
|
||||
};
|
||||
let tree = usvg::Tree::from_data(svg_bytes, &opt)?;
|
||||
@@ -167,6 +173,54 @@ fn shared_fontdb() -> Arc<fontdb::Database> {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Builds a `usvg::FontResolver` that mirrors the upstream default
|
||||
/// `select_font` but appends the CSS generics `sans-serif` and `serif`
|
||||
/// to every query's family list. The upstream selector only appends
|
||||
/// `serif` and emits a `log::warn!` when its `fontdb.query` returns
|
||||
/// `None`; on systems without the named families requested by the
|
||||
/// SVG (e.g. Arial on Linux), every text node bridges that warn into
|
||||
/// our tracing output. By appending two generics — both resolved via
|
||||
/// fontconfig (or fontdb's built-in defaults) to whatever sans-serif /
|
||||
/// serif the user has installed — we guarantee the query finds *some*
|
||||
/// face, so the warn branch is never taken. The visible behaviour is
|
||||
/// "use the system's default font when the requested one isn't
|
||||
/// installed", which is the intent here.
|
||||
///
|
||||
/// The fallback `select_fallback` is kept as the upstream default —
|
||||
/// per-character fallback (for combining marks, scripts the primary
|
||||
/// face doesn't cover) doesn't have the same warn-spam pathology.
|
||||
fn lenient_font_resolver() -> usvg::FontResolver<'static> {
|
||||
use usvg::{FontFamily, FontResolver};
|
||||
|
||||
usvg::FontResolver {
|
||||
select_font: Box::new(|font, db| {
|
||||
let mut families: Vec<fontdb::Family> = font
|
||||
.families()
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
FontFamily::Serif => fontdb::Family::Serif,
|
||||
FontFamily::SansSerif => fontdb::Family::SansSerif,
|
||||
FontFamily::Cursive => fontdb::Family::Cursive,
|
||||
FontFamily::Fantasy => fontdb::Family::Fantasy,
|
||||
FontFamily::Monospace => fontdb::Family::Monospace,
|
||||
FontFamily::Named(s) => fontdb::Family::Name(s),
|
||||
})
|
||||
.collect();
|
||||
families.push(fontdb::Family::SansSerif);
|
||||
families.push(fontdb::Family::Serif);
|
||||
|
||||
let query = fontdb::Query {
|
||||
families: &families,
|
||||
weight: fontdb::Weight(font.weight()),
|
||||
stretch: font.stretch().into(),
|
||||
style: font.style().into(),
|
||||
};
|
||||
db.query(&query)
|
||||
}),
|
||||
select_fallback: FontResolver::default_fallback_selector(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -201,6 +255,28 @@ mod tests {
|
||||
assert!(matches!(err, SvgLoaderError::PixmapAlloc(0, 100)));
|
||||
}
|
||||
|
||||
/// SVG with a text node that requests an unlikely-installed family
|
||||
/// ("FontThatProbablyDoesNotExist"). Exercises `lenient_font_resolver`'s
|
||||
/// "fall through to system sans-serif/serif" behaviour: rasterising
|
||||
/// must succeed, never panic, and the test runner's log output must
|
||||
/// not contain `No match for ... font-family.` for the named family.
|
||||
/// Catching the warn directly would require a tracing subscriber; we
|
||||
/// rely on `cargo test`'s default behaviour of capturing stdout/stderr
|
||||
/// and surfacing only failing tests' output, plus visual review of
|
||||
/// the suite's log stream.
|
||||
const TEST_SVG_WITH_TEXT: &[u8] = br##"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300" width="200" height="300">
|
||||
<text x="100" y="150" style="font-family:FontThatProbablyDoesNotExist;font-size:32">A</text>
|
||||
</svg>"##;
|
||||
|
||||
#[test]
|
||||
fn rasterizes_svg_with_unmatched_font_family() {
|
||||
let image =
|
||||
rasterize_svg(TEST_SVG_WITH_TEXT, UVec2::new(64, 96)).expect("rasterisation");
|
||||
assert_eq!(image.size().x, 64);
|
||||
assert_eq!(image.size().y, 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_svg() {
|
||||
let err = rasterize_svg(b"not actually svg", UVec2::new(64, 96)).unwrap_err();
|
||||
|
||||
Reference in New Issue
Block a user