diff --git a/Cargo.lock b/Cargo.lock index 4305391..86dc7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2154,6 +2154,7 @@ dependencies = [ "tracing-subscriber", "treehouse-format", "ulid", + "xmlparser", ] [[package]] @@ -2491,6 +2492,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.7.5" diff --git a/crates/treehouse/Cargo.toml b/crates/treehouse/Cargo.toml index d58ae13..509b003 100644 --- a/crates/treehouse/Cargo.toml +++ b/crates/treehouse/Cargo.toml @@ -31,3 +31,4 @@ tracing.workspace = true tracing-chrome = "0.7.2" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } ulid = "1.0.0" +xmlparser = "0.13.6" diff --git a/crates/treehouse/src/vfs/image_size_cache.rs b/crates/treehouse/src/vfs/image_size_cache.rs index 48bb5f7..ec0fcab 100644 --- a/crates/treehouse/src/vfs/image_size_cache.rs +++ b/crates/treehouse/src/vfs/image_size_cache.rs @@ -29,11 +29,16 @@ where fn compute_image_size(&self, path: &VPath) -> anyhow::Result> { if path.extension().is_some_and(config::is_image_file) { if let Some(content) = query::(&self.inner, path) { - let reader = image::ImageReader::new(Cursor::new(content.bytes())) - .with_guessed_format() - .context("cannot guess image format")?; - let (width, height) = reader.into_dimensions()?; - return Ok(Some(ImageSize { width, height })); + if path.extension() == Some("svg") { + return Ok(svg_size(&content.string()?)); + } else { + let _span = info_span!("raster_image_size").entered(); + let reader = image::ImageReader::new(Cursor::new(content.bytes())) + .with_guessed_format() + .context("cannot guess image format")?; + let (width, height) = reader.into_dimensions()?; + return Ok(Some(ImageSize { width, height })); + } } } @@ -77,3 +82,60 @@ where write!(f, "ImageSizeCache({:?})", self.inner) } } + +/// Quickly determine the size of an SVG without parsing it into a DOM. +/// +/// This method is a tentative check; the point is to return an image size that's _good enough +/// default_ rather than what the size is going to be in the user's web browser. +#[instrument(skip(svg))] +fn svg_size(svg: &str) -> Option { + let mut tokenizer = xmlparser::Tokenizer::from(svg); + + fn parse_view_box(s: &str) -> Option<[u32; 4]> { + let mut iter = s.split_whitespace(); + let min_x = iter.next()?.parse().ok()?; + let min_y = iter.next()?.parse().ok()?; + let width = iter.next()?.parse().ok()?; + let height = iter.next()?.parse().ok()?; + Some([min_x, min_y, width, height]) + } + + let mut in_svg = false; + let mut width: Option = None; + let mut height: Option = None; + let mut view_box: Option<[u32; 4]> = None; + while let Some(Ok(token)) = tokenizer.next() { + if let xmlparser::Token::ElementStart { local, .. } = &token { + if local == "svg" { + in_svg = true; + continue; + } + } + + if in_svg { + // If another element starts, we're no longer in the root . + if let xmlparser::Token::ElementStart { .. } = &token { + break; + } + + if let xmlparser::Token::Attribute { local, value, .. } = &token { + match local.as_str() { + "width" => width = value.parse().ok(), + "height" => height = value.parse().ok(), + "viewBox" => { + view_box = parse_view_box(value); + } + _ => (), + } + continue; + } + } + } + + match (width, height, view_box) { + (Some(width), Some(height), _) | (_, _, Some([_, _, width, height])) => { + Some(ImageSize { width, height }) + } + _ => None, + } +}