diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 7a9fcf42..bfbc6c0a 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -4461,6 +4461,333 @@ fn observe_blend_layer(canvas: &Canvas) -> crate::trace::blend_layers::Observati } } +/// Device-space source extents for the rectangular linear-ramp profile. +/// +/// A ramp's ordered dither is anchored to its raster device, not the final +/// canvas. Derive bounds from the actual draw commands and current view, never +/// the damage envelope. Recompute on execution: raw drawlists are mutable and +/// the host view is not part of the compiled product. Neutral lists never call +/// this pass. Websem patrols sources whose local-space extent is not retained +/// by the resolved stream. Other raw drawlist programs keep their old route. +fn blend_source_extents(list: &DrawList, view: &Affine) -> Option>> { + use skia_safe::RoundOut; + + fn linear(paints: &Paints) -> bool { + paints + .iter() + .any(|paint| matches!(paint, ModelPaint::LinearGradient(_))) + } + fn stroke_box(w: f32, h: f32, stroke: &Stroke, space: StrokeSpace) -> Option { + let StrokeWidth::Uniform(width) = stroke.width.normalized() else { + return None; + }; + if space != StrokeSpace::Local + || stroke.align != StrokeAlign::Center + || stroke.cap != StrokeCap::Butt + || stroke.join != StrokeJoin::Miter + || stroke.dash_array.is_some() + || !width.is_finite() + || width < 0.0 + { + return None; + } + let mut rect = Rect::from_wh(w, h); + rect.outset((width * 0.5, width * 0.5)); + Some(rect) + } + #[derive(Clone, Copy)] + enum Kind { + Blend(usize, rframe::ScopeBlendMode), + Opacity, + Clip, + } + struct Source { + kind: Kind, + bounds: Option, + known: bool, + ramp: bool, + } + impl Source { + fn add(&mut self, bounds: Option, known: bool, ramp: bool) { + self.known &= known; + self.ramp |= ramp; + if let Some(bounds) = bounds { + if let Some(accumulated) = &mut self.bounds { + accumulated.join(bounds); + } else { + self.bounds = Some(bounds); + } + } + } + } + if !list.items.iter().any(|item| match &item.kind { + ItemKind::RectFill { paints, .. } => linear(paints), + ItemKind::RectStroke { stroke, .. } => linear(&stroke.paints), + _ => false, + }) { + return None; + } + let mut output = vec![None; list.items.len()]; + let mut stack: Vec = Vec::new(); + for (index, item) in list.items.iter().enumerate() { + let matrix = skia_matrix(&view.then(&item.world)); + let mut begin = None; + let (bounds, ramp) = match &item.kind { + ItemKind::BeginIsolatedBlend { blend } => { + begin = Some(Kind::Blend(index, blend.mode())); + (None, false) + } + ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } => { + begin = Some(Kind::Opacity); + (None, false) + } + ItemKind::BeginClipRect { .. } | ItemKind::BeginClipPath { .. } => { + begin = Some(Kind::Clip); + (None, false) + } + ItemKind::EndIsolatedBlend | ItemKind::EndOpacity | ItemKind::EndClip => { + let source = stack.pop()?; + // Chromium accumulates drawable bounds, not clipped ink. + // The canvas clip still limits the allocation at execution. + if let Kind::Blend(start, mode) = source.kind { + if mode != rframe::ScopeBlendMode::Normal && source.known && source.ramp { + output[start] = source.bounds.map(|bounds| -> Rect { bounds.round_out() }); + } + } + if let Some(parent) = stack.last_mut() { + parent.add( + source.bounds, + source.known, + matches!(source.kind, Kind::Clip) && source.ramp, + ); + } + continue; + } + ItemKind::RectFill { + w, + h, + corner_radius, + paints, + .. + } if corner_radius.is_zero() => (Some(Rect::from_wh(*w, *h)), linear(paints)), + ItemKind::RectStroke { + w, + h, + corner_radius, + stroke, + space, + .. + } if corner_radius.is_zero() => { + (stroke_box(*w, *h, stroke, *space), linear(&stroke.paints)) + } + ItemKind::PatternFill { + geometry: ResolvedPatternGeometry::Rect { x, y, w, h }, + .. + } => (Some(Rect::from_xywh(*x, *y, *w, *h)), false), + // These sources need their own materialization profile. In + // particular, never turn an effect region into a geometry bound. + _ => { + if let Some(source) = stack.last_mut() { + source.known = false; + } + continue; + } + }; + if let Some(kind) = begin { + stack.push(Source { + kind, + bounds: None, + known: true, + ramp: false, + }); + } else if let Some(source) = stack.last_mut() { + let bounds = bounds + .map(|bounds| matrix.map_rect(bounds).0) + .filter(|bounds| bounds.is_finite()); + source.add(bounds, bounds.is_some(), ramp); + } + } + Some(output) +} + +#[cfg(test)] +mod blend_source_extent_tests { + use super::*; + use crate::drawlist::Item; + + fn item(kind: ItemKind) -> Item<()> { + Item { + node: (), + world: Affine::IDENTITY, + kind, + } + } + + fn ramp() -> Item<()> { + let mut draw = item(ItemKind::RectFill { + w: 38.2, + h: 28.4, + corner_radius: Default::default(), + corner_smoothing: Default::default(), + paints: Paints::new([ModelPaint::LinearGradient(LinearGradientPaint::default())]), + post_paint_opacity: PostPaintOpacity::IDENTITY, + }); + draw.world = Affine::translate(8.3, 12.7); + draw + } + + fn scene() -> DrawList<()> { + DrawList::from_items(vec![ + item(ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new(rframe::ScopeBlendMode::Multiply, None), + }), + ramp(), + item(ItemKind::EndIsolatedBlend), + ]) + } + + fn stroked_scene(width: StrokeWidth, mode: rframe::ScopeBlendMode) -> DrawList<()> { + let mut list = scene(); + list.items[0].kind = ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new(mode, None), + }; + list.items[1].kind = ItemKind::RectStroke { + w: 38.2, + h: 28.4, + corner_radius: Default::default(), + corner_smoothing: Default::default(), + stroke: Stroke { + paints: Paints::new([ModelPaint::LinearGradient(LinearGradientPaint { + stops: vec![ + GradientStop { + offset: 0.0, + color: n0_model::model::Color(0xFFCD_6843).into(), + }, + GradientStop { + offset: 1.0, + color: n0_model::model::Color(0x995B_ACE1).into(), + }, + ], + ..Default::default() + })]), + width, + align: StrokeAlign::Center, + cap: StrokeCap::Butt, + join: StrokeJoin::Miter, + miter_limit: 4.0, + dash_array: None, + }, + space: StrokeSpace::Local, + dash_phase: StrokeDashPhase::ZERO, + post_paint_opacity: PostPaintOpacity::IDENTITY, + }; + list + } + + #[test] + fn equal_sided_stroke_spellings_have_identical_extents_and_pixels() { + let raster = |list: &DrawList<()>| { + let mut surface = skia_safe::surfaces::raster_n32_premul((64, 64)).unwrap(); + surface.canvas().clear(Color::from_rgb(66, 101, 137)); + let saves = surface.canvas().save_count(); + execute_unchecked( + surface.canvas(), + list, + &Affine::IDENTITY, + &PaintCtx::new(None), + ); + assert_eq!(surface.canvas().save_count(), saves); + read_pixels(&mut surface, 64, 64) + }; + for mode in [ + rframe::ScopeBlendMode::Multiply, + rframe::ScopeBlendMode::Screen, + ] { + let uniform = stroked_scene(StrokeWidth::Uniform(3.5), mode); + let rectangular = stroked_scene( + StrokeWidth::Rectangular(RectangularStrokeWidth::all(3.5)), + mode, + ); + let expected = blend_source_extents(&uniform, &Affine::IDENTITY).unwrap(); + assert_eq!(expected[0], Some(Rect::new(6.0, 10.0, 49.0, 43.0))); + assert_eq!( + blend_source_extents(&rectangular, &Affine::IDENTITY).unwrap(), + expected + ); + // Representation equivalence, not a replacement Chromium oracle. + let pixels = raster(&uniform); + assert!(pixels.chunks_exact(4).any(|pixel| pixel != &pixels[..4])); + assert_eq!(raster(&rectangular), pixels); + } + } + + #[test] + fn unequal_and_zero_stroke_widths_do_not_gain_an_extent() { + for width in [ + StrokeWidth::Rectangular(RectangularStrokeWidth { + stroke_top_width: 3.5, + stroke_right_width: 4.0, + stroke_bottom_width: 3.5, + stroke_left_width: 3.5, + }), + StrokeWidth::Rectangular(RectangularStrokeWidth::all(0.0)), + StrokeWidth::Uniform(0.0), + ] { + let list = stroked_scene(width, rframe::ScopeBlendMode::Multiply); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + None + ); + } + } + + #[test] + fn actual_list_and_current_view_are_the_only_extent_inputs() { + let mut list = scene(); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + Some(Rect::new(8.0, 12.0, 47.0, 42.0)) + ); + assert_eq!( + blend_source_extents(&list, &Affine::translate(3.0, 2.0)).unwrap()[0], + Some(Rect::new(11.0, 14.0, 50.0, 44.0)) + ); + list.items[1].world = Affine::translate(2.0, 5.0); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + Some(Rect::new(2.0, 5.0, 41.0, 34.0)) + ); + if let ItemKind::RectFill { paints, .. } = &mut list.items[1].kind { + *paints = Paints::default(); + } + assert!(blend_source_extents(&list, &Affine::IDENTITY).is_none()); + } + + #[test] + fn unknown_sibling_does_not_disable_a_separate_known_source() { + let mut list = scene(); + list.items.insert( + 0, + item(ItemKind::OvalFill { + w: 48.0, + h: 48.0, + paints: Paints::default(), + post_paint_opacity: PostPaintOpacity::IDENTITY, + }), + ); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[1], + Some(Rect::new(8.0, 12.0, 47.0, 42.0)) + ); + let unknown = list.items.remove(0); + list.items.insert(2, unknown); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + None + ); + } +} + /// Replay a raw [`DrawList`] without a frame-environment check. /// /// This low-level entry exists for engine-owned resource-free glyphless @@ -4483,6 +4810,7 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, enum Scope { Opacity, Blend { + source_clip: bool, #[cfg(feature = "trace")] observed_bytes: u128, }, @@ -4497,7 +4825,8 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, let initial_save_count = canvas.save_count(); let mut scopes = Vec::new(); let mut glyph_scratch = GlyphScratch::default(); - for item in &list.items { + let mut blend_sources = None; + for (item_index, item) in list.items.iter().enumerate() { match &item.kind { ItemKind::BeginOpacity { opacity } => { // Copy the current backdrop into the group layer so descendant @@ -4549,8 +4878,16 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, rframe::ScopeBlendMode::Multiply => unreachable!(), }); } + let sources = blend_sources.get_or_insert_with(|| blend_source_extents(list, view)); + let bounds = sources.as_ref().and_then(|sources| sources[item_index]); + if let Some(bounds) = bounds { + canvas.save(); + canvas.reset_matrix(); + canvas.clip_rect(bounds, None, false); + } canvas.save_layer(&SaveLayerRec::default().paint(&restore_paint)); scopes.push(Scope::Blend { + source_clip: bounds.is_some(), #[cfg(feature = "trace")] observed_bytes: crate::trace::blend_layers::Execute::begin_layer( observe_blend_layer(canvas), @@ -4563,8 +4900,14 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, if scope.is_some() { canvas.restore(); } + if let Some(Scope::Blend { + source_clip: true, .. + }) = scope + { + canvas.restore(); + } #[cfg(feature = "trace")] - if let Some(Scope::Blend { observed_bytes }) = scope { + if let Some(Scope::Blend { observed_bytes, .. }) = scope { crate::trace::blend_layers::Execute::end_layer(observed_bytes); } } diff --git a/crates/n0/tests/group_blending.rs b/crates/n0/tests/group_blending.rs index f3d4f14a..34e1df66 100644 --- a/crates/n0/tests/group_blending.rs +++ b/crates/n0/tests/group_blending.rs @@ -75,17 +75,17 @@ fn frame(items: Vec) -> Frame { } fn raster(product: &FrameProduct, backdrop: CGColor) -> Vec { + raster_at(product, backdrop, &AffineTransform::identity()) +} + +fn raster_at(product: &FrameProduct, backdrop: CGColor, view: &AffineTransform) -> Vec { let mut surface = skia_safe::surfaces::raster_n32_premul((SIZE, SIZE)).unwrap(); surface.canvas().clear(skia_safe::Color::from_argb( backdrop.a, backdrop.r, backdrop.g, backdrop.b, )); let saves = surface.canvas().save_count(); product - .execute( - surface.canvas(), - &AffineTransform::identity(), - &PaintCtx::new(None), - ) + .execute(surface.canvas(), view, &PaintCtx::new(None)) .unwrap(); assert_eq!( surface.canvas().save_count(), @@ -100,11 +100,75 @@ fn at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] { pixels[offset..offset + 4].try_into().unwrap() } +fn linear_node(id: u64, bounds: Rectangle) -> FrameItem { + let gradient = cg::LinearGradientPaint::from_colors(vec![FIRST, SECOND]); + let mut source = node( + id, + bounds, + PaintStack::try_from_paints(cg::Paints::new([cg::Paint::LinearGradient(gradient)])) + .unwrap(), + ); + source.bounds = math2::rect_transform(bounds, &source.transform); + FrameItem::Node(source) +} + +#[test] +fn linear_source_extent_replay_at_changed_views_matches_fresh() { + for mode in [ScopeBlendMode::Multiply, ScopeBlendMode::Screen] { + let source = frame(vec![ + blend(10, mode, Some(0.6)), + linear_node(1, rect(8.3, 12.7, 28.2, 18.4)), + FrameItem::ScopeEnd, + ]); + let retained = compile(source.clone()).unwrap(); + let original = raster(&retained, BACKDROP); + for view in [ + AffineTransform::new(3.0, 2.0, 0.0), + AffineTransform::from_acebdf(0.5, 0.0, 5.0, 0.0, 0.5, 3.0), + AffineTransform::identity(), + ] { + let fresh = compile(source.clone()).unwrap(); + assert_eq!( + raster_at(&retained, BACKDROP, &view), + raster_at(&fresh, BACKDROP, &view) + ); + assert_eq!( + raster(&retained, BACKDROP), + original, + "a different view must not leave a cached raster origin" + ); + } + } +} + #[cfg(feature = "trace")] mod layer_metrics { use super::*; use n0::trace::{sink::drain_blend_layers, BlendLayerMetrics}; + #[test] + fn linear_source_layer_observes_rounded_draw_bounds_not_the_frame_envelope() { + drain_blend_layers(); + let product = compile(frame(vec![ + blend(10, ScopeBlendMode::Multiply, None), + linear_node(1, rect(8.3, 12.7, 28.2, 18.4)), + FrameItem::ScopeEnd, + ])) + .unwrap(); + raster(&product, BACKDROP); + let metrics = drain_blend_layers(); + assert_eq!(metrics.len(), 1); + assert_eq!(metrics[0].observed_raster_bytes, 29 * 20 * 4); + assert_eq!(metrics[0].save_layer_calls, 1); + raster_at( + &product, + BACKDROP, + &AffineTransform::from_acebdf(0.5, 0.0, 5.0, 0.0, 0.5, 3.0), + ); + let metrics = drain_blend_layers(); + assert_eq!(metrics[0].observed_raster_bytes, 15 * 10 * 4); + } + fn single() -> FrameProduct { compile(frame(vec![ blend(10, ScopeBlendMode::Multiply, Some(0.5)), diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 958c183b..5bc81783 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -607,10 +607,10 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 34 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete primitive corpus contains 1,398 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,423 Chromium-baked cells plus 16 sampled frames; the text estate contains sixteen exact text pixel cells and eight exact-number artifact-geometry witnesses (six Allerta and two - Bungee), and the named refusal register has 303 rows. `feFlood`, `feComposite`, + Bungee), and the named refusal register has 325 rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, `feDiffuseLighting`, `feDistantLight`, `fePointLight`, `feSpotLight`, @@ -858,7 +858,21 @@ isolate` have a bounded static SVG group profile. One Stylo computed value the resolved stream, independently of the caller's canvas clear color. The current group-source profile is sharp-cornered rectangles, including solid, linear-gradient and admitted repeating-pattern paints, fractional - placement, 2D transforms and simple local butt/miter strokes. Non-rectangular + placement and simple local butt/miter strokes. Linear-gradient sources have + a narrower source-extent profile: untransformed rectangular draws, including + simple strokes, solid/pattern-filled siblings and own Multiply/Screen group + opacity. The temporary raster origin follows their outward-rounded + drawable bounds. Mapped contributors, nested source scopes/opacity, + non-painted geometry contributors, omitted transparent/unresolved/context stroke + extents (even beside a live fill), and patterned strokes in a source that + also paints a linear ramp retain the named `linear-gradient source-extent` + refusal. This conservatively includes otherwise harmless combinations; + a completed child blend image is not a bare ramp in its parent. This + profile still admits `stroke:none`, resolved zero-width strokes and retained + all-transparent gradient strokes whose geometry remains in the frame. The same + patrol covers a required root boundary, including root opacity around a + mixed ramp/blend source, where both admissions refuse. + Solid-only 2D transforms retain their existing admission. Non-rectangular source geometry, radial source paints, wider strokes and curved, subpixel or rotated clip coverage retain the named `group-source precision` refusal. These guards deliberately over-refuse unproved combinations. Eliding a diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index 0b95784e..43c4213b 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -3423,6 +3423,11 @@ fn compile_svg_element( if adds_root_blend_boundary && let Some(reason) = root_facts.blend_precision_boundary { return Err(blend_precision_refusal(reason)); } + let has_root_blend_source = adds_root_blend_boundary + || (root_facts.has_blend && root_patrol.opacity > 0.0 && root_patrol.opacity < 1.0); + if has_root_blend_source && let Some(reason) = blend_linear_source_boundary(root_facts) { + return Err(blend_linear_source_refusal(reason)); + } walk.compact_elided_blends(); let ChildWalk { mut items, @@ -3532,6 +3537,11 @@ struct SpanFacts { /// Classify source material once; adding an isolation/blend later must not /// silently switch its raster origin or its coverage/alpha materialization. blend_precision_boundary: Option<&'static str>, + /// A linear ramp still painted into this source, not an already completed + /// blend image. Its dither origin needs an exact source-space extent. + has_linear_source: bool, + /// Contributor facts that cannot be recovered from the resolved drawlist. + linear_source_boundary: Option<&'static str>, } impl SpanFacts { @@ -3548,7 +3558,25 @@ impl SpanFacts { self.blend_precision_boundary = self .blend_precision_boundary .or(other.blend_precision_boundary); + self.has_linear_source |= other.has_linear_source; + self.linear_source_boundary = self.linear_source_boundary.or(other.linear_source_boundary); + } +} + +fn blend_linear_source_boundary(facts: SpanFacts) -> Option<&'static str> { + if !facts.has_linear_source { + return None; } + facts.linear_source_boundary.or_else(|| { + (facts.transformed || facts.has_scope || facts.has_opacity) + .then_some("with a transformed or nested linear-gradient source") + }) +} + +fn blend_linear_source_refusal(reason: &str) -> CompileError { + CompileError::UnsupportedStyle(format!( + "mix-blend-mode/isolation {reason} needs the linear-gradient source-extent profile" + )) } fn blend_precision_refusal(reason: &str) -> CompileError { @@ -5617,6 +5645,9 @@ impl<'a> ChildWalk<'a> { if (composite.is_some() || facts.has_blend) && let Some(reason) = facts.blend_precision_boundary { return Err(blend_precision_refusal(reason)); } + if (composite.is_some() || facts.has_blend) && let Some(reason) = blend_linear_source_boundary(facts) { + return Err(blend_linear_source_refusal(reason)); + } if let Some(composite) = composite { if facts.has_image_effect { return Err(CompileError::UnsupportedStyle( @@ -5640,6 +5671,10 @@ impl<'a> ChildWalk<'a> { facts.has_blend = true; facts.has_opacity |= composite.opacity().is_some(); facts.escaping_blend = composite.mode() != ScopeBlendMode::Normal; + // The parent composites a completed image. Do not confuse + // it with a ramp rasterized directly into the parent. + facts.has_linear_source = false; + facts.linear_source_boundary = None; } } Ok(facts) @@ -6494,6 +6529,31 @@ impl<'a> ChildWalk<'a> { facts.transformed = outcome.transformed; facts.blend_precision_boundary = outcome.nodes.iter().find_map(blend_node_precision_boundary); + facts.has_linear_source = outcome.nodes.iter().any(|node| { + node.paints + .iter() + .chain(node.stroke.iter().flat_map(|stroke| stroke.paints().iter())) + .any(|paint| matches!(paint, cg::Paint::LinearGradient(_))) + }); + facts.linear_source_boundary = if outcome.omitted_stroke_extent { + Some("with a non-painted stroke extent") + } else if outcome.has_geometry && outcome.draws == 0 { + Some("with a non-painted source contributor") + } else { + outcome.nodes.iter().find_map(|node| { + if node.transform != AffineTransform::identity() { + Some("with a mapped source contributor") + } else if node + .stroke + .as_ref() + .is_some_and(|stroke| stroke.paints().pattern().is_some()) + { + Some("with a patterned source stroke") + } else { + None + } + }) + }; } let marker_facts = match self.compile_marker_instances( el, @@ -11608,6 +11668,7 @@ fn compile_tspan_text( has_opacity: true, has_geometry: true, transformed: false, + omitted_stroke_extent: false, })); } let one_pass_fold = (replay_opacity < 1.0 && paths.len() == 1).then_some(replay_opacity); @@ -11654,6 +11715,7 @@ fn compile_tspan_text( has_opacity: replay_opacity < 1.0, has_geometry: true, transformed: false, + omitted_stroke_extent: false, })) } @@ -12343,6 +12405,9 @@ struct ShapeOutcome { draws: usize, /// Structural paint passes Chromium's element-opacity fold observes. opacity_passes: usize, + /// A selected stroke can enlarge Chromium's drawable bounds without a + /// paint pass. Keep this separate from opacity-fold participation. + omitted_stroke_extent: bool, /// The shape's own opacity composites fill and stroke through one /// isolated layer — the walk wraps the node in a scope. scope_opacity: Option, @@ -12706,6 +12771,7 @@ fn shape_node( }; let mut stroke = resolved_stroke.stroke; let stroke_opacity_pass = resolved_stroke.opacity_pass; + let omitted_stroke_extent = resolved_stroke.omitted_extent; patrol_mixed_contour_cap(&geometry, stroke.as_ref())?; debug_assert!( @@ -12743,6 +12809,7 @@ fn shape_node( has_opacity: true, has_geometry: true, transformed: false, + omitted_stroke_extent, }); } if opacity < 1.0 && has_geometry { @@ -12813,6 +12880,7 @@ fn shape_node( nodes, draws, opacity_passes, + omitted_stroke_extent, scope_opacity, has_opacity, has_geometry, @@ -14054,6 +14122,10 @@ fn resolve_stroke_width( struct StrokeResolution { stroke: Option, opacity_pass: bool, + /// A non-none selected stroke may still enlarge SVG drawable bounds when + /// its paint is transparent or its server is unresolved. The exact extent + /// is absent from FrameNode; source-origin-sensitive groups must patrol it. + omitted_extent: bool, } /// Blink's `markerUnits="strokeWidth"` scale for a non-scaling-stroke client. @@ -14179,6 +14251,7 @@ impl StrokeResolution { Self { stroke: None, opacity_pass: false, + omitted_extent: false, } } } @@ -14198,6 +14271,10 @@ fn resolve_stroke( ) -> Result { let data = el.borrow_data().ok_or(CompileError::MissingComputedStyle)?; let style: &ComputedValues = data.styles.primary(); + // Context paint that resolves to no paint is still not computed `none` + // for Chromium's stroke bounding box. Preserve that distinction before + // following the context relation (which may also have no provider). + let computed_stroke_is_none = matches!(style.clone_stroke().kind, SVGPaintKind::None); // Direct colours, valid paint servers, and invalid-reference fallbacks // stage element opacity exactly as [`resolve_fill`] describes. @@ -14214,7 +14291,11 @@ fn resolve_stroke( let Some(selected) = select_paint(el, PaintProperty::Stroke, paint_contexts) .map_err(CompileError::UnsupportedStroke)? else { - return Ok(StrokeResolution::none()); + return Ok(StrokeResolution { + stroke: None, + opacity_pass: false, + omitted_extent: !computed_stroke_is_none, + }); }; let owner_data = selected .owner @@ -14232,7 +14313,13 @@ fn resolve_stroke( _ => Ok(PaintResolution::none()), }; let paint = match paint.kind { - SVGPaintKind::None => return Ok(StrokeResolution::none()), + SVGPaintKind::None => { + return Ok(StrokeResolution { + stroke: None, + opacity_pass: false, + omitted_extent: !computed_stroke_is_none, + }); + } SVGPaintKind::Color(ref color) => { admitted_srgb(owner_style.resolve_color(color), solid_opacity) .map(PaintStack::solid) @@ -14268,7 +14355,15 @@ fn resolve_stroke( } }; if !paint.opacity_pass { - return Ok(StrokeResolution::none()); + // Paint-server failure is not computed `stroke:none`. Chromium's + // visual rectangle can retain the selected stroke's extent even when + // no drawing/opacity pass survives. Conservatively retain that risk; + // do not resolve otherwise-inert width grammar solely for this patrol. + return Ok(StrokeResolution { + stroke: None, + opacity_pass: false, + omitted_extent: true, + }); } // A valid transparent paint still records the stroke pass Chromium's @@ -14281,6 +14376,7 @@ fn resolve_stroke( return Ok(StrokeResolution { stroke: None, opacity_pass: true, + omitted_extent: true, }); } @@ -14441,6 +14537,7 @@ fn resolve_stroke( Ok(StrokeResolution { stroke, opacity_pass: true, + omitted_extent: false, }) } diff --git a/crates/websem/tests/svg_blending.rs b/crates/websem/tests/svg_blending.rs index 99619b4e..8dfb84dc 100644 --- a/crates/websem/tests/svg_blending.rs +++ b/crates/websem/tests/svg_blending.rs @@ -23,6 +23,134 @@ fn blends(frame: &Frame) -> Vec { .collect() } const RECT: &str = r#""#; +const RAMP: &str = ""; +const RAMP_RECT: &str = ""; + +#[test] +fn linear_source_extent_patrol_is_transactional_and_names_the_owner() { + for content in [ + format!("{RAMP_RECT}"), + format!("{RAMP_RECT}{RECT}"), + format!("{RAMP_RECT}{RECT}"), + format!("{RAMP_RECT}{RECT}"), + format!("{RAMP_RECT}"), + format!("{RAMP_RECT}"), + format!("{RAMP_RECT}{RECT}"), + format!("{RAMP_RECT}"), + RAMP_RECT.replace("/>", " stroke='transparent' stroke-width='4'/>"), + RAMP_RECT.replace("/>", " stroke='red' stroke-opacity='0' stroke-width='4'/>"), + format!( + "{}", + RAMP_RECT.replace("/>", " stroke='url(#empty)' stroke-width='4'/>") + ), + format!( + "{RAMP_RECT}" + ), + RAMP_RECT.replace("/>", " stroke='url(#missing)' stroke-width='4'/>"), + RAMP_RECT.replace("/>", " stroke='context-stroke' stroke-width='4'/>"), + format!( + "{}", + RAMP_RECT.replace("/>", " id='ctx' stroke='context-fill' stroke-width='4'/>") + ), + RAMP_RECT.replace("/>", " stroke='url(#missing) none' stroke-width='4'/>"), + format!( + "{}", + RAMP_RECT.replace("/>", " stroke='url(#wrong)' stroke-width='4'/>") + ), + ] { + for style in [ + "mix-blend-mode:multiply", + "mix-blend-mode:screen", + "isolation:isolate", + ] { + let source = svg(&format!("{RAMP}{content}{RECT}")); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!( + strict.contains("linear-gradient source-extent"), + "{strict}: {source}" + ); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!( + best.base_frame().items.len(), + 1, + "the entire failed group is rolled back: {source}" + ); + assert!( + best.degradations().iter().any(|d| d.path() == "svg/g[1]" + && d.reason().contains("linear-gradient source-extent")), + "{:?}", + best.degradations() + ); + } + } +} + +#[test] +fn root_linear_source_extent_refusal_cannot_silently_fall_back() { + let base = svg(&format!( + "{RAMP}{RAMP_RECT}{RECT}" + )); + for opacity in ["1", ".5", ".999"] { + let source = base.replace("width=\"64\"", &format!("opacity='{opacity}' width=\"64\"")); + for result in [ + SvgFrameSource::from_standalone_svg(source.as_str(), InitialViewport::new(64.0, 64.0)), + SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ), + ] { + assert!( + result + .unwrap_err() + .to_string() + .contains("linear-gradient source-extent") + ); + } + } +} + +#[test] +fn completed_linear_blend_images_do_not_poison_outer_solid_groups() { + for content in [ + RAMP_RECT.to_string(), + format!("{RAMP_RECT}{RECT}"), + format!("{RAMP_RECT}"), + ] { + frame(&format!( + "{RAMP}{content}" + )); + } + // The new patrol is source-specific; ordinary transformed solids remain + // admitted, and an ordinary ramp without blending keeps its old route. + frame(&format!( + "{RAMP}{}", + RAMP_RECT.replace( + "fill='url(#r)'", + "fill='transparent' stroke='url(#r)' stroke-width='4'" + ) + )); + for attrs in [ + "stroke='none' stroke-width='4'", + "stroke='transparent' stroke-width='0'", + ] { + frame(&format!( + "{RAMP}{}", + RAMP_RECT.replace("/>", &format!(" {attrs}/>")) + )); + } + frame(&format!( + "{RECT}" + )); + frame(&format!( + "{RAMP}{RAMP_RECT}" + )); +} #[test] fn neutral_groups_have_no_scope_and_raw_attribute_lookalikes_are_inert() { diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index 8a51d0a1..78cb67e7 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -46,6 +46,116 @@ use Departure::{BothRefuse, DeclaredByBestEffort}; /// construct itself — a refusal that stopped naming what it refused would pass /// a bare "does it error" check and fail this one. const CORPUS: &[(&str, Departure, &str)] = &[ + ( + "svg-group-blend-linear-extent-stroke-context-missing", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-context-none", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-transparent", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-opacity-zero", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-empty-gradient", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-missing-reference", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-none-fallback", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-stroke-sibling", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-root-opacity", + BothRefuse, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-transform", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-rotation", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-viewport", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-clip", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-child-opacity", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-zero-opacity", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-transparent", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-fill-zero", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-empty-gradient", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-pattern-stroke", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-nested-blend", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-isolation", + DeclaredByBestEffort, + "linear-gradient source-extent", + ), + ( + "svg-group-blend-linear-extent-root", + BothRefuse, + "linear-gradient source-extent", + ), ( "svg-group-blend-root-filter-sibling", BothRefuse, @@ -1525,6 +1635,19 @@ fn every_unsupported_fixture_departs_by_name_in_both_admissions() { declared.iter().all(|d| !d.path().is_empty()), "{id}: every declaration carries a structural path" ); + if id.starts_with("svg-group-blend-linear-extent-") { + let path = if id.ends_with("-viewport") { + "svg/svg[1]/g[1]" + } else { + "svg/g[1]" + }; + assert!( + declared + .iter() + .any(|d| d.path() == path && d.reason().contains(named)), + "{id}: source-extent refusal must stay at {path}: {declared:?}" + ); + } } } } diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index dd6ef469..ad6c6e33 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -60,7 +60,8 @@ from the dated addenda below: `` and `` containers, visibility, isolated element/group/root opacity, and HTML-ancestor opacity around the selected inline SVG; bounded static SVG group blending/isolation through a combined resolved blend/opacity scope - ([B1](#b1-svg-group-blending)); the whole + ([B1](#b1-svg-group-blending), with the + [B2a source-extent correction](#b2a-linear-gradient-blend-source-extents)); the whole `transform` grammar in both spellings (the attribute is a presentation hint of the CSS `transform` property, and `gradientTransform` is that attribute on gradient elements); @@ -116,7 +117,7 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,398 Chromium-baked primitive cells plus 16 sampled frames, +- **The corpus** is 1,423 Chromium-baked primitive cells plus 16 sampled frames, with a separate sixteen-cell exact text suite whose current cells select hash-pinned Ahem and Ahem-derived bytes from explicit family/face environments, and eight exact-number artifact-geometry @@ -127,7 +128,7 @@ from the dated addenda below: carrying declared one-code-value ramp-quantization bounds. The measured per-cell counts and causes are listed in the [corpus record](../../../fixtures/web-first/README.md). - The named refusal register has 303 rows. + The named refusal register has 325 rows. - **Not claimed:** no conformance score exists or may be computed — FLIP is unratified. The FLIP record and identity-changing review are prepared, but only the owner act on gridaco/nothing#49 may authorize them and the first @@ -6113,3 +6114,147 @@ repeated active-clip-sized source materialization with explicit byte arithmetic over those layers, not a larger semantic tree. Source extent/precision must be proved before tightening bounds or pooling layers; B1 records that cost and leaves the optimization unshipped. + +## B2a: linear-gradient blend source extents + +B2a repairs a silent pixel defect found while preparing B1's bounds work. +It is correctness work, not general layer-bounds optimization or a completed +blending grammar. The [admitted-slice record](../../../crates/n0_cli/README.md) +owns the resulting profile; all three CSS compositing rows remain unchecked. +The audience for this evidence is a maintainer extending that profile. + +### Cause and discriminators + +A rectangle at `(8.3,12.7)`, size `(38.2,28.4)`, uses an object-box ramp from +`#cd6843` to `#5bace1` with last-stop opacity `.6`, over `#426589`. +Both actual CLI admissions at the unmodified B1 baseline silently differed +from pinned Chromium by 28 pixels for Multiply and 56 for Screen, maximum +channel delta 1. The corresponding `svg-group-blend-extent-{multiply,screen}-fractional` +cells are now exact. Integer placement `(8,12)` also exposed the problem: +50/84 pixels at delta 1, now guarded by the `-integer` pair. Fractional +coordinates were not the cause. The normal and opaque-ramp cells separate +the blend source from ordinary gradient rendering and stop translucency. + +The etiology is the temporary raster's origin. Pinned Skia's gradient paint +enables ordered dithering, whose 8×8 phase uses device coordinates. B1's +unbounded source layer retained the active clip's origin instead of the +drawable source origin; its old `(8,8)` gradient cells happened to share the +same phase. In a separate backend diagnostic, a hard clip with origin `(8,12)` +made the original Multiply witness exact, whether its far edge was tight or +extended to the viewport. Changing only the layer size while keeping origin +`(0,0)` retained all 28 differing pixels **(measured, not celled)**. This +diagnostic isolated the mechanism; Chromium remained the pixel oracle. + +The upstream source explains the wider boundary: +[SVGDrawingRecorder](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/paint/svg_model_object_painter.h) +encloses a shape's visual rectangle in local SVG coordinates, and +[paint-chunk conversion](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc) +maps drawable bounds into each effect's transform space. These are upstream +source observations, not instrumentation of the pinned browser. The pinned +capture measurements establish the actual witness verdicts. + +### Correction and guarded remainder + +The n0 executor derives source bounds from the actual rectangular draw +commands and current view, including simple stroke outsets and pattern-fill +contributors. It rounds outward and applies an inert hard clip before the +existing exact blend restore. It does not use the damage envelope, change +restore arithmetic, add a raster cache or add a field to `rframe`. The pass +is lazy: lists without a blend command never call it; lists without a linear +rectangle paint allocate no extent table. A completed child blend contributes +an image, not a newly rasterized ramp. Execution tests check balanced saves, +current-view replay, fresh/retained identity and mutable raw-list recomputation. + +The 58-source reduced matrix first proved the simple correction. A separate +72-source composition matrix and 16 boundary controls then found the limits: +transformed sources, nested image groups, zero-opacity/transparent/empty-paint +contributors and patterned strokes can require information absent from the +resolved draw stream. The original B1 source was restored byte-for-byte before +the broad baseline rerender; both admissions were run through the actual CLI, +not just a successful compiler or a backend-only renderer. These wider matrix +facts are **(measured, not celled)** except for the specific cells below. + +Websem now names the `linear-gradient source-extent` boundary before it can +silently reach the old route. The patrol is deliberately conservative: even +an exact root-bare-ramp control, a harmless non-painted contributor, or a +particular exact transformed/clip case does not prove the full source-space +profile. Fourteen initial registered refusals cover mapped/rotated/viewport sources, +child clip and opacity, zero element/fill opacity, transparent/empty-gradient +contributors, pattern stroke, mixed nested blend, explicit isolation and the +required root boundary with unit or partial opacity. Four root-opacity controls +at `.5`/`.999` were exact before the conservative guard, not newly found pixel +defects **(measured, not celled)**. Attributable cases roll back the whole affected group +and keep named siblings; the root cases refuse in both admissions. Completed +blend images remain distinct from bare ramps when ancestor facts are combined. + +Twenty-five new exact cells cover the off-phase integer/fractional and opaque +ramps, normal control, fill/stroke routes, combined/outer opacity, sibling +bounds and order, pattern-fill siblings, gradient direction/transform, leaf +ownership, an unpositioned local use instance, viewport-edge clipping and the +four omitted-stroke boundary controls described below. +They use the unchanged hash-pinned capture module through the common probe +harness and baker. No existing oracle, tolerance or FLIP record changes. +The primitive corpus moves from 1,398 to 1,423 and the named refusal register +from 303 to 325; the sixteen sampled frames, sixteen text-pixel cells and eight +text-geometry witnesses are unchanged. + +Independent TICK/LAW review found a missing contributor the initial patrol +missed: a selected transparent stroke can enlarge the source bounds while its +gradient fill remains visible. Sixteen follow-up source controls confirmed +transparent, zero-opacity and empty-gradient strokes differ at 30 pixels for +Multiply and 102 for Screen, delta 1. Twelve adjacent controls confirmed +unresolved/wrong-kind references and explicit `none` fallbacks expose the same +class, even without an opacity pass. A solid sibling with a dropped stroke +changes 89/124 pixels at delta 1 **(measured, not celled)**. + +`StrokeResolution` therefore carries a producer-private omitted-extent fact +separate from opacity participation. It survives the visible fill and combines +with other source contributors before the named guard; no fake paint or +backend hint enters `rframe`. Six additional refusal witnesses cover these +branches. Four exact cells distinguish `stroke:none`, resolved zero width, +a retained all-transparent gradient stroke and transparent fill surrounding +a live gradient stroke. An unresolved paint server with otherwise inert width +grammar is conservatively guarded without newly evaluating that grammar. + +Eight final context-paint controls found the same 30/102-pixel class when a +context stroke has no provider or follows a provider selecting `none` +**(measured, not celled)**. Two more registered refusals guard those early +returns. One final best-effort render crossed the guard update and is excluded +from pre-guard pixel evidence; its strict witness and both final guarded +admissions were independently checked. The original computed stroke kind is +preserved before context +selection: resolving a context paint to nothing must not relabel it as +computed `stroke:none` for the extent decision. This remains a compositing +gap, covered by the unclosed blending rows; ordinary paint selection outside +that composition is unchanged. + +Gate sensitivity is measured, not inferred from a green run. Disabling only +the source-extent clip makes `just gate` fail on twenty-three new cells, including +the original fractional pair at 28/56 pixels and the integer pair at 50/84, +all at delta 1. The normal and offscreen controls remain exact. Restoring the +exact source hash returns the primitive gate to green. Separately disabling +the omitted-stroke extent patrol makes the refusal gate fail on +`stroke-context-missing`; an actual best-effort CLI render of the context-fill +source becomes silently wrong at 102 pixels, delta 1. Exact source restoration +reinstates the named refusal. No oracle changes are involved in either direction. + +The full nine-crate affected-path tests, full n0 trace suite, trace-enabled +Chromium gate, formatting, strict no-dependency Clippy, fixture/status gates +and link/OSS audits pass locally. No Workflow runner was exposed, so the saved +verification workflow's independent TICK/LAW and REPRO roles were reproduced +manually. Both pass after the omitted-stroke finding and final sensitivity +checks; the code hashes were restored exactly before the final green gates. + +Hosted review also caught a raw-drawlist representation mismatch: the painter +normalizes equal-sided rectangular stroke widths, while the extent helper +initially matched only the scalar spelling. The helper now uses the same +normalization. Consumer tests fail before that correction and prove identical +rounded extents, exact pixels and balanced saves for both spellings in Multiply +and Screen; unequal sides and zero-width raw strokes gain no extent. This is +representation-equivalence evidence, not additional Chromium cell coverage. + +The follow-on is a source-coordinate-space contract that can carry the missing +extent facts, before wider geometry or blend modes. This correction does not +resolve the separate ordinary-opacity findings in gridaco/nothing#136 or +the generic damage/coverage follow-ups in gridaco/nothing#87/#88. No timing +improvement is claimed. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 0b27ae41..9d39392e 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -688,6 +688,9 @@ excluded. > boundaries, and the [B1 evidence](./svg-engine-of-record.md#b1-svg-group-blending) > records the measured split. Partial coverage is not a tick under > gridaco/nothing#81/#89/#90. +> The [B2a source-extent correction](./svg-engine-of-record.md#b2a-linear-gradient-blend-source-extents) +> adds exact off-phase gradient controls and narrows unproved source +> combinations by name. It closes a silent-pixel defect, not these rows. ### CSS fonts diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 60a055eb..7b0404ac 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -19,8 +19,8 @@ Every root primitive here is a closed enumeration in `primitives.json` with a committed Chromium oracle beside it. Text follows the ratified corpus-growth law in its own closed [text estate](./text/README.md): sixteen exact text cells and eight exact-number real-font artifact-geometry witnesses. The current evidence -estate is 1,398 primitive cells plus 16 sampled frames, those twenty-four text -witnesses, and 303 named refusal rows. Pixel cells use byte equality: what each +estate is 1,423 primitive cells plus 16 sampled frames, those twenty-four text +witnesses, and 325 named refusal rows. Pixel cells use byte equality: what each corpus admits is exactly what the engine renders pixel-for-pixel, except only the primitive rows carrying an explicit measured tolerance block. The real-font witness grades geometry before rasterization and makes no Chromium @@ -28,6 +28,9 @@ pixel claim. | File | Role | | --- | --- | +| `svg-group-blend-extent-multiply-stroke-{none,clear-gradient}.svg` · `svg-group-blend-extent-screen-stroke-{zero-width,transparent-fill}.svg` | Four exact controls for the review-discovered omitted-stroke extent boundary. A retained all-transparent gradient stroke still supplies its geometry; `none` and zero width create no stroke extent; transparent fill does not lose the surrounding live gradient stroke's extent. Dropped transparent/unresolved stroke paints are separately refused rather than confused with these branches. | +| `svg-group-blend-extent-{multiply,screen}-{fractional,integer,opaque}.svg` · `svg-group-blend-extent-normal-fractional.svg` | B2a source-origin controls. B1 silently differed at 28/56 pixels for the fractional translucent pair and 50/84 for its integer-position pair, all at delta 1; the new cells are exact. The old gradient origin `(8,8)` hid the device-dither phase error. Normal and opaque-ramp controls separate source materialization from stop translucency. | +| `svg-group-blend-extent-*.svg` (remaining fourteen cells) | Exact simple stroke/fill, own and outer opacity, sibling union/order, repeating-pattern sibling, gradient direction/transform, leaf blend, local use and offscreen-source controls. The [B2a evidence](../../docs/wg/consolidation/svg-engine-of-record.md#b2a-linear-gradient-blend-source-extents) records the bounded correction and its conservative named refusals. No general source-space or timing claim. | | `svg-group-blend-{normal,multiply,screen}-{leaf,group,opacity,alpha,transparent,stroke}.svg` · `svg-group-blend-{multiply,screen}-each.svg` | B1's exact group-operation controls: mode-sensitive backdrops, overlapping children, combined opacity, translucency, transparent initial source and fill/stroke composition. Whole-group versus per-child blending changes 576 pixels at maximum deltas 89 (multiply) and 98 (screen). No new tolerance. | | `svg-group-blend-{multiply,screen}-opacity-small-{opaque,partial}.svg` | Opacity .123456 with opaque/translucent source colors over a translucent destination. The multiply partial-source cell rejects a float-first opacity prototype at 1,600 pixels/delta 1: byte opacity must be applied before the byte-domain blend. Hosted x86 also guards the low-precision backend's approximate division; no oracle or tolerance is relaxed. | | `svg-group-blend-near-unit-{isolated,plain,screen}-{unit,near,p999,p998}.svg` | Rotated groups at opacity 1, the next smaller f32, .999 and .998. The first three match for isolated blending children and screen; plain isolation instead changes 143 pixels/delta 1 at the unit-to-partial boundary. A byte-255 restore must not erase the authored partial-opacity source layer. The .998 controls discriminate the next opacity byte. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index d29600ba..0368ec65 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (1398) +## Chromium-baked cells (1423) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -613,6 +613,31 @@ to its fixture source. No new image is committed for this view. svg-group-blend-css-keyword svg-group-blend-css-var svg-group-blend-css-winner +svg-group-blend-extent-multiply-diagonal +svg-group-blend-extent-multiply-fractional +svg-group-blend-extent-multiply-integer +svg-group-blend-extent-multiply-leaf +svg-group-blend-extent-multiply-many +svg-group-blend-extent-multiply-opacity-half +svg-group-blend-extent-multiply-opaque +svg-group-blend-extent-multiply-outer-opacity +svg-group-blend-extent-multiply-stroke +svg-group-blend-extent-multiply-stroke-clear-gradient +svg-group-blend-extent-multiply-stroke-none +svg-group-blend-extent-normal-fractional +svg-group-blend-extent-screen-fill-stroke-gradient +svg-group-blend-extent-screen-fractional +svg-group-blend-extent-screen-gradient-transform +svg-group-blend-extent-screen-integer +svg-group-blend-extent-screen-many-reversed +svg-group-blend-extent-screen-offscreen +svg-group-blend-extent-screen-opacity-near +svg-group-blend-extent-screen-opaque +svg-group-blend-extent-screen-pattern-sibling +svg-group-blend-extent-screen-stroke-gradient +svg-group-blend-extent-screen-stroke-transparent-fill +svg-group-blend-extent-screen-stroke-zero-width +svg-group-blend-extent-screen-use svg-group-blend-fractional-multiply svg-group-blend-fractional-normal svg-group-blend-fractional-screen @@ -1426,7 +1451,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (303) +## The refusal register (325) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1544,6 +1569,28 @@ its row into the cells above. | `svg-group-blend-elided-mask-partial` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | | `svg-group-blend-elided-mask-unit` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | | `svg-group-blend-filter` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | +| `svg-group-blend-linear-extent-child-opacity` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-clip` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-empty-gradient` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-fill-zero` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-isolation` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-nested-blend` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-pattern-stroke` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a patterned source stroke needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-root` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-root-opacity` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with a transformed or nested linear-gradient source needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-rotation` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a mapped source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-context-missing` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-context-none` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-empty-gradient` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-missing-reference` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-none-fallback` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-opacity-zero` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-sibling` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-stroke-transparent` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted stroke extent needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-transform` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a mapped source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-transparent` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-viewport` | declared | skipped svg/svg[1]/g[1]: unsupported computed style: mix-blend-mode/isolation with a mapped source contributor needs the linear-gradient source-extent profile | +| `svg-group-blend-linear-extent-zero-opacity` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a non-painted source contributor needs the linear-gradient source-extent profile | | `svg-group-blend-mask` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | | `svg-group-blend-mode-color` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode Color is outside the admitted normal/multiply/screen group profile | | `svg-group-blend-mode-color-burn` | declared | skipped svg/rect[2]: unsupported computed style: mix-blend-mode ColorBurn is outside the admitted normal/multiply/screen group profile | diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-diagonal.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-diagonal.png new file mode 100644 index 00000000..483accbb Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-diagonal.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-fractional.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-fractional.png new file mode 100644 index 00000000..f7b68cc7 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-integer.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-integer.png new file mode 100644 index 00000000..40b5596e Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-integer.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-leaf.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-leaf.png new file mode 100644 index 00000000..f7b68cc7 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-leaf.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-many.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-many.png new file mode 100644 index 00000000..f579e737 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-many.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opacity-half.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opacity-half.png new file mode 100644 index 00000000..13b23d1e Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opacity-half.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opaque.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opaque.png new file mode 100644 index 00000000..57475e86 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-opaque.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-outer-opacity.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-outer-opacity.png new file mode 100644 index 00000000..a794782a Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-outer-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-clear-gradient.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-clear-gradient.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-clear-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-none.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-none.png new file mode 100644 index 00000000..f7b68cc7 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke-none.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke.png b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke.png new file mode 100644 index 00000000..077d12fc Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-multiply-stroke.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-normal-fractional.png b/fixtures/web-first/chromium/svg-group-blend-extent-normal-fractional.png new file mode 100644 index 00000000..70ec02e2 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-normal-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-fill-stroke-gradient.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-fill-stroke-gradient.png new file mode 100644 index 00000000..786d7c8d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-fill-stroke-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-fractional.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-fractional.png new file mode 100644 index 00000000..0f7673a1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-gradient-transform.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-gradient-transform.png new file mode 100644 index 00000000..5b658d25 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-gradient-transform.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-integer.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-integer.png new file mode 100644 index 00000000..ba351f2d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-integer.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-many-reversed.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-many-reversed.png new file mode 100644 index 00000000..23741229 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-many-reversed.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-offscreen.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-offscreen.png new file mode 100644 index 00000000..86d166d1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-offscreen.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-opacity-near.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-opacity-near.png new file mode 100644 index 00000000..0f7673a1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-opacity-near.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-opaque.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-opaque.png new file mode 100644 index 00000000..75ae2792 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-opaque.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-pattern-sibling.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-pattern-sibling.png new file mode 100644 index 00000000..ceccbc72 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-pattern-sibling.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-gradient.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-gradient.png new file mode 100644 index 00000000..f2a2d6cc Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-transparent-fill.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-transparent-fill.png new file mode 100644 index 00000000..f2a2d6cc Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-transparent-fill.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-zero-width.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-zero-width.png new file mode 100644 index 00000000..0f7673a1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-stroke-zero-width.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-extent-screen-use.png b/fixtures/web-first/chromium/svg-group-blend-extent-screen-use.png new file mode 100644 index 00000000..0f7673a1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-extent-screen-use.png differ diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index ca98e80c..15fa8f24 100644 --- a/fixtures/web-first/oracle-bake.json +++ b/fixtures/web-first/oracle-bake.json @@ -5,7 +5,7 @@ "bake_script_sha256": "2bdb5f933d072a1e87c9a675c3342fcf506c0955c0f5c26e2988f4e8fa37c4f2", "capture_module_sha256": "069296201718c43d29efe356fbea893781b73250dca51de3b6d47468d74027b0", "suite": "primitives.json", - "suite_sha256": "0fac8f2a916aecd3ee3e975f162c4d73e27b7e0b35fc3fb06eb0dfe8c7745af5", + "suite_sha256": "29e60218917c4e34c0b6be81178e8524b0d856c7f85863049e1b1d393c926694", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -5293,6 +5293,231 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-extent-multiply-diagonal", + "source": "svg-group-blend-extent-multiply-diagonal.svg", + "source_sha256": "3f7ca8ed0f472e911e08da932391548a70dab5ac2983af4bc50ddeb53624cb87", + "oracle": "chromium/svg-group-blend-extent-multiply-diagonal.png", + "oracle_sha256": "58bd7b3b055455a3c3b1293dc471e1c82be6d08e08aa206d2fbc0d628e11924b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-fractional", + "source": "svg-group-blend-extent-multiply-fractional.svg", + "source_sha256": "e0c7a9102b3c5f6174e8a22a5f7b39376495db10ca93162ff9feb6d148c8e092", + "oracle": "chromium/svg-group-blend-extent-multiply-fractional.png", + "oracle_sha256": "12cba9825574d6272239c8f5ea913c84f67e07ef01782ba3c7e45f3bdd3f94e9", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-integer", + "source": "svg-group-blend-extent-multiply-integer.svg", + "source_sha256": "ffbdfa36f8397ec910c0c275eb6a18a5639ebec93a8710aab32837a23e769a06", + "oracle": "chromium/svg-group-blend-extent-multiply-integer.png", + "oracle_sha256": "6a4f898118730df677d28eba6eaf152aa0178fef02671a0aad0340cfe6c67456", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-leaf", + "source": "svg-group-blend-extent-multiply-leaf.svg", + "source_sha256": "f45af133dfd9788edbe8515184cd3b96231f189dba366df0cbe86e937c2b564a", + "oracle": "chromium/svg-group-blend-extent-multiply-leaf.png", + "oracle_sha256": "12cba9825574d6272239c8f5ea913c84f67e07ef01782ba3c7e45f3bdd3f94e9", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-many", + "source": "svg-group-blend-extent-multiply-many.svg", + "source_sha256": "bfb0609eb83c6618f52754b1ff0e13cf8bc45530fca016946a56364911a04c03", + "oracle": "chromium/svg-group-blend-extent-multiply-many.png", + "oracle_sha256": "6c0f96bde93cb4267f98ca040c1eafac2f670b9be36947268c19b009e6192539", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-opacity-half", + "source": "svg-group-blend-extent-multiply-opacity-half.svg", + "source_sha256": "a82669ad2b89275f8b24761e3cea3cd2969fe36b3c6dfe4bf0174e378363433e", + "oracle": "chromium/svg-group-blend-extent-multiply-opacity-half.png", + "oracle_sha256": "e34b971f40846fb1c2ee57aaa48a8681b64885e1d0d8442015e2a46e3ff99419", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-opaque", + "source": "svg-group-blend-extent-multiply-opaque.svg", + "source_sha256": "7204009c8c97a8f076f404005727968f345779612668c912dec3d4fbdd2baf98", + "oracle": "chromium/svg-group-blend-extent-multiply-opaque.png", + "oracle_sha256": "ed508843958b0878db1bc23e79ecceb1bb6b3326924bf1f63204d86cc699ff32", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-outer-opacity", + "source": "svg-group-blend-extent-multiply-outer-opacity.svg", + "source_sha256": "ca2d04ec7e034809a6595fe000e1756071167df8236b76291802402bddea988d", + "oracle": "chromium/svg-group-blend-extent-multiply-outer-opacity.png", + "oracle_sha256": "e9c4ff9debf0776c31b79ca748e48f05c8c4f637e0fd4ca936c163e2f9f54b37", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke", + "source": "svg-group-blend-extent-multiply-stroke.svg", + "source_sha256": "f359c0b3e89a26beed4e171933f22eb26cef6a174e8f1ba336a8e0feee6e0269", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke.png", + "oracle_sha256": "4d473eb2dba5ab86b3d17a1faf2c1459d632a73c14bd1ed0299b3e4ff6f54b50", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke-clear-gradient", + "source": "svg-group-blend-extent-multiply-stroke-clear-gradient.svg", + "source_sha256": "c8d140f088a74508c5afa0e85f729a92a22dc578565a79b8d975d01da29fa6fc", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke-clear-gradient.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke-none", + "source": "svg-group-blend-extent-multiply-stroke-none.svg", + "source_sha256": "9c4bff78440c10f115026aaeb79e3ed91fbdeab0d61a1a76ee624dceee0614f3", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke-none.png", + "oracle_sha256": "12cba9825574d6272239c8f5ea913c84f67e07ef01782ba3c7e45f3bdd3f94e9", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-normal-fractional", + "source": "svg-group-blend-extent-normal-fractional.svg", + "source_sha256": "5ad6ec3f92f185eb0efa5c5befa942e1a37aa18e55d511b73521a36ea73cef50", + "oracle": "chromium/svg-group-blend-extent-normal-fractional.png", + "oracle_sha256": "8fd768559ce902ae57f2f30cfbd47b99c3cb1d0236a462e23c115877f0e47f4d", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-fill-stroke-gradient", + "source": "svg-group-blend-extent-screen-fill-stroke-gradient.svg", + "source_sha256": "88d9c0b7f0388d152d65c05307b42692bbab44047d5e8cbc0d51d03b9493eee9", + "oracle": "chromium/svg-group-blend-extent-screen-fill-stroke-gradient.png", + "oracle_sha256": "2abc3fd8ffd44d52685c66f8e1dbba35520bf991c9f71dcdcf29a5ced1351cb8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-fractional", + "source": "svg-group-blend-extent-screen-fractional.svg", + "source_sha256": "dc583aed878c6f935c85cec57e4296a8e948f98256d9894d5c2ad0659e02a333", + "oracle": "chromium/svg-group-blend-extent-screen-fractional.png", + "oracle_sha256": "a27a3a28784465b200d855437476628de1dda8a048b1f97be205e271a8d2d0d2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-gradient-transform", + "source": "svg-group-blend-extent-screen-gradient-transform.svg", + "source_sha256": "53e3669b1da66605bc63fd7769267078f63ce7ce5713a71313fda1b75025df39", + "oracle": "chromium/svg-group-blend-extent-screen-gradient-transform.png", + "oracle_sha256": "f7c709d90f0471fa0694f178f6163d2ab6d08010cf79739ca4b1836e46adab53", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-integer", + "source": "svg-group-blend-extent-screen-integer.svg", + "source_sha256": "56aed81b26d0d6871e36e70d36acb5ba98a6c2305dcb550f3b8ff8cb247454c6", + "oracle": "chromium/svg-group-blend-extent-screen-integer.png", + "oracle_sha256": "0378acf96897ecf2e0c5223c113fa3dbc0ab0432675d2ffcac944a392e7b06dc", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-many-reversed", + "source": "svg-group-blend-extent-screen-many-reversed.svg", + "source_sha256": "775cfaabf582f40717fb977d256fc741584c7bb7ae02f795f5cb7f6ed7157228", + "oracle": "chromium/svg-group-blend-extent-screen-many-reversed.png", + "oracle_sha256": "6755b7a49afab272040f68ff6b20df31922627b825e1ae2cd206a638233fee14", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-offscreen", + "source": "svg-group-blend-extent-screen-offscreen.svg", + "source_sha256": "7243f27087a4e24e084a767dfbd72802e40b27441a4cfdcf5a4fe222f845cc0e", + "oracle": "chromium/svg-group-blend-extent-screen-offscreen.png", + "oracle_sha256": "02ba295748fb9b3d927f55340308997226f570bf8a1317edad830afb9655b92e", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-opacity-near", + "source": "svg-group-blend-extent-screen-opacity-near.svg", + "source_sha256": "1308834c3b1b949a7fcfb0d8d58d5f5b2e9eda9cf75d714843bcba88d92ba68f", + "oracle": "chromium/svg-group-blend-extent-screen-opacity-near.png", + "oracle_sha256": "a27a3a28784465b200d855437476628de1dda8a048b1f97be205e271a8d2d0d2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-opaque", + "source": "svg-group-blend-extent-screen-opaque.svg", + "source_sha256": "804b715d3e48f68a4ede1505c94b56e404beb98bcbcf91dd01f5a8e2a4cc302c", + "oracle": "chromium/svg-group-blend-extent-screen-opaque.png", + "oracle_sha256": "e4c3e3ed2aa60fbc447a9b286579d598fc6c144816b4d5cd790b5ea0c0694a8c", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-pattern-sibling", + "source": "svg-group-blend-extent-screen-pattern-sibling.svg", + "source_sha256": "d0729b045be869c6f75a38f9ea5be4968bf4ae1c1da9419ebe3846a8429fa110", + "oracle": "chromium/svg-group-blend-extent-screen-pattern-sibling.png", + "oracle_sha256": "0795872482f7bcc3093a4e5f80097fedc1bb67b5d2a2a44070a9eea64ec18dd8", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-gradient", + "source": "svg-group-blend-extent-screen-stroke-gradient.svg", + "source_sha256": "bdafad5cce9ab20fd09695c6050d71aa7ee273bbc5f2e3ef4b742dc82c0e0d31", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-gradient.png", + "oracle_sha256": "6f81a62b3ac6ec18338bbb899cf51098a25d6f6f9e84f42f763f82e70c3a4a27", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-transparent-fill", + "source": "svg-group-blend-extent-screen-stroke-transparent-fill.svg", + "source_sha256": "63bd3eb43ee128ee43ee804496962cb15993382895b8cafbec8d423b00e377d8", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-transparent-fill.png", + "oracle_sha256": "6f81a62b3ac6ec18338bbb899cf51098a25d6f6f9e84f42f763f82e70c3a4a27", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-zero-width", + "source": "svg-group-blend-extent-screen-stroke-zero-width.svg", + "source_sha256": "c6e6bceb8f5474c5fc2b0e5ce70d0ec992f99c82a7ecf7a7272db2cb5966a467", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-zero-width.png", + "oracle_sha256": "a27a3a28784465b200d855437476628de1dda8a048b1f97be205e271a8d2d0d2", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-use", + "source": "svg-group-blend-extent-screen-use.svg", + "source_sha256": "fc3f3785eae3462f0a331102670102d737a0c8d056ee7299aaeb6a31b2a82d8a", + "oracle": "chromium/svg-group-blend-extent-screen-use.png", + "oracle_sha256": "a27a3a28784465b200d855437476628de1dda8a048b1f97be205e271a8d2d0d2", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-fractional-multiply", "source": "svg-group-blend-fractional-multiply.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index cffc860a..367f82ab 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -4777,6 +4777,206 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-extent-multiply-diagonal", + "source": "svg-group-blend-extent-multiply-diagonal.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-diagonal.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-fractional", + "source": "svg-group-blend-extent-multiply-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-integer", + "source": "svg-group-blend-extent-multiply-integer.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-integer.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-leaf", + "source": "svg-group-blend-extent-multiply-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-many", + "source": "svg-group-blend-extent-multiply-many.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-many.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-opacity-half", + "source": "svg-group-blend-extent-multiply-opacity-half.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-opacity-half.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-opaque", + "source": "svg-group-blend-extent-multiply-opaque.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-opaque.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-outer-opacity", + "source": "svg-group-blend-extent-multiply-outer-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-outer-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke", + "source": "svg-group-blend-extent-multiply-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke-clear-gradient", + "source": "svg-group-blend-extent-multiply-stroke-clear-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke-clear-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-multiply-stroke-none", + "source": "svg-group-blend-extent-multiply-stroke-none.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-multiply-stroke-none.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-normal-fractional", + "source": "svg-group-blend-extent-normal-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-normal-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-fill-stroke-gradient", + "source": "svg-group-blend-extent-screen-fill-stroke-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-fill-stroke-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-fractional", + "source": "svg-group-blend-extent-screen-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-gradient-transform", + "source": "svg-group-blend-extent-screen-gradient-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-gradient-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-integer", + "source": "svg-group-blend-extent-screen-integer.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-integer.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-many-reversed", + "source": "svg-group-blend-extent-screen-many-reversed.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-many-reversed.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-offscreen", + "source": "svg-group-blend-extent-screen-offscreen.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-offscreen.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-opacity-near", + "source": "svg-group-blend-extent-screen-opacity-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-opacity-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-opaque", + "source": "svg-group-blend-extent-screen-opaque.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-opaque.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-pattern-sibling", + "source": "svg-group-blend-extent-screen-pattern-sibling.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-pattern-sibling.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-gradient", + "source": "svg-group-blend-extent-screen-stroke-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-transparent-fill", + "source": "svg-group-blend-extent-screen-stroke-transparent-fill.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-transparent-fill.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-stroke-zero-width", + "source": "svg-group-blend-extent-screen-stroke-zero-width.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-stroke-zero-width.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-extent-screen-use", + "source": "svg-group-blend-extent-screen-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-extent-screen-use.png", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-fractional-multiply", "source": "svg-group-blend-fractional-multiply.svg", diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-diagonal.svg b/fixtures/web-first/svg-group-blend-extent-multiply-diagonal.svg new file mode 100644 index 00000000..601a4bcf --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-diagonal.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-fractional.svg b/fixtures/web-first/svg-group-blend-extent-multiply-fractional.svg new file mode 100644 index 00000000..546e6eb6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-integer.svg b/fixtures/web-first/svg-group-blend-extent-multiply-integer.svg new file mode 100644 index 00000000..2785269d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-integer.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-leaf.svg b/fixtures/web-first/svg-group-blend-extent-multiply-leaf.svg new file mode 100644 index 00000000..2e59490c --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-many.svg b/fixtures/web-first/svg-group-blend-extent-multiply-many.svg new file mode 100644 index 00000000..ea61db33 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-many.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-opacity-half.svg b/fixtures/web-first/svg-group-blend-extent-multiply-opacity-half.svg new file mode 100644 index 00000000..c638228e --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-opacity-half.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-opaque.svg b/fixtures/web-first/svg-group-blend-extent-multiply-opaque.svg new file mode 100644 index 00000000..d13599bd --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-opaque.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-outer-opacity.svg b/fixtures/web-first/svg-group-blend-extent-multiply-outer-opacity.svg new file mode 100644 index 00000000..cd22c450 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-outer-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-stroke-clear-gradient.svg b/fixtures/web-first/svg-group-blend-extent-multiply-stroke-clear-gradient.svg new file mode 100644 index 00000000..0c5533c3 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-stroke-clear-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-stroke-none.svg b/fixtures/web-first/svg-group-blend-extent-multiply-stroke-none.svg new file mode 100644 index 00000000..956709dd --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-stroke-none.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-multiply-stroke.svg b/fixtures/web-first/svg-group-blend-extent-multiply-stroke.svg new file mode 100644 index 00000000..fbe18393 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-multiply-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-normal-fractional.svg b/fixtures/web-first/svg-group-blend-extent-normal-fractional.svg new file mode 100644 index 00000000..42b59ab4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-normal-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-fill-stroke-gradient.svg b/fixtures/web-first/svg-group-blend-extent-screen-fill-stroke-gradient.svg new file mode 100644 index 00000000..68cc54c5 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-fill-stroke-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-fractional.svg b/fixtures/web-first/svg-group-blend-extent-screen-fractional.svg new file mode 100644 index 00000000..23e54621 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-gradient-transform.svg b/fixtures/web-first/svg-group-blend-extent-screen-gradient-transform.svg new file mode 100644 index 00000000..4ba97578 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-gradient-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-integer.svg b/fixtures/web-first/svg-group-blend-extent-screen-integer.svg new file mode 100644 index 00000000..478225d0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-integer.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-many-reversed.svg b/fixtures/web-first/svg-group-blend-extent-screen-many-reversed.svg new file mode 100644 index 00000000..7c71db11 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-many-reversed.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-offscreen.svg b/fixtures/web-first/svg-group-blend-extent-screen-offscreen.svg new file mode 100644 index 00000000..236280fe --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-offscreen.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-opacity-near.svg b/fixtures/web-first/svg-group-blend-extent-screen-opacity-near.svg new file mode 100644 index 00000000..ed27a139 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-opacity-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-opaque.svg b/fixtures/web-first/svg-group-blend-extent-screen-opaque.svg new file mode 100644 index 00000000..6c7a9ac9 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-opaque.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-pattern-sibling.svg b/fixtures/web-first/svg-group-blend-extent-screen-pattern-sibling.svg new file mode 100644 index 00000000..1bd15ef0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-pattern-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-stroke-gradient.svg b/fixtures/web-first/svg-group-blend-extent-screen-stroke-gradient.svg new file mode 100644 index 00000000..484afa85 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-stroke-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-stroke-transparent-fill.svg b/fixtures/web-first/svg-group-blend-extent-screen-stroke-transparent-fill.svg new file mode 100644 index 00000000..8cf57c2a --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-stroke-transparent-fill.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-stroke-zero-width.svg b/fixtures/web-first/svg-group-blend-extent-screen-stroke-zero-width.svg new file mode 100644 index 00000000..2e8eab02 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-stroke-zero-width.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-extent-screen-use.svg b/fixtures/web-first/svg-group-blend-extent-screen-use.svg new file mode 100644 index 00000000..4fd691e8 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-extent-screen-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index 45b846cd..63227ab2 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -23,6 +23,10 @@ The scannable, generated view of this register (beside the baked cells) is | File | Required result | | --- | --- | +| `svg-group-blend-linear-extent-stroke-context-{missing,none}.svg` | Preserve the original computed context-stroke/context-fill selection before following a missing provider or a provider selecting `none`. Chromium still retains the selected stroke's bounding-box contribution; both routes must reach the named source-extent guard, not become computed `stroke:none`. | +| `svg-group-blend-linear-extent-stroke-{transparent,opacity-zero,empty-gradient,missing-reference,none-fallback,sibling}.svg` | A selected stroke can enlarge Chromium's drawable bounds even when a live fill remains and the stroke paint disappears. Unresolved references can do so without an opacity pass. Preserve the producer-private omitted-extent fact and refuse the containing linear source by name; do not treat it as `stroke:none`. The sibling case guards propagation across distinct leaves. | +| `svg-group-blend-linear-extent-{transform,rotation,viewport,clip,child-opacity,zero-opacity,transparent,fill-zero,empty-gradient,pattern-stroke,nested-blend,isolation}.svg` | Name `linear-gradient source-extent` and skip the complete affected group. The current resolved stream cannot prove the source-space extent for these combinations. The guard is conservative, not a claim that every member has a measured mismatch; the [B2a evidence](../../../docs/wg/consolidation/svg-engine-of-record.md#b2a-linear-gradient-blend-source-extents) separates those facts. | +| `svg-group-blend-linear-extent-root{,-opacity}.svg` | The required root boundary contains a bare ramp and a completed child blend, with unit or partial root opacity. Refuse in both admissions under the conservative source-extent profile, even though these full-background controls were exact before the patrol (measured, not celled). | | `svg-group-blend-mode-{overlay,darken,lighten,color-dodge,color-burn,hard-light,soft-light,difference,exclusion,hue,saturation,color,luminosity,plus-lighter}.svg` | Fourteen live computed values outside B1's normal/multiply/screen profile must name `mix-blend-mode`; best-effort skips the attributed group, never substitutes normal. | | `svg-group-blend-{filter,mask}.svg` | Name the unadmitted image-effect composition profile and skip the complete affected group transaction. | | `svg-group-blend-elided-{filter,mask}-{unit,partial}.svg` | Eliding redundant Normal isolation must retain its authored participation for an ancestor filter/mask patrol, without inventing a physical layer or blocking the established opacity fold. These four inputs keep the conservative image-effect refusal; they are not claims of measured pixel defects. | diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-child-opacity.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-child-opacity.svg new file mode 100644 index 00000000..db6ba235 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-child-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-clip.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-clip.svg new file mode 100644 index 00000000..18bf1672 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-empty-gradient.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-empty-gradient.svg new file mode 100644 index 00000000..a7178035 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-empty-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-fill-zero.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-fill-zero.svg new file mode 100644 index 00000000..24373255 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-fill-zero.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-isolation.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-isolation.svg new file mode 100644 index 00000000..dc590ed7 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-isolation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-nested-blend.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-nested-blend.svg new file mode 100644 index 00000000..c4233626 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-nested-blend.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-pattern-stroke.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-pattern-stroke.svg new file mode 100644 index 00000000..9d690f8b --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-pattern-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root-opacity.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root-opacity.svg new file mode 100644 index 00000000..6982e693 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root.svg new file mode 100644 index 00000000..6129ccd6 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-root.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-rotation.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-rotation.svg new file mode 100644 index 00000000..147419f2 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-rotation.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-missing.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-missing.svg new file mode 100644 index 00000000..42db30cf --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-missing.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-none.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-none.svg new file mode 100644 index 00000000..5b7fe1de --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-context-none.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-empty-gradient.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-empty-gradient.svg new file mode 100644 index 00000000..69786b57 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-empty-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-missing-reference.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-missing-reference.svg new file mode 100644 index 00000000..d9120bd0 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-missing-reference.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-none-fallback.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-none-fallback.svg new file mode 100644 index 00000000..da40b60c --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-none-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-opacity-zero.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-opacity-zero.svg new file mode 100644 index 00000000..7156537d --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-opacity-zero.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-sibling.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-sibling.svg new file mode 100644 index 00000000..8cf8df66 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-transparent.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-transparent.svg new file mode 100644 index 00000000..b6f612c4 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-transparent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transform.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transform.svg new file mode 100644 index 00000000..ca9c2f29 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transparent.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transparent.svg new file mode 100644 index 00000000..07a2a1a0 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-transparent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-viewport.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-viewport.svg new file mode 100644 index 00000000..7912800b --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-viewport.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-zero-opacity.svg b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-zero-opacity.svg new file mode 100644 index 00000000..4682d13d --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-linear-extent-zero-opacity.svg @@ -0,0 +1 @@ +