diff --git a/.isu/issues.json b/.isu/issues.json index 8594473..50a6f4b 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 384, + "next_id": 386, "issues": [ { "id": 1, @@ -4699,6 +4699,19 @@ "author": "piefev", "state": "open", "created_at": "2026-07-16T11:37:55Z" + }, + { + "id": 385, + "repo": "we", + "title": "Bing parity remains blocked after SVG presentation inheritance", + "body": "Parent: isu issue 280.\n\nAfter adding inherited SVG presentation attributes so root and group fill and stroke settings propagate into child shapes, the Bing real-web scenario still cannot remove its xfail.\n\nRepro: cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts\n\nCurrent committed-golden failure after the SVG pass:\n- desktop: 4.85% match, 1168901 of 1228500 px differ, tolerance 4, max_diff 0.1000%\n- mobile: 5.17% match, 312140 of 329160 px differ, tolerance 4, max_diff 0.1000%\n\nThe DOM, carousel hydration, and interaction assertions still pass; console captures are empty aside from the seeded 404 reported to stdout. The committed Chromium goldens remain stale against the Sichuan Tea snapshot, tracked by isu issue 305. The deterministic same-cache visual gap remains tracked by isu issue 383.\n\nArtifacts:\n- crates/e2e/artifacts/real-web/bing.com/desktop.png\n- crates/e2e/artifacts/real-web/bing.com/desktop.png.diff.png\n- crates/e2e/artifacts/real-web/bing.com/mobile.png\n- crates/e2e/artifacts/real-web/bing.com/mobile.png.diff.png\n\nAcceptance: remove the xfail from crates/e2e/scenarios/real-web/bing.com.we only after the committed Bing Chromium references are deterministic and both screenshot assertions pass within the default threshold.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-16T11:54:04Z" } ] } diff --git a/crates/svg/src/render.rs b/crates/svg/src/render.rs index 590d56b..7865f44 100644 --- a/crates/svg/src/render.rs +++ b/crates/svg/src/render.rs @@ -80,6 +80,7 @@ enum LineJoin { } /// SVG presentation attributes for a single element. +#[derive(Clone)] struct PresentationAttrs { fill: Option, stroke: Option, @@ -206,7 +207,16 @@ pub fn render_svg_scaled( let total = ss as f32 * ds; let base_transform = Transform::scale(total, total).multiply(&viewport.transform()); - render_children(doc, svg_node, &base_transform, &mut canvas, font); + let root_attrs = parse_presentation_attrs(doc, svg_node, &PresentationAttrs::default()); + let root_transform = base_transform.multiply(&root_attrs.transform); + render_children( + doc, + svg_node, + &root_transform, + &root_attrs, + &mut canvas, + font, + ); let data = if ss > 1 { downsample_rgba(&canvas.data, cw, ch, ss) @@ -305,11 +315,12 @@ fn render_children( doc: &Document, parent: NodeId, transform: &Transform, + inherited: &PresentationAttrs, canvas: &mut Canvas, font: Option<&Font>, ) { for child in doc.children(parent) { - render_element(doc, child, transform, canvas, font); + render_element(doc, child, transform, inherited, canvas, font); } } @@ -317,6 +328,7 @@ fn render_element( doc: &Document, node: NodeId, parent_transform: &Transform, + inherited: &PresentationAttrs, canvas: &mut Canvas, font: Option<&Font>, ) { @@ -325,12 +337,12 @@ fn render_element( None => return, }; - let attrs = parse_presentation_attrs(doc, node); + let attrs = parse_presentation_attrs(doc, node, inherited); let transform = parent_transform.multiply(&attrs.transform); match tag.as_str() { "g" => { - render_children(doc, node, &transform, canvas, font); + render_children(doc, node, &transform, &attrs, canvas, font); } "rect" => render_rect(doc, node, &attrs, &transform, canvas), "circle" => render_circle(doc, node, &attrs, &transform, canvas), @@ -340,7 +352,7 @@ fn render_element( "text" => render_text(doc, node, &attrs, &transform, canvas, font), _ => { // Unknown SVG element — recurse into children anyway. - render_children(doc, node, &transform, canvas, font); + render_children(doc, node, &transform, &attrs, canvas, font); } } } @@ -1565,8 +1577,16 @@ impl Canvas { // --- Attribute parsing helpers --- -fn parse_presentation_attrs(doc: &Document, node: NodeId) -> PresentationAttrs { - let mut attrs = PresentationAttrs::default(); +fn parse_presentation_attrs( + doc: &Document, + node: NodeId, + inherited: &PresentationAttrs, +) -> PresentationAttrs { + let inherited_opacity = inherited.opacity; + let mut attrs = inherited.clone(); + // `transform` is not inherited. Ancestor transforms are already folded into + // the transform argument passed during traversal. + attrs.transform = Transform::identity(); if let Some(fill) = doc.get_attribute(node, "fill") { if fill == "none" { @@ -1612,7 +1632,7 @@ fn parse_presentation_attrs(doc: &Document, node: NodeId) -> PresentationAttrs { } if let Some(o) = doc.get_attribute(node, "opacity") { - attrs.opacity = parse_length(o).clamp(0.0, 1.0); + attrs.opacity = inherited_opacity * parse_length(o).clamp(0.0, 1.0); } if let Some(fo) = doc.get_attribute(node, "fill-opacity") { @@ -1970,6 +1990,50 @@ mod tests { assert_eq!(h, 100); } + #[test] + fn group_fill_is_inherited_by_child_paths() { + let mut doc = Document::new(); + let root = doc.root(); + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); + doc.set_attribute(svg, "width", "100"); + doc.set_attribute(svg, "height", "100"); + doc.append_child(root, svg); + + let group = doc.create_element_ns("g", Some("http://www.w3.org/2000/svg")); + doc.set_attribute(group, "fill", "#00ff00"); + doc.append_child(svg, group); + + let path = doc.create_element_ns("path", Some("http://www.w3.org/2000/svg")); + doc.set_attribute(path, "d", "M 10 10 L 90 10 L 90 90 L 10 90 Z"); + doc.append_child(group, path); + + let (_w, _h, data) = render_svg(&doc, svg, None).expect("render"); + let px = ((50 * 100 + 50) * 4) as usize; + assert_eq!(data[px], 0); + assert_eq!(data[px + 1], 255); + assert_eq!(data[px + 2], 0); + assert_eq!(data[px + 3], 255); + } + + #[test] + fn root_fill_none_suppresses_child_default_fill() { + let mut doc = Document::new(); + let root = doc.root(); + let svg = doc.create_element_ns("svg", Some("http://www.w3.org/2000/svg")); + doc.set_attribute(svg, "width", "100"); + doc.set_attribute(svg, "height", "100"); + doc.set_attribute(svg, "fill", "none"); + doc.append_child(root, svg); + + let path = doc.create_element_ns("path", Some("http://www.w3.org/2000/svg")); + doc.set_attribute(path, "d", "M 10 10 L 90 10 L 90 90 L 10 90 Z"); + doc.append_child(svg, path); + + let (_w, _h, data) = render_svg(&doc, svg, None).expect("render"); + let px = ((50 * 100 + 50) * 4) as usize; + assert_eq!(data[px + 3], 0); + } + #[test] fn render_path_triangle() { let mut doc = Document::new();