diff --git a/Cargo.lock b/Cargo.lock index a441ae1005..256b34c27f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,7 +282,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -293,7 +293,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -305,8 +305,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "anyrender" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea77d85ae0be665528618c97164c2b510dc635e80b805e9a9df9a9fe72da423" +source = "git+https://github.com/DioxusLabs/anyrender?branch=devin%2F1785858394-usvg-048#baed6ca2253c9b3005c295948ceba8690235cf86" dependencies = [ "kurbo", "peniko", @@ -324,7 +323,7 @@ dependencies = [ "anyrender", "image", "peniko", - "read-fonts", + "read-fonts 0.39.2", "serde", "serde_json", "sha2", @@ -365,8 +364,7 @@ dependencies = [ [[package]] name = "anyrender_svg" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "454619983a9b55af8692211e15d171189af9fc677646b8a4724c822fa9b6d8c5" +source = "git+https://github.com/DioxusLabs/anyrender?branch=devin%2F1785858394-usvg-048#baed6ca2253c9b3005c295948ceba8690235cf86" dependencies = [ "anyrender", "image", @@ -786,6 +784,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bindgen" version = "0.72.1" @@ -896,7 +900,7 @@ dependencies = [ "percent-encoding", "rayon", "selectors", - "skrifa", + "skrifa 0.42.1", "slotmap", "smallvec", "stylo", @@ -1483,7 +1487,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width 0.2.2", ] [[package]] @@ -2458,7 +2462,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2718,7 +2722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2865,6 +2869,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7299a780854a6d391be2ae1c8521c9368471b559dbfd6a8dbd9f407eaff100" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -2876,16 +2889,15 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +checksum = "2660c5e9157bf76d2db1294e4a9feba604ef610819a3b591088d0d8392a3290f" dependencies = [ "fontconfig-parser", "log", "memmap2 0.9.11", "slotmap", "tinyvec", - "ttf-parser", ] [[package]] @@ -2902,7 +2914,7 @@ dependencies = [ "objc2-core-text", "objc2-foundation 0.3.2", "parlance", - "read-fonts", + "read-fonts 0.39.2", "roxmltree 0.21.1", "smallvec", "windows", @@ -3206,7 +3218,7 @@ dependencies = [ "hashbrown 0.17.1", "log", "peniko", - "skrifa", + "skrifa 0.42.1", "smallvec", "vello_common", ] @@ -3396,7 +3408,19 @@ checksum = "d12c7c642d4ce8c2e784b4751a6634bd89583912265add4a679a8882d123fbcd" dependencies = [ "bitflags 2.13.0", "bytemuck", - "read-fonts", + "read-fonts 0.39.2", + "smallvec", +] + +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "read-fonts 0.41.0", "smallvec", ] @@ -3866,9 +3890,9 @@ dependencies = [ [[package]] name = "imagesize" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" [[package]] name = "imgref" @@ -4724,7 +4748,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4843,7 +4867,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc8f19a1c361e5fb1a57e487b993ff8b3c44321b50ca86c9e2b235e57dc94008" dependencies = [ - "font-types", + "font-types 0.11.3", ] [[package]] @@ -5330,7 +5354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1cfcf399c774719fb1fa51bc6b91e86bdf003b03202a0902f1827ba6750746" dependencies = [ "fontique", - "harfrust", + "harfrust 0.8.4", "hashbrown 0.17.1", "icu_normalizer", "icu_properties", @@ -5338,7 +5362,7 @@ dependencies = [ "linebender_resource_handle", "parlance", "parley_data", - "skrifa", + "skrifa 0.42.1", ] [[package]] @@ -5937,7 +5961,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", - "font-types", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.2", + "once_cell", ] [[package]] @@ -6203,7 +6238,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6245,24 +6280,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.13.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.23" @@ -6626,7 +6643,7 @@ checksum = "caad12d9a3f75948b725a643cd60aea6de9d20cb03d3e35221e7a8a31c549409" dependencies = [ "fnv", "hashbrown 0.17.1", - "skrifa", + "skrifa 0.42.1", "thiserror 2.0.18", "write-fonts", ] @@ -6665,7 +6682,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.39.2", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", ] [[package]] @@ -6772,7 +6799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7203,7 +7230,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7327,7 +7354,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "tiny-skia-path", + "tiny-skia-path 0.11.4", ] [[package]] @@ -7341,6 +7368,17 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tiny-xlib" version = "0.2.5" @@ -7706,9 +7744,6 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] [[package]] name = "ttf2woff2" @@ -7759,7 +7794,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7792,18 +7827,6 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -7819,12 +7842,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-script" version = "0.5.8" @@ -7882,25 +7899,25 @@ dependencies = [ [[package]] name = "usvg" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +version = "0.48.1" +source = "git+https://github.com/DioxusLabs/resvg?branch=devin%2F1785858271-intrinsic-dimensions#3289a9b0c3d3352692bf5acdf5f6e6949cdb57b5" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "data-url", "flate2", "fontdb", + "harfrust 0.12.0", "imagesize", "kurbo", "log", "pico-args", "roxmltree 0.21.1", - "rustybuzz", "simplecss", "siphasher", + "skrifa 0.44.0", "strict-num", "svgtypes", - "tiny-skia-path", + "tiny-skia-path 0.12.0", "unicode-bidi", "unicode-script", "unicode-vo", @@ -7970,7 +7987,7 @@ dependencies = [ "log", "peniko", "png 0.18.1", - "skrifa", + "skrifa 0.42.1", "static_assertions", "thiserror 2.0.18", "vello_encoding", @@ -8020,7 +8037,7 @@ dependencies = [ "bytemuck", "guillotiere", "peniko", - "skrifa", + "skrifa 0.42.1", "smallvec", ] @@ -8650,7 +8667,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9298,11 +9315,11 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" dependencies = [ - "font-types", + "font-types 0.11.3", "indexmap", "kurbo", "log", - "read-fonts", + "read-fonts 0.39.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dac4e21567..fb2834730b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ linebender_resource_handle = "0.1" peniko = "0.6.0" kurbo = "0.13.1" wgpu = "29" -usvg = "0.46" +usvg = "0.48.1" # Windowing & Input raw-window-handle = "0.6.0" @@ -270,6 +270,13 @@ color = { workspace = true } env_logger = "0.11" tracing-subscriber = "0.3" +# TODO: remove these patches once `usvg` (with `Tree::intrinsic_dimensions`) and +# `anyrender_svg` (with the usvg 0.48 bump) are released. +[patch.crates-io] +usvg = { git = "https://github.com/DioxusLabs/resvg", branch = "devin/1785858271-intrinsic-dimensions" } +anyrender = { git = "https://github.com/DioxusLabs/anyrender", branch = "devin/1785858394-usvg-048" } +anyrender_svg = { git = "https://github.com/DioxusLabs/anyrender", branch = "devin/1785858394-usvg-048" } + # [patch.crates-io] # anyrender = { path = "../anyrender/crates/anyrender" } # anyrender_skia = { path = "../anyrender/crates/anyrender_skia" } diff --git a/packages/blitz-dom/src/layout/mod.rs b/packages/blitz-dom/src/layout/mod.rs index f79a8999b6..e82a3fc050 100644 --- a/packages/blitz-dom/src/layout/mod.rs +++ b/packages/blitz-dom/src/layout/mod.rs @@ -180,7 +180,7 @@ impl BaseDocument { // // TODO: smarter sizing using these (depending on object-fit, they shouldn't // necessarily just override the native size) - let attr_size = taffy::Size { + let mut attr_size = taffy::Size { width: element_data .attr(local_name!("width")) .and_then(|val| val.parse::().ok()), @@ -201,12 +201,36 @@ impl BaseDocument { } #[cfg(feature = "svg")] ImageData::Svg(svg) => { - let size = svg.tree.size(); - let size = taffy::Size { - width: size.width(), - height: size.height(), - }; - (size, Some(size.width / size.height)) + // For an inline `` element the width/height attributes are + // presentation attributes: percentages resolve against the + // containing block. For SVG loaded as an image the intrinsic + // dimensions are context-free. + if *element_data.name.local == local_name!("svg") { + attr_size = taffy::Size { + width: svg.resolved_width(inputs.parent_size.width), + height: svg.resolved_height(inputs.parent_size.height), + }; + } + let (mut width, mut height) = svg.intrinsic_size(); + // A replaced element with only an intrinsic aspect ratio uses the + // stretch-fit width in normal flow (CSS2 ยง10.3.2): fill the + // definite available width and derive the height from the ratio. + // Shrink-to-fit contexts (floats, abspos) keep the default object + // size that `intrinsic_size` already applied. + if svg.intrinsic_width().is_none() + && svg.intrinsic_height().is_none() + { + if let ( + Some(ratio), + AvailableSpace::Definite(available_width), + ) = + (svg.viewbox_aspect_ratio(), inputs.available_space.width) + { + width = available_width; + height = available_width / ratio; + } + } + (taffy::Size { width, height }, Some(svg.aspect_ratio())) } ImageData::None => (taffy::Size::ZERO, None), }, diff --git a/packages/blitz-dom/src/node/element.rs b/packages/blitz-dom/src/node/element.rs index 197312bff9..c1e0e8ab46 100644 --- a/packages/blitz-dom/src/node/element.rs +++ b/packages/blitz-dom/src/node/element.rs @@ -696,34 +696,121 @@ impl RasterImageData { } } -/// A parsed SVG image together with its CSS intrinsic dimensions. +/// A parsed SVG image. /// /// usvg always resolves the root `` to a concrete [`usvg::Tree::size`], /// falling back to the `viewBox` size when `width`/`height` are absent or given /// as percentages. For CSS sizing purposes, however, such an SVG has *no* -/// intrinsic width/height (only an intrinsic aspect ratio). We record which -/// dimensions were actually declared as absolute lengths so the paint layer can -/// apply the CSS default sizing algorithm correctly. +/// intrinsic width/height (only an intrinsic aspect ratio). The accessors on +/// this type resolve the CSS intrinsic dimensions lazily from +/// [`usvg::Tree::intrinsic_dimensions`], which preserves what was actually +/// declared on the root element. #[cfg(feature = "svg")] #[derive(Debug, Clone)] pub struct SvgImageData { /// The parsed SVG tree. pub tree: Arc, +} + +#[cfg(feature = "svg")] +impl SvgImageData { /// The intrinsic width in CSS px, present only when the root `` /// declared an absolute (non-percentage) `width`. - pub intrinsic_width: Option, + pub fn intrinsic_width(&self) -> Option { + use usvg::svgtypes::LengthUnit; + let declared = self + .tree + .intrinsic_dimensions() + .width + .is_some_and(|len| len.unit != LengthUnit::Percent); + declared.then(|| self.tree.size().width()) + } + /// The intrinsic height in CSS px, present only when the root `` /// declared an absolute (non-percentage) `height`. - pub intrinsic_height: Option, -} + pub fn intrinsic_height(&self) -> Option { + use usvg::svgtypes::LengthUnit; + let declared = self + .tree + .intrinsic_dimensions() + .height + .is_some_and(|len| len.unit != LengthUnit::Percent); + declared.then(|| self.tree.size().height()) + } -#[cfg(feature = "svg")] -impl SvgImageData { - /// The intrinsic aspect ratio of the SVG (always available: usvg resolves - /// the `viewBox` or declared size into a non-zero [`usvg::Tree::size`]). + /// The aspect ratio of the root ``'s `viewBox`, if it declares one. + pub fn viewbox_aspect_ratio(&self) -> Option { + self.tree + .intrinsic_dimensions() + .view_box + .map(|vb| vb.width() / vb.height()) + } + + /// The root `width` attribute resolved against a containing block width: + /// percentages resolve against the containing block (`None` if it is + /// indefinite) and an absent attribute is `None`. + /// + /// This is only appropriate for an inline `` element, where the + /// attributes behave as presentation attributes. SVG used as an image + /// (e.g. `` or a background) must use [`Self::intrinsic_width`], + /// as its intrinsic dimensions are context-free per CSS. + pub fn resolved_width(&self, container_width: Option) -> Option { + use usvg::svgtypes::LengthUnit; + match self.tree.intrinsic_dimensions().width { + Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()), + Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0), + None => None, + } + } + + /// The root `height` attribute resolved against a containing block height. + /// See [`Self::resolved_width`]. + pub fn resolved_height(&self, container_height: Option) -> Option { + use usvg::svgtypes::LengthUnit; + match self.tree.intrinsic_dimensions().height { + Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()), + Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0), + None => None, + } + } + + /// The intrinsic aspect ratio of the SVG: the ratio of its declared + /// `width`/`height` when both are absolute lengths, otherwise the + /// `viewBox` ratio, otherwise the ratio of the resolved + /// [`usvg::Tree::size`] (which is always non-zero). pub fn aspect_ratio(&self) -> f32 { - let size = self.tree.size(); - size.width() / size.height() + match (self.intrinsic_width(), self.intrinsic_height()) { + (Some(w), Some(h)) => w / h, + _ => self.viewbox_aspect_ratio().unwrap_or_else(|| { + let size = self.tree.size(); + size.width() / size.height() + }), + } + } + + /// The intrinsic dimensions of the SVG resolved per CSS replaced element + /// sizing: a missing dimension is computed from the declared one and the + /// intrinsic aspect ratio; if neither is declared, the resolved + /// [`usvg::Tree::size`] is used as a fallback. + pub fn intrinsic_size(&self) -> (f32, f32) { + let aspect_ratio = self.aspect_ratio(); + match (self.intrinsic_width(), self.intrinsic_height()) { + (Some(w), Some(h)) => (w, h), + (Some(w), None) => (w, w / aspect_ratio), + (None, Some(h)) => (h * aspect_ratio, h), + (None, None) => { + // No intrinsic dimensions. If there is an intrinsic aspect ratio, apply + // the CSS default sizing algorithm: contain within the default object + // size of 300x150. Otherwise fall back to the resolved tree size. + if self.viewbox_aspect_ratio().is_some() { + let scale = (300.0 / aspect_ratio).min(150.0); + (scale * aspect_ratio, scale) + } else { + let size = self.tree.size(); + (size.width(), size.height()) + } + } + } } } diff --git a/packages/blitz-dom/src/util.rs b/packages/blitz-dom/src/util.rs index 7309842ef9..c8b86a2f95 100644 --- a/packages/blitz-dom/src/util.rs +++ b/packages/blitz-dom/src/util.rs @@ -153,87 +153,19 @@ pub fn walk_tree(indent: usize, node: &Node) { } } -/// Parse an SVG image and record its CSS intrinsic dimensions. -/// -/// usvg always resolves the root `` to a concrete size, using the -/// `viewBox` when `width`/`height` are missing or expressed as percentages, and -/// its [`usvg::Tree`] does not retain the raw `width`/`height` attributes. For -/// CSS sizing, however, an SVG in that situation has only an intrinsic aspect -/// ratio and *no* intrinsic width/height, so we inspect the root `` -/// attributes ourselves. -/// -/// To avoid parsing the XML twice, we parse it once into a -/// [`usvg::roxmltree::Document`] (mirroring [`usvg::Tree::from_data`], including -/// SVGZ decompression) and reuse that document both to read the intrinsic -/// dimensions and to build the tree via [`usvg::Tree::from_xmltree`]. +/// Parse an SVG image. #[cfg(feature = "svg")] pub(crate) fn parse_svg_image(source: &[u8]) -> Result { - use usvg::roxmltree; - let options = usvg::Options { fontdb: Arc::clone(&*FONT_DB), ..Default::default() }; - - // Transparently decompress gzip-compressed SVGZ, as `Tree::from_data` does. - let decompressed; - let data = if source.starts_with(&[0x1f, 0x8b]) { - decompressed = usvg::decompress_svgz(source)?; - &decompressed[..] - } else { - source - }; - let text = std::str::from_utf8(data).map_err(|_| usvg::Error::NotAnUtf8Str)?; - - let xml_opt = roxmltree::ParsingOptions { - allow_dtd: true, - ..Default::default() - }; - let doc = roxmltree::Document::parse_with_options(text, xml_opt) - .map_err(usvg::Error::ParsingFailed)?; - - let (has_width, has_height) = svg_has_absolute_dimensions(&doc); - - let tree = usvg::Tree::from_xmltree(&doc, &options)?; - let size = tree.size(); + let tree = usvg::Tree::from_data(source, &options)?; Ok(crate::node::SvgImageData { - intrinsic_width: has_width.then(|| size.width()), - intrinsic_height: has_height.then(|| size.height()), tree: Arc::new(tree), }) } -/// Returns whether the root `` element declares absolute (non-percentage) -/// `width` and `height` attributes. A missing attribute defaults to `100%`, so -/// it is treated as non-absolute (i.e. no intrinsic dimension). -#[cfg(feature = "svg")] -fn svg_has_absolute_dimensions(doc: &usvg::roxmltree::Document) -> (bool, bool) { - let root = doc.root_element(); - ( - root.attribute("width").is_some_and(is_absolute_length), - root.attribute("height").is_some_and(is_absolute_length), - ) -} - -/// Returns whether `value` is a valid absolute (non-percentage) SVG length such -/// as `48`, `12px`, or `2.5em`. Percentages are relative, and unparseable -/// values are not lengths at all, so neither counts as an intrinsic dimension. -#[cfg(feature = "svg")] -fn is_absolute_length(value: &str) -> bool { - let value = value.trim(); - if value.is_empty() || value.ends_with('%') { - return false; - } - // Strip a recognised absolute unit (if present) and require the remaining - // numeric part to be a finite number, mirroring how usvg parses lengths. - let number = ["px", "pt", "pc", "mm", "cm", "in", "em", "ex"] - .iter() - .find_map(|unit| value.strip_suffix(unit)) - .unwrap_or(value) - .trim(); - number.parse::().is_ok_and(f64::is_finite) -} - pub trait ToColorColor { /// Converts a color into the `AlphaColor` type from the `color` crate fn as_color_color(&self) -> Color; @@ -252,12 +184,24 @@ impl ToColorColor for AbsoluteColor { mod svg_tests { use super::parse_svg_image; + #[test] + fn missing_height_is_computed_from_width_and_viewbox_ratio() { + let src = br#""#; + let svg = parse_svg_image(src).unwrap(); + assert_eq!(svg.intrinsic_width(), Some(200.0)); + assert_eq!(svg.intrinsic_height(), None); + assert_eq!(svg.viewbox_aspect_ratio(), Some(1.0)); + assert_eq!(svg.tree.size().width(), 200.0); + assert_eq!(svg.tree.size().height(), 200.0); + assert_eq!(svg.intrinsic_size(), (200.0, 200.0)); + } + #[test] fn viewbox_only_has_no_intrinsic_dimensions() { let src = br#""#; let svg = parse_svg_image(src).unwrap(); - assert_eq!(svg.intrinsic_width, None); - assert_eq!(svg.intrinsic_height, None); + assert_eq!(svg.intrinsic_width(), None); + assert_eq!(svg.intrinsic_height(), None); // The aspect ratio is still available from the viewBox. assert!((svg.aspect_ratio() - (485.0 / 58.0)).abs() < 1e-3); } @@ -266,47 +210,32 @@ mod svg_tests { fn absolute_dimensions_are_intrinsic() { let src = br#""#; let svg = parse_svg_image(src).unwrap(); - assert_eq!(svg.intrinsic_width, Some(24.0)); - assert_eq!(svg.intrinsic_height, Some(16.0)); + assert_eq!(svg.intrinsic_width(), Some(24.0)); + assert_eq!(svg.intrinsic_height(), Some(16.0)); } #[test] fn percentage_dimensions_are_not_intrinsic() { let src = br#""#; let svg = parse_svg_image(src).unwrap(); - assert_eq!(svg.intrinsic_width, None); - assert_eq!(svg.intrinsic_height, None); + assert_eq!(svg.intrinsic_width(), None); + assert_eq!(svg.intrinsic_height(), None); } #[test] fn unit_lengths_are_intrinsic() { let src = br#""#; let svg = parse_svg_image(src).unwrap(); - assert!(svg.intrinsic_width.is_some()); - assert!(svg.intrinsic_height.is_some()); + assert!(svg.intrinsic_width().is_some()); + assert!(svg.intrinsic_height().is_some()); } #[test] fn non_numeric_dimensions_are_not_intrinsic() { let src = br#""#; let svg = parse_svg_image(src).unwrap(); - assert_eq!(svg.intrinsic_width, None); - assert_eq!(svg.intrinsic_height, None); - } - - #[test] - fn is_absolute_length_validates_numbers() { - use super::is_absolute_length; - assert!(is_absolute_length("48")); - assert!(is_absolute_length(" 12px ")); - assert!(is_absolute_length("2.5em")); - assert!(is_absolute_length("1e3")); - - assert!(!is_absolute_length("100%")); - assert!(!is_absolute_length("auto")); - assert!(!is_absolute_length("foo")); - assert!(!is_absolute_length("")); - assert!(!is_absolute_length("px")); + assert_eq!(svg.intrinsic_width(), None); + assert_eq!(svg.intrinsic_height(), None); } } diff --git a/packages/blitz-paint/src/render/background.rs b/packages/blitz-paint/src/render/background.rs index 6e7080dbb5..7ffc89f6f1 100644 --- a/packages/blitz-paint/src/render/background.rs +++ b/packages/blitz-paint/src/render/background.rs @@ -291,7 +291,7 @@ impl ElementCx<'_, '_> { // both the concrete size (used verbatim for `auto`) and the aspect // ratio (used by `cover`/`contain` and single-`auto` sizes). let aspect_ratio = svg.aspect_ratio(); - let (object_w, object_h) = match (svg.intrinsic_width, svg.intrinsic_height) { + let (object_w, object_h) = match (svg.intrinsic_width(), svg.intrinsic_height()) { (Some(w), Some(h)) => (w, h), (Some(w), None) => (w, w / aspect_ratio), (None, Some(h)) => (h * aspect_ratio, h), diff --git a/tests/blitz-tests/tests/svg_background_size.rs b/tests/blitz-tests/tests/svg_background_size.rs index dfcd1a3a61..531b04fb88 100644 --- a/tests/blitz-tests/tests/svg_background_size.rs +++ b/tests/blitz-tests/tests/svg_background_size.rs @@ -16,17 +16,9 @@ use blitz_traits::shell::{ColorScheme, Viewport}; use std::sync::Arc; /// Renders a 100x100 div whose `background` is set to the given shorthand and -/// whose loaded background image is the provided SVG source. `intrinsic_*` -/// mirror what `parse_svg_image` would detect for the SVG. Returns the pixel +/// whose loaded background image is the provided SVG source. Returns the pixel /// at (x, y). The div sits on a solid blue page background. -fn pixel( - background: &str, - svg_src: &str, - intrinsic_width: Option, - intrinsic_height: Option, - x: usize, - y: usize, -) -> [u8; 3] { +fn pixel(background: &str, svg_src: &str, x: usize, y: usize) -> [u8; 3] { let html = format!( r#"
@@ -47,8 +39,6 @@ fn pixel( usvg::Tree::from_str(svg_src, &usvg::Options::default()).expect("valid test SVG"); let svg = SvgImageData { tree: Arc::new(tree), - intrinsic_width, - intrinsic_height, }; let node = doc.get_node_mut(box_id).unwrap(); let el = node.element_data_mut().unwrap(); @@ -85,8 +75,6 @@ fn viewbox_only_svg_is_contained_not_intrinsic() { let top = pixel( "url('https://example.com/x.svg') no-repeat", VIEWBOX_ONLY, - None, - None, 50, 25, ); @@ -95,8 +83,6 @@ fn viewbox_only_svg_is_contained_not_intrinsic() { let bottom = pixel( "url('https://example.com/x.svg') no-repeat", VIEWBOX_ONLY, - None, - None, 50, 75, ); @@ -113,8 +99,6 @@ fn svg_with_intrinsic_size_uses_it() { let inside = pixel( "url('https://example.com/x.svg') no-repeat", WITH_SIZE, - Some(50.0), - Some(50.0), 25, 25, ); @@ -126,8 +110,6 @@ fn svg_with_intrinsic_size_uses_it() { let outside = pixel( "url('https://example.com/x.svg') no-repeat", WITH_SIZE, - Some(50.0), - Some(50.0), 75, 75, ); @@ -144,8 +126,6 @@ fn viewbox_only_svg_respects_explicit_contain() { let bottom = pixel( "url('https://example.com/x.svg') 0 0/contain no-repeat", VIEWBOX_ONLY, - None, - None, 50, 75, );