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:
@@ -100,19 +100,6 @@ fn main() {
|
|||||||
.set(bevy::asset::AssetPlugin {
|
.set(bevy::asset::AssetPlugin {
|
||||||
file_path: "../assets".to_string(),
|
file_path: "../assets".to_string(),
|
||||||
..default()
|
..default()
|
||||||
})
|
|
||||||
// The bundled hayeah card SVGs declare `font-family="Arial"`
|
|
||||||
// for rank/suit text. usvg compares family names exactly,
|
|
||||||
// so on systems without Arial installed (every Linux
|
|
||||||
// distro by default) it bridges a `log::warn!` per text
|
|
||||||
// node into our tracing output — 50+ lines per game on
|
|
||||||
// launch. The substitution path in `svg_loader::shared_fontdb`
|
|
||||||
// already resolves the glyphs to whatever sans-serif the
|
|
||||||
// user does have; the warn is purely informational and
|
|
||||||
// dropping it leaves real errors visible.
|
|
||||||
.set(bevy::log::LogPlugin {
|
|
||||||
filter: format!("{},usvg::text=error", bevy::log::DEFAULT_FILTER),
|
|
||||||
..default()
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.add_plugins(AssetSourcesPlugin)
|
.add_plugins(AssetSourcesPlugin)
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ impl AssetLoader for SvgLoader {
|
|||||||
pub fn rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, SvgLoaderError> {
|
pub fn rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, SvgLoaderError> {
|
||||||
let opt = usvg::Options {
|
let opt = usvg::Options {
|
||||||
fontdb: shared_fontdb(),
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let tree = usvg::Tree::from_data(svg_bytes, &opt)?;
|
let tree = usvg::Tree::from_data(svg_bytes, &opt)?;
|
||||||
@@ -167,6 +173,54 @@ fn shared_fontdb() -> Arc<fontdb::Database> {
|
|||||||
.clone()
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -201,6 +255,28 @@ mod tests {
|
|||||||
assert!(matches!(err, SvgLoaderError::PixmapAlloc(0, 100)));
|
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]
|
#[test]
|
||||||
fn rejects_malformed_svg() {
|
fn rejects_malformed_svg() {
|
||||||
let err = rasterize_svg(b"not actually svg", UVec2::new(64, 96)).unwrap_err();
|
let err = rasterize_svg(b"not actually svg", UVec2::new(64, 96)).unwrap_err();
|
||||||
|
|||||||
Reference in New Issue
Block a user