diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8dea779..efbf835 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2095,9 +2095,9 @@ dependencies = [ [[package]] name = "takumi" -version = "1.0.7" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4bb0cc39b45f7cd1c1d8dd669f713460a907c4f99dddc5982e1fadb2ce6a729" +checksum = "abdce7c3fce9eeb272b5d6cffd26e8cfa1ebf9ea083b2532df8dd7a16d903b6e" dependencies = [ "bitflags 2.11.0", "bytemuck", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e9044f9..672caac 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,7 +11,7 @@ name = "takumi_php" crate-type = ["cdylib"] [dependencies] -takumi = { version = "1.0.7", features = ["woff2", "woff"] } +takumi = { version = "1.0.15", features = ["woff2", "woff"] } parley = { version = "0.8", default-features = false } scraper = "0.22" ego-tree = "0.10" diff --git a/rust/src/internal/html.rs b/rust/src/internal/html.rs index a25076c..88f62be 100644 --- a/rust/src/internal/html.rs +++ b/rust/src/internal/html.rs @@ -105,6 +105,30 @@ fn convert_node( return Some(Node::text("\n".to_string())); } + // — serialize the whole subtree back to SVG markup and treat it + // as an image source so resvg can rasterize it. + if tag == "svg" { + if let Some(el) = scraper::ElementRef::wrap(node_ref) { + let mut svg_markup = el.html(); + // html5ever may drop the xmlns attribute when serializing SVG + // in an HTML context; takumi's is_svg_like() requires it. + if !svg_markup.contains("xmlns") { + svg_markup = svg_markup + .replacen("().ok()), + element.attr("height").and_then(|h| h.parse::().ok()), + ) { + (Some(w), Some(h)) => Node::image((svg_markup.as_str(), w, h)), + _ => Node::image(svg_markup.as_str()), + }; + let node = apply_metadata(node, element, extra_css, counter); + return Some(node); + } + return None; + } + // — image node if tag == "img" { let src = element.attr("src").unwrap_or("").to_string(); @@ -238,6 +262,45 @@ mod tests { ); } + #[test] + fn inline_svg_renders_opaque() { + // A red square drawn as inline SVG — should produce non-transparent pixels. + let image = render_html( + r#" + + "#, + ); + let p = image.get_pixel(100, 100); + assert!( + p[3] > 0, + "expected opaque pixel at (100,100) from inline SVG, got transparent; RGBA={p:?}" + ); + assert!( + p[0] > 200, + "expected red pixel at (100,100) from inline SVG; RGBA={p:?}" + ); + } + + #[test] + fn inline_svg_respects_width_height_attributes() { + // SVG is 50×50 but placed in a 200×200 canvas — pixel at (150,150) must be + // transparent, proving the image node was sized to 50×50 and not stretched. + let image = render_html( + r#" + + "#, + ); + let inside = image.get_pixel(25, 25); + assert!(inside[3] > 0, "expected opaque pixel inside the SVG at (25,25); RGBA={inside:?}"); + assert!(inside[2] > 150, "expected blue pixel inside the SVG at (25,25); RGBA={inside:?}"); + + let outside = image.get_pixel(150, 150); + assert!( + outside[3] == 0, + "expected transparent pixel outside the 50×50 SVG at (150,150); RGBA={outside:?}" + ); + } + /// Diagnostic: print what the scraper tree looks like for a simple fragment. #[test] fn diagnose_fragment_tree_structure() {