use bevy::asset::{Asset, AssetLoader, LoadContext, io::Reader};
use bevy::math::Vec2;
use bevy::prelude::BevyError;
use bevy::reflect::TypePath;
#[derive(Asset, TypePath, Debug)]
pub struct SvgDocument {
pub tree: usvg::Tree,
pub size: Vec2,
}
#[derive(Debug)]
pub struct SvgParseError(pub usvg::Error);
impl std::fmt::Display for SvgParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid SVG: {}", self.0)
}
}
impl std::error::Error for SvgParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
pub fn parse_svg_bytes(bytes: &[u8]) -> Result<SvgDocument, SvgParseError> {
let mut opts = usvg::Options::default();
super::text::configure_text_options(&mut opts);
let tree = usvg::Tree::from_data(bytes, &opts).map_err(SvgParseError)?;
let size = tree.size();
Ok(SvgDocument {
size: Vec2::new(size.width(), size.height()),
tree,
})
}
#[derive(TypePath)]
pub struct SvgAssetLoader;
impl AssetLoader for SvgAssetLoader {
type Asset = SvgDocument;
type Settings = ();
type Error = BevyError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
Ok(parse_svg_bytes(&bytes)?)
}
fn extensions(&self) -> &[&str] {
&["svg"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::svg::CIRCLE_SVG;
#[test]
fn parses_valid_svg_and_rejects_garbage() {
let doc = parse_svg_bytes(CIRCLE_SVG.as_bytes()).expect("valid SVG parses");
assert_eq!(
doc.size,
Vec2::new(100.0, 100.0),
"intrinsic size must come from the viewBox"
);
parse_svg_bytes(b"not svg").expect_err("garbage bytes must be rejected");
parse_svg_bytes(b"").expect_err("empty input must be rejected");
}
#[test]
fn loader_claims_svg_extension() {
assert_eq!(SvgAssetLoader.extensions(), ["svg"]);
}
}