diff --git a/crates/n0/README.md b/crates/n0/README.md index 91ac7bdd..085f53db 100644 --- a/crates/n0/README.md +++ b/crates/n0/README.md @@ -308,6 +308,20 @@ the previously blended pixels. `tests/group_blending.rs` pins these consumer laws with hand-built frames and exact pixel probes. Those tests do not claim Chromium parity or measured performance. +An optional checked `BlendSourceDomain` on that operation is complete, +already-enclosed source material, not a geometry/damage box or an allocation +hint. The first glyphless consumer accepts identity-mapped declarations over +untransformed solid/linear rectangles and simple local strokes; it rejects +under-enclosing domains, nested effects within the declaration, nonidentity +declaration maps, enclosing filter/mask programs, and declarations inside +repeating programs. Ordinary undeclared scopes keep their existing profile. +The declaration targets the containing stream, independently of a scope item's +drawing map. Current-view mapping and device enclosure are recalculated for +every execution. Unsupported device bounds return an owner-bearing +`FrameExecutionError::SourceDomain` before touching the canvas. There is no +source-image cache. Consumer tests distinguish raster identity from painted +geometry and prove changed-view replay equals a fresh product. + Multiply and partial-opacity Screen restoration explicitly round byte opacity before scaling source bytes and applying the byte-domain blend. This avoids pinned Skia's differing ARM/x86 low-precision arithmetic and runtime-blender opacity ordering. diff --git a/crates/n0/src/frame.rs b/crates/n0/src/frame.rs index 5cd5e4ea..1d251e78 100644 --- a/crates/n0/src/frame.rs +++ b/crates/n0/src/frame.rs @@ -140,6 +140,10 @@ pub enum FrameExecutionError { Environment(PaintEnvironmentMismatch), Image(crate::paint::ImagePreflightError), Pattern(crate::paint::PatternPreflightError), + /// A declared source domain cannot be represented under the current view. + SourceDomain { + owner: rframe::VisualRef, + }, } impl std::fmt::Display for FrameExecutionError { @@ -148,6 +152,9 @@ impl std::fmt::Display for FrameExecutionError { FrameExecutionError::Environment(error) => error.fmt(f), FrameExecutionError::Image(error) => error.fmt(f), FrameExecutionError::Pattern(error) => error.fmt(f), + FrameExecutionError::SourceDomain { owner } => { + write!(f, "blend source domain {owner:?} has an unsupported device enclosure under the current view") + } } } } diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index faaeb31d..b89980aa 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -175,6 +175,9 @@ pub struct FrameProduct { resolved: Frame, drawlist: DrawList, provenance: ProvenanceProjection, + /// Only declared domains need current-view validation. Empty means no + /// added execution traversal or allocation for ordinary products. + source_domains: Vec<(VisualRef, rframe::BlendSourceDomain)>, } impl FrameProduct { @@ -191,6 +194,7 @@ impl FrameProduct { ctx: &PaintCtx, ) -> Result<(), FrameExecutionError> { self.assert_provenance_complete(); + self.preflight_source_domains(view)?; crate::paint::preflight_patterns(&self.drawlist, ctx)?; crate::paint::execute_unchecked(canvas, &self.drawlist, &to_affine(*view), ctx); Ok(()) @@ -206,6 +210,7 @@ impl FrameProduct { ctx: &PaintCtx, ) -> Result, FrameExecutionError> { self.assert_provenance_complete(); + self.preflight_source_domains(view)?; crate::paint::preflight_patterns(&self.drawlist, ctx)?; Ok(crate::paint::raster_to_bytes_unchecked( &self.drawlist, @@ -216,6 +221,30 @@ impl FrameProduct { )) } + fn preflight_source_domains( + &self, + view: &math2::transform::AffineTransform, + ) -> Result<(), FrameExecutionError> { + for &(owner, domain) in &self.source_domains { + // The admitted declaration map is identity. Validate all view + // arithmetic before touching the caller's canvas; never ignore a + // domain just because its current device bounds cannot be formed. + if rframe::BlendSourceDomain::new(domain.rect(), *view).is_err() { + return Err(FrameExecutionError::SourceDomain { owner }); + } + let bounds = math2::rect_transform(domain.rect(), view); + if bounds + .corners() + .into_iter() + .flatten() + .any(|v| v.abs() > 8_388_608.0) + { + return Err(FrameExecutionError::SourceDomain { owner }); + } + } + Ok(()) + } + fn assert_provenance_complete(&self) { debug_assert!(self .drawlist @@ -361,11 +390,19 @@ pub fn compile(resolved: Frame) -> Result { // scope composites — and a child scope's union folds into its parent's // when it closes. let mut open_scopes: Vec = Vec::new(); + let mut active_source_domain: Option<(VisualRef, rframe::BlendSourceDomain)> = None; + let mut source_domains = Vec::new(); for frame_item in resolved.items.iter() { let node = match frame_item { FrameItem::Node(node) => node, FrameItem::ScopeBegin(scope) => { + if let Some((owner, _)) = active_source_domain { + return Err(BuildError::Blend { + owner, + reason: "a declared source domain does not yet admit nested effects".into(), + }); + } if !unique.insert(scope.owner) { return Err(BuildError::DuplicateOwner(scope.owner)); } @@ -402,6 +439,21 @@ pub fn compile(resolved: Frame) -> Result { (OpenScopeKind::Opacity, None) } ScopeEffect::Blend(blend) => { + if let Some(domain) = blend.source_domain() { + if domain.source_to_stream() + != math2::transform::AffineTransform::identity() + || open_scopes.iter().any(|scope| { + matches!( + scope.kind, + OpenScopeKind::Mask { .. } | OpenScopeKind::Filter { .. } + ) + }) + { + return Err(BuildError::Blend { owner: scope.owner, reason: "a declared source domain needs the unmapped, non-image-effect profile".into() }); + } + active_source_domain = Some((scope.owner, domain)); + source_domains.push((scope.owner, domain)); + } crate::paint::preflight_isolated_blend(*blend).map_err(|reason| { BuildError::Blend { owner: scope.owner, @@ -492,6 +544,11 @@ pub fn compile(resolved: Frame) -> Result { ), }; let slot = scope.slot; + if active_source_domain + .is_some_and(|(owner, _)| provenance.owners[slot.index()] == owner) + { + active_source_domain = None; + } provenance.coverage[slot.index()] = coverage; if let (Some(coverage), Some(parent)) = (coverage, open_scopes.last_mut()) { parent.coverage = Some(match parent.coverage { @@ -507,6 +564,13 @@ pub fn compile(resolved: Frame) -> Result { continue; } FrameItem::MaskBegin(mask) => { + if let Some((owner, _)) = active_source_domain { + return Err(BuildError::Blend { + owner, + reason: "a declared source domain does not yet admit nested image masks" + .into(), + }); + } if !unique.insert(mask.owner) { return Err(BuildError::DuplicateOwner(mask.owner)); } @@ -623,6 +687,12 @@ pub fn compile(resolved: Frame) -> Result { if node.bounds != math2::rect_transform(rect, &node.transform) { return Err(BuildError::VisualBoundsMismatch(node.owner)); } + if let Some((owner, domain)) = active_source_domain { + validate_source_domain_node(node, domain).map_err(|reason| BuildError::Blend { + owner, + reason: reason.into(), + })?; + } // The paint reference box is the geometry's own extent. Ordinary box // routes draw at their item origin, so their paint box already starts // there. A path's stream carries absolute local coordinates, so its box @@ -877,9 +947,59 @@ pub fn compile(resolved: Frame) -> Result { resolved, drawlist, provenance, + source_domains, }) } +/// The first domain consumer has a bounded, independently tested source +/// profile. Other declarations refuse before compilation returns a product. +fn validate_source_domain_node( + node: &rframe::FrameNode, + domain: rframe::BlendSourceDomain, +) -> Result<(), &'static str> { + let Geometry::Rect(rect) = node.geometry else { + return Err("a declared source domain requires rectangular source geometry"); + }; + if node.transform != math2::transform::AffineTransform::identity() + || node.paints.pattern().is_some() + || node + .paints + .iter() + .any(|paint| matches!(paint, CgPaint::RadialGradient(_))) + || (node.paints.is_empty() && node.stroke.is_none()) + { + return Err("a declared source domain requires unmapped solid/linear painted rectangles"); + } + let width = if let Some(stroke) = &node.stroke { + if stroke.space() != rframe::StrokeSpace::Local + || stroke.cap() != rframe::StrokeCap::Butt + || stroke.join() != rframe::StrokeJoin::Miter + || stroke.dash().is_some() + || stroke.dash_intervals().is_some() + || stroke.paints().pattern().is_some() + || stroke + .paints() + .iter() + .any(|paint| matches!(paint, CgPaint::RadialGradient(_))) + { + return Err("a declared source domain does not yet admit complex strokes"); + } + stroke.width() + } else { + 0.0 + }; + let bounds = domain.rect(); + let half = f64::from(width) / 2.0; + if f64::from(rect.x) - half < f64::from(bounds.x) + || f64::from(rect.y) - half < f64::from(bounds.y) + || f64::from(rect.x) + f64::from(rect.width) + half > f64::from(bounds.x + bounds.width) + || f64::from(rect.y) + f64::from(rect.height) + half > f64::from(bounds.y + bounds.height) + { + return Err("a declared source domain does not enclose its painted rectangle"); + } + Ok(()) +} + /// Project the contract's checked command stream into the engine's resolved /// path material. /// @@ -1395,6 +1515,9 @@ fn compile_pattern( owner, reason: format!("nested pattern program failed projection: {error}"), })?; + if !product.source_domains.is_empty() { + return Err(BuildError::Paint { owner, reason: "a declared blend source domain inside a repeating program needs its own execution profile".into() }); + } Ok(Arc::new(ResolvedPattern { width: pattern.width(), height: pattern.height(), @@ -4254,4 +4377,254 @@ mod tests { ); assert!(diff_frame(&before, &before).is_empty()); } + + fn domain_illustration( + domain: Option, + mode: rframe::ScopeBlendMode, + ) -> Frame { + let mut blend = rframe::ScopeBlend::new(mode, Some(ScopeOpacity::new(0.75).unwrap())); + if let Some(domain) = domain { + blend = blend.with_source_domain(domain); + } + let mut ramp = rect_node( + RECT_OWNER, + Rectangle::from_xywh(8.3, 12.7, 38.2, 28.4), + 0xFFCD_6843, + ); + ramp.bounds = math2::rect_transform(ramp.geometry.local_box(), &ramp.transform); + ramp.paints = PaintStack::try_from_paints(CgPaints::new([CgPaint::LinearGradient( + cg::LinearGradientPaint { + stops: vec![ + cg::GradientStop { + offset: 0.0, + color: CGColor::from_rgba(205, 104, 67, 255).into(), + }, + cg::GradientStop { + offset: 1.0, + color: CGColor::from_rgba(91, 172, 225, 153).into(), + }, + ], + ..Default::default() + }, + )])) + .unwrap(); + frame_of( + FrameItems::try_new(vec![ + FrameItem::Node(rect_node( + OTHER_OWNER, + Rectangle::from_xywh(0.0, 0.0, 64.0, 48.0), + 0xFF42_6589, + )), + FrameItem::ScopeBegin(Scope { + owner: SCOPE_OWNER, + effect: ScopeEffect::Blend(blend), + }), + FrameItem::Node(ramp), + FrameItem::ScopeEnd, + ]) + .unwrap(), + ) + } + + fn source_domain() -> rframe::BlendSourceDomain { + rframe::BlendSourceDomain::new( + Rectangle::from_xywh(6.0, 10.0, 43.0, 34.0), + AffineTransform::identity(), + ) + .unwrap() + } + + #[test] + fn declared_source_domain_is_raster_material_not_geometry_or_an_extra_draw() { + let ordinary = + compile(domain_illustration(None, rframe::ScopeBlendMode::Multiply)).unwrap(); + let declared = compile(domain_illustration( + Some(source_domain()), + rframe::ScopeBlendMode::Multiply, + )) + .unwrap(); + assert!(ordinary.source_domains.is_empty()); + assert_eq!( + declared.source_domains, + vec![(SCOPE_OWNER, source_domain())] + ); + assert_eq!(ordinary.resolved.nodes(), declared.resolved.nodes()); + assert_eq!( + ordinary.provenance.coverage, declared.provenance.coverage, + "source material is not damage coverage" + ); + assert_eq!(ordinary.drawlist.items.len(), declared.drawlist.items.len()); + assert!(!ordinary.drawlist.raster_eq(&declared.drawlist)); + let context = PaintCtx::new(None); + assert_ne!( + ordinary + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &context) + .unwrap(), + declared + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &context) + .unwrap(), + "the independent illustration exercises source material, not only field equality" + ); + } + + #[test] + fn declared_source_replay_under_changed_views_matches_fresh() { + let context = PaintCtx::new(None); + for mode in [ + rframe::ScopeBlendMode::Normal, + rframe::ScopeBlendMode::Multiply, + rframe::ScopeBlendMode::Screen, + ] { + let frame = domain_illustration(Some(source_domain()), mode); + let retained = compile(frame.clone()).unwrap(); + for matrix in [ + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + [[1.0, 0.0, 3.25], [0.0, 1.0, -2.5]], + [[1.25, 0.0, -4.0], [0.0, 0.75, 2.0]], + [[1.0, 0.2, 0.0], [0.1, 1.0, 0.0]], + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + ] { + let view = AffineTransform { matrix }; + let pixels = retained.raster_to_bytes(&view, 80, 64, &context).unwrap(); + assert_eq!( + pixels, + compile(frame.clone()) + .unwrap() + .raster_to_bytes(&view, 80, 64, &context) + .unwrap() + ); + let mut surface = surfaces::raster_n32_premul((80, 64)).unwrap(); + surface.canvas().clear(skia_safe::Color::WHITE); + let saves = surface.canvas().save_count(); + retained.execute(surface.canvas(), &view, &context).unwrap(); + assert_eq!(surface.canvas().save_count(), saves); + assert_eq!(crate::paint::read_pixels(&mut surface, 80, 64), pixels); + } + } + } + + #[test] + fn unrepresentable_current_source_view_refuses_before_touching_canvas() { + let product = compile(domain_illustration( + Some(source_domain()), + rframe::ScopeBlendMode::Screen, + )) + .unwrap(); + let context = PaintCtx::new(None); + let mut surface = surfaces::raster_n32_premul((64, 48)).unwrap(); + surface.canvas().clear(skia_safe::Color::MAGENTA); + let before = crate::paint::read_pixels(&mut surface, 64, 48); + let saves = surface.canvas().save_count(); + for matrix in [ + [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + [[1.0, 0.0, f32::NAN], [0.0, 1.0, 0.0]], + [[1.0, 0.0, 10_000_000.0], [0.0, 1.0, 0.0]], + [[f32::MAX, 0.0, 0.0], [0.0, 1.0, 0.0]], + ] { + let view = AffineTransform { matrix }; + let error = product + .execute(surface.canvas(), &view, &context) + .unwrap_err(); + assert!(matches!( + error, + FrameExecutionError::SourceDomain { owner: SCOPE_OWNER } + )); + assert!(error.to_string().contains("blend source domain")); + assert!(matches!( + product.raster_to_bytes(&view, 64, 48, &context), + Err(FrameExecutionError::SourceDomain { owner: SCOPE_OWNER }) + )); + assert_eq!(crate::paint::read_pixels(&mut surface, 64, 48), before); + assert_eq!(surface.canvas().save_count(), saves); + } + } + + #[test] + fn declared_source_profile_refuses_unproved_contract_valid_programs() { + let reject = |items: Vec| { + let error = compile(frame_of(FrameItems::try_new(items).unwrap())) + .err() + .expect("unsupported source profile"); + assert!( + matches!( + error, + BuildError::Blend { + owner: SCOPE_OWNER, + .. + } + ), + "{error}" + ); + }; + for domain in [ + rframe::BlendSourceDomain::new( + Rectangle::from_xywh(8.0, 12.0, 2.0, 2.0), + AffineTransform::identity(), + ) + .unwrap(), + rframe::BlendSourceDomain::new( + source_domain().rect(), + AffineTransform { + matrix: [[1.0, 0.0, 3.0], [0.0, 1.0, 0.0]], + }, + ) + .unwrap(), + ] { + reject( + domain_illustration(Some(domain), rframe::ScopeBlendMode::Multiply) + .items + .iter() + .cloned() + .collect(), + ); + } + for variant in 0..5 { + let mut items: Vec<_> = + domain_illustration(Some(source_domain()), rframe::ScopeBlendMode::Multiply) + .items + .iter() + .cloned() + .collect(); + let FrameItem::Node(node) = &mut items[2] else { + unreachable!() + }; + match variant { + 0 => node.geometry = Geometry::Ellipse(node.geometry.local_box()), + 1 => { + node.transform.matrix[0][2] = 1.0; + node.bounds = math2::rect_transform(node.geometry.local_box(), &node.transform); + } + 2 => { + node.stroke = Some(checked_stroke( + 1.0, + rframe::StrokeCap::Round, + rframe::StrokeJoin::Miter, + 4.0, + None, + )) + } + 3 => { + node.stroke = Some(checked_stroke( + 1.0, + rframe::StrokeCap::Butt, + rframe::StrokeJoin::Miter, + 4.0, + Some(vec![2.0, 3.0]), + )) + } + 4 => node.paints = PaintStack::empty(), + _ => unreachable!(), + } + reject(items); + } + let mut items: Vec<_> = + domain_illustration(Some(source_domain()), rframe::ScopeBlendMode::Multiply) + .items + .iter() + .cloned() + .collect(); + items.insert(2, scope_begin(INNER_SCOPE_OWNER, 0.5)); + items.insert(4, FrameItem::ScopeEnd); + reject(items); + } } diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index bfbc6c0a..6fb74218 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -4464,11 +4464,12 @@ 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 +/// canvas. Honor a declared complete source domain before considering private +/// reconstruction from draw commands. Map either through the 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. +/// by the resolved stream. Other undeclared raw programs keep their old route. fn blend_source_extents(list: &DrawList, view: &Affine) -> Option>> { use skia_safe::RoundOut; @@ -4506,12 +4507,13 @@ fn blend_source_extents(list: &DrawList, view: &Affine) -> Option, known: bool, ramp: bool, + declared: 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(bounds) = bounds.filter(|_| !self.declared) { if let Some(accumulated) = &mut self.bounds { accumulated.join(bounds); } else { @@ -4523,6 +4525,7 @@ fn blend_source_extents(list: &DrawList, view: &Affine) -> Option linear(paints), ItemKind::RectStroke { stroke, .. } => linear(&stroke.paints), + ItemKind::BeginIsolatedBlend { blend } => blend.source_domain().is_some(), _ => false, }) { return None; @@ -4550,7 +4553,9 @@ fn blend_source_extents(list: &DrawList, view: &Affine) -> Option Rect { bounds.round_out() }); } } @@ -4594,11 +4599,24 @@ fn blend_source_extents(list: &DrawList, view: &Affine) -> Option blend.source_domain(), + _ => None, + }; + let bounds = declared.map(|domain| { + let [[a, c, e], [b, d, f]] = domain.source_to_stream().matrix; + let map = view.then(&Affine { a, b, c, d, e, f }); + let rect = domain.rect(); + skia_matrix(&map) + .map_rect(Rect::from_xywh(rect.x, rect.y, rect.width, rect.height)) + .0 + }); stack.push(Source { kind, - bounds: None, + bounds, known: true, ramp: false, + declared: declared.is_some(), }); } else if let Some(source) = stack.last_mut() { let bounds = bounds @@ -4763,6 +4781,53 @@ mod blend_source_extent_tests { assert!(blend_source_extents(&list, &Affine::IDENTITY).is_none()); } + #[test] + fn declared_domain_is_authoritative_and_never_a_draw_derived_hint() { + let domain = rframe::BlendSourceDomain::new( + math2::Rectangle::from_xywh(6.0, 10.0, 43.0, 34.0), + math2::transform::AffineTransform::identity(), + ) + .unwrap(); + for mode in [ + rframe::ScopeBlendMode::Normal, + rframe::ScopeBlendMode::Multiply, + rframe::ScopeBlendMode::Screen, + ] { + let mut list = scene(); + list.items[0].kind = ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new(mode, None).with_source_domain(domain), + }; + // The declaration targets the stream, not the scope item's local + // drawing map. A nonzero frame origin must not be applied twice. + list.items[0].world = Affine::translate(100.0, 200.0); + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + Some(Rect::new(6.0, 10.0, 49.0, 44.0)) + ); + assert_eq!( + blend_source_extents(&list, &Affine::translate(3.25, -2.5)).unwrap()[0], + Some(Rect::new(9.0, 7.0, 53.0, 42.0)) + ); + // An independent producer can use the same fact with solid paint. + if let ItemKind::RectFill { paints, .. } = &mut list.items[1].kind { + *paints = Paints::new([ModelPaint::Solid(n0_model::model::SolidPaint::new( + n0_model::model::Color(0xFFCD_6843), + ))]); + } + assert_eq!( + blend_source_extents(&list, &Affine::IDENTITY).unwrap()[0], + Some(Rect::new(6.0, 10.0, 49.0, 44.0)) + ); + list.items[0].kind = ItemKind::BeginIsolatedBlend { + blend: rframe::ScopeBlend::new(mode, None), + }; + assert!( + blend_source_extents(&list, &Affine::IDENTITY).is_none(), + "a mutable raw list cannot reuse an old declaration" + ); + } + } + #[test] fn unknown_sibling_does_not_disable_a_separate_known_source() { let mut list = scene(); diff --git a/crates/n0/tests/group_blending.rs b/crates/n0/tests/group_blending.rs index 34e1df66..83ac3de7 100644 --- a/crates/n0/tests/group_blending.rs +++ b/crates/n0/tests/group_blending.rs @@ -1207,3 +1207,130 @@ fn a_source_generating_filter_supplies_blend_pixels_and_scope_coverage() { assert_eq!(damage.changed, [owner(10)]); assert_eq!(damage.union_frame, Some(region)); } + +#[test] +fn declared_source_domains_do_not_inherit_the_wider_undeclared_image_profile() { + use rframe::{ + BlendSourceDomain, Filter, FilterColorSpace, FilterNode, FilterPrimitive, FilterProgram, + }; + let region = rect(0.0, 0.0, 48.0, 48.0); + let declared = || { + begin( + 10, + ScopeEffect::Blend( + ScopeBlend::new(ScopeBlendMode::Multiply, None).with_source_domain( + BlendSourceDomain::new(region, AffineTransform::identity()).unwrap(), + ), + ), + ) + }; + let source = || solid(1, rect(8.0, 8.0, 24.0, 24.0), FIRST); + let reject = |items| { + let error = compile(frame(items)).unwrap_err(); + assert!( + matches!(error, BuildError::Blend { owner: value, .. } if value == owner(10)), + "{error}" + ); + }; + // An inherited output clip is distinct from an effect inside the declared + // source. The first consumer accepts the former and refuses the latter. + compile(frame(vec![ + begin(11, ScopeEffect::Clip(clip(region))), + declared(), + source(), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ])) + .unwrap(); + reject(vec![ + declared(), + begin(11, ScopeEffect::Clip(clip(region))), + source(), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]); + let mask = || FrameItem::MaskBegin(Mask::new(owner(11), MaskMode::Alpha, clip(region))); + reject(vec![ + declared(), + mask(), + source(), + FrameItem::MaskSource, + solid(2, region, CGColor::WHITE), + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + ]); + reject(vec![ + mask(), + declared(), + source(), + FrameItem::ScopeEnd, + FrameItem::MaskSource, + solid(2, region, CGColor::WHITE), + FrameItem::MaskEnd, + ]); + let program = FilterProgram::new(Arc::from([FilterNode::new( + Arc::from([]), + region, + FilterColorSpace::Srgb, + FilterPrimitive::SolidColor { + color: FIRST.into(), + }, + )])) + .unwrap(); + let filter = Filter::new(AffineTransform::identity(), region, program).unwrap(); + reject(vec![ + begin(11, ScopeEffect::Filter(filter)), + declared(), + source(), + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]); + let tile = FrameItems::try_new(vec![declared(), source(), FrameItem::ScopeEnd]).unwrap(); + let pattern = + PatternPaint::new(48.0, 48.0, AffineTransform::identity(), Arc::new(tile), 1.0).unwrap(); + let error = compile(frame(vec![FrameItem::Node(node( + 1, + region, + PaintStack::from_pattern(pattern), + ))])) + .unwrap_err(); + assert!( + matches!(error, BuildError::Paint { owner: value, .. } if value == owner(1)), + "{error}" + ); + assert!(error.to_string().contains("own execution profile")); +} + +#[cfg(feature = "trace")] +#[test] +fn declared_source_changes_observed_extent_without_adding_a_layer() { + use n0::trace::sink::drain_blend_layers; + for declared in [false, true] { + let mut operation = ScopeBlend::new(ScopeBlendMode::Multiply, None); + if declared { + operation = operation.with_source_domain( + rframe::BlendSourceDomain::new( + rect(4.0, 6.0, 32.0, 28.0), + AffineTransform::identity(), + ) + .unwrap(), + ); + } + let product = compile(frame(vec![ + begin(10, ScopeEffect::Blend(operation)), + linear_node(1, rect(8.0, 8.0, 24.0, 24.0)), + FrameItem::ScopeEnd, + ])) + .unwrap(); + drain_blend_layers(); + raster(&product, BACKDROP); + let observed = drain_blend_layers(); + assert_eq!(observed.len(), 1); + assert_eq!(observed[0].save_layer_calls, 1); + assert_eq!(observed[0].observed_raster_layers, 1); + assert_eq!( + observed[0].observed_raster_bytes, + if declared { 32 * 28 * 4 } else { 24 * 24 * 4 } + ); + } +} diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 5bc81783..72f481ec 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,423 Chromium-baked cells plus + cells. The complete primitive corpus contains 1,467 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 325 rows. `feFlood`, `feComposite`, + Bungee), and the named refusal register has 329 rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, `feDiffuseLighting`, `feDistantLight`, `fePointLight`, `feSpotLight`, @@ -862,9 +862,16 @@ isolate` have a bounded static SVG group profile. One Stylo computed value 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 + drawable bounds. A live rectangular linear fill with a resolved positive-width + transparent or zero-alpha solid local stroke now retains a complete source + domain on its blend scope, without inventing stroke paint. This narrower + declaration includes untransformed solid/linear rectangle siblings and simple + local painted strokes, enclosing each contributor before union. Its local + endpoints must stay within ±8,388,608 and enclose the resolved painted values + without inward rounding; a repeating-pattern sibling cannot yet + enter that declared domain. Mapped contributors, nested source scopes/opacity, + non-painted geometry contributors, omitted unresolved/context stroke extents, + non-scaling invisible strokes, 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 @@ -872,6 +879,10 @@ isolate` have a bounded static SVG group profile. One Stylo computed value 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. + An authored non-normal root blend over a bare linear source also refuses in + both admissions, including without any omitted stroke. A root source cannot + borrow the child-boundary complete-domain exemption; elided Normal root + isolation without an omitted contribution keeps its established route. 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. diff --git a/crates/rframe/README.md b/crates/rframe/README.md index e9429569..42cd8228 100644 --- a/crates/rframe/README.md +++ b/crates/rframe/README.md @@ -53,6 +53,43 @@ Fill/stroke overlap is already part of that completed group. A separate outer opacity scope around a blend scope states a different nesting and backdrop; it is not an equivalent spelling of combined blend and opacity. +`BlendSourceDomain::new(rect, source_to_stream)` checks one strictly positive, +already-enclosed source-local rectangle and its map into the containing item +stream. `ScopeBlend::with_source_domain(domain)` attaches this complete source +declaration to the existing boundary. `source_domain()` returns it; the ordinary +`ScopeBlend::new` leaves it absent, making no completeness assertion. + +The source materializes against transparent black over the declared domain +before that boundary's final opacity/blend. Contribution discovery and local +enclosure have already happened. A consumer must preserve that domain rather +than reconstruct it from surviving paint, conservatively enlarge it, or enclose +contributors after mapping. Its transparent margins can distinguish sources +with identical painted nodes. `rect()` and `source_to_stream()` preserve the +supplied facts exactly, including when different declarations have the same +final enclosing box. The map targets frame space in a frame and tile-local +coordinates in a repeating program. It never becomes an inherited transform +for the enclosed nodes. Nested declarations stay with their existing boundary +and completed source; the checked stream remains the only composition order. + +This declaration supplies no paint, changes no geometry or gradient paint box, +and introduces no clip. A hard clip specifies coverage and cannot substitute +for a complete source domain. Output clipping remains separate. The declaration +carries no host view, device grid, layer allocation, or cache policy; current-view +mapping and device enclosure remain execution work. A consumer must refuse a +declaration it cannot honor rather than silently ignore it. Domain equality +does not remove the enclosing backdrop dependency. + +Construction proves numerical usability, not producer completeness: the local +rectangle must have finite members, positive extents, and finite, strictly +ordered endpoints in `f32`. The map must have a finite determinant and a +supported finite affine inverse, and +the mapped corners and their enclosing box must remain finite and positive. +The existing affine inverse's small-determinant refusal applies. No integer +coordinate rule or enclosure algorithm is imposed on the source's unit system. +Empty domains are refused; a domain never makes an empty blend scope meaningful. +Producer-only laws, including independent illustration construction, live in +[`tests/blend_source_domain.rs`](tests/blend_source_domain.rs). + `ScopeBlend::new(mode, opacity)` takes `ScopeBlendMode::{Normal, Multiply, Screen}` and `Option`. `mode()` and `opacity()` return those facts unchanged. `None` means opacity 1; `Some` reuses the finite, strictly diff --git a/crates/rframe/src/lib.rs b/crates/rframe/src/lib.rs index b3cb46ca..6161eee0 100644 --- a/crates/rframe/src/lib.rs +++ b/crates/rframe/src/lib.rs @@ -37,7 +37,10 @@ pub use frame::{ }; pub use mask::{Mask, MaskMode}; pub use path::{FillRule, PathCommand, PathData, PathDataError}; -pub use scope::{Scope, ScopeBlend, ScopeBlendMode, ScopeEffect, ScopeOpacity, ScopeOpacityError}; +pub use scope::{ + BlendSourceDomain, BlendSourceDomainError, Scope, ScopeBlend, ScopeBlendMode, ScopeEffect, + ScopeOpacity, ScopeOpacityError, +}; pub use stroke::{ Stroke, StrokeCap, StrokeDash, StrokeDashError, StrokeDashIntervals, StrokeDashIntervalsError, StrokeError, StrokeJoin, StrokeSpace, diff --git a/crates/rframe/src/scope.rs b/crates/rframe/src/scope.rs index df8813d2..ee70fa87 100644 --- a/crates/rframe/src/scope.rs +++ b/crates/rframe/src/scope.rs @@ -20,6 +20,8 @@ use crate::clip::ClipPath; use crate::filter::Filter; use crate::frame::VisualRef; +use math2::Rectangle; +use math2::transform::AffineTransform; /// Why an opacity cannot be a scope fact. /// @@ -79,6 +81,140 @@ pub enum ScopeBlendMode { Screen, } +/// Why a complete blend-source domain cannot cross the resolved contract. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlendSourceDomainError { + /// The local rectangle is non-finite, empty, or has unrepresentable endpoints. + InvalidRectangle, + /// The map is non-finite or has no supported finite inverse. + InvalidTransform, + /// Mapped corners or their enclosing rectangle are non-finite or collapse. + InvalidMappedBounds, +} + +impl std::fmt::Display for BlendSourceDomainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::InvalidRectangle => { + "a blend-source domain must have finite local bounds with positive extents and ordered endpoints" + } + Self::InvalidTransform => { + "a blend-source domain map must have finite members and a supported finite inverse" + } + Self::InvalidMappedBounds => { + "a blend-source domain must map to finite, strictly positive bounds" + } + }) + } +} + +impl std::error::Error for BlendSourceDomainError {} + +/// The complete, already-enclosed local domain of one isolated blend source. +/// +/// The producer has finished discovering contributions and enclosing them in +/// source-local coordinates. Materialize the isolated source over this domain +/// against transparent black, then apply the owning [`ScopeBlend`]'s final +/// opacity and blend. The domain includes the producer's resolved non-painted +/// extent contributions; it is neither a tight geometry box nor a supplemental +/// margin. Reconstructing it from visible paints, enlarging it conservatively, +/// or enclosing contributors after mapping states a different source. +/// +/// `source_to_stream` maps the already-enclosed rectangle into its containing +/// [`crate::FrameItems`] coordinates: frame space for a frame, tile-local space +/// for a repeating program. Child node transforms keep their existing meaning; +/// this mapping is not an inherited transform. Each nested blend boundary owns +/// its own declaration and completed source; it does not donate its descendants +/// as fresh geometry to an enclosing boundary. +/// +/// This fact supplies no paint and introduces no geometric clip. Output clipping +/// stays a separate operation. It carries no host view, device grid, allocation, +/// or raster policy. Current-view mapping and device enclosure remain execution +/// work. A consumer unable to honor a declaration must refuse it, not ignore it +/// or replace its map with identity. +/// +/// Construction checks numerical usability only. It neither performs enclosure +/// nor proves that the producer's declaration is complete. There is no integer +/// coordinate requirement: the producer has resolved the enclosure in its own +/// source space, whose unit need not be a device pixel. Empty domains are not +/// admitted; absence is represented by [`ScopeBlend::source_domain`] returning +/// `None`, which makes no completeness assertion. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BlendSourceDomain { + rect: Rectangle, + source_to_stream: AffineTransform, +} + +impl BlendSourceDomain { + /// Check an already-enclosed rectangle and retain both supplied facts exactly. + /// + /// Local endpoints must be finite and strictly ordered in `f32`. The map + /// must have a finite determinant and an inverse supported by + /// [`AffineTransform::inverse`] (including its small-determinant refusal); + /// every inverse member must also be finite. Mapped corners and their + /// enclosing rectangle must remain finite and strictly positive in `f32`. + /// These checks make no promise about a later host view. + pub fn new( + rect: Rectangle, + source_to_stream: AffineTransform, + ) -> Result { + if !valid_domain_rectangle(rect) { + return Err(BlendSourceDomainError::InvalidRectangle); + } + let [[a, c, _], [b, d, _]] = source_to_stream.matrix; + if !source_to_stream + .matrix + .into_iter() + .flatten() + .all(f32::is_finite) + || !(a * d - b * c).is_finite() + || !source_to_stream + .inverse() + .is_some_and(|inverse| inverse.matrix.into_iter().flatten().all(f32::is_finite)) + { + return Err(BlendSourceDomainError::InvalidTransform); + } + let corners = rect + .corners() + .map(|point| math2::vector2::transform(point, &source_to_stream)); + // Check every corner before bounding: min/max can otherwise hide NaN. + if !corners.into_iter().flatten().all(f32::is_finite) + || !valid_domain_rectangle(Rectangle::from_points(&corners)) + { + return Err(BlendSourceDomainError::InvalidMappedBounds); + } + Ok(Self { + rect, + source_to_stream, + }) + } + + /// The complete already-enclosed rectangle in source-local coordinates. + #[must_use] + pub const fn rect(self) -> Rectangle { + self.rect + } + + /// The exact source-local to containing-stream map, without a host view. + #[must_use] + pub const fn source_to_stream(self) -> AffineTransform { + self.source_to_stream + } +} + +fn valid_domain_rectangle(rect: Rectangle) -> bool { + rect.x.is_finite() + && rect.y.is_finite() + && rect.width.is_finite() + && rect.height.is_finite() + && rect.width > 0.0 + && rect.height > 0.0 + && (rect.x + rect.width).is_finite() + && (rect.y + rect.height).is_finite() + && rect.x + rect.width > rect.x + && rect.y + rect.height > rect.y +} + /// One isolated group's combined final blend and optional opacity. /// /// Children paint in order against transparent black. Their completed @@ -102,6 +238,12 @@ pub enum ScopeBlendMode { /// This names visual meaning, never a layer allocation, backdrop copy, cache /// policy, or authored group. /// +/// An optional [`BlendSourceDomain`] states the complete source domain before +/// this final operation. Absence preserves the existing group meaning without +/// asserting completeness. A domain neither creates another scope nor makes an +/// empty group meaningful, and equality of domains does not erase the backdrop +/// dependency. +/// /// ``` /// use rframe::{ScopeBlend, ScopeBlendMode, ScopeOpacity}; /// @@ -118,6 +260,7 @@ pub enum ScopeBlendMode { pub struct ScopeBlend { mode: ScopeBlendMode, opacity: Option, + source_domain: Option, } impl ScopeBlend { @@ -125,7 +268,24 @@ impl ScopeBlend { /// `None` means opacity 1; it never means absence of isolation. #[must_use] pub const fn new(mode: ScopeBlendMode, opacity: Option) -> Self { - Self { mode, opacity } + Self { + mode, + opacity, + source_domain: None, + } + } + + /// Declare the complete source domain without changing the final operation. + #[must_use] + pub const fn with_source_domain(mut self, domain: BlendSourceDomain) -> Self { + self.source_domain = Some(domain); + self + } + + /// The declared complete source domain, or no completeness assertion. + #[must_use] + pub const fn source_domain(self) -> Option { + self.source_domain } /// The blend function used only when the completed group joins its backdrop. diff --git a/crates/rframe/tests/blend_source_domain.rs b/crates/rframe/tests/blend_source_domain.rs new file mode 100644 index 00000000..4d1d159e --- /dev/null +++ b/crates/rframe/tests/blend_source_domain.rs @@ -0,0 +1,445 @@ +//! Producer-only laws for complete, already-enclosed blend-source domains. +//! +//! An illustration producer states paint and source enclosure independently. +//! These tests establish representation and refusal laws, not raster results. + +use std::sync::Arc; + +use cg::{CGColor, GradientStop, LinearGradientPaint, Paint, Paints}; +use math2::Rectangle; +use math2::transform::AffineTransform; +use rframe::{ + BlendSourceDomain, BlendSourceDomainError, ClipEdgeMode, ClipGeometry, ClipLayer, ClipPath, + Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, + MAX_PATTERN_DEPTH, MAX_SCOPE_DEPTH, Mask, MaskMode, PaintAlphaFactor, PaintStack, PatternPaint, + PatternPaintError, Provenance, Scope, ScopeBlend, ScopeBlendMode, ScopeEffect, ScopeOpacity, + Stroke, StrokeCap, StrokeJoin, VisualRef, +}; + +fn owner(id: u64) -> VisualRef { + VisualRef::new(Identity::new(id), Provenance::new(id + 100)) +} + +fn rect() -> Rectangle { + Rectangle::from_xywh(4.0, 6.0, 12.0, 8.0) +} + +fn domain() -> BlendSourceDomain { + BlendSourceDomain::new( + Rectangle::from_xywh(0.0, 0.0, 24.0, 24.0), + AffineTransform::identity(), + ) + .unwrap() +} + +fn node(id: u64, paints: PaintStack) -> FrameItem { + FrameItem::Node(FrameNode { + owner: owner(id), + transform: AffineTransform::identity(), + geometry: Geometry::Rect(rect()), + bounds: rect(), + paints, + stroke: None, + }) +} + +fn begin(id: u64, source: BlendSourceDomain) -> FrameItem { + FrameItem::ScopeBegin(Scope { + owner: owner(id), + effect: ScopeEffect::Blend( + ScopeBlend::new(ScopeBlendMode::Normal, None).with_source_domain(source), + ), + }) +} + +fn illustration(source: Option) -> Frame { + let mut blend = ScopeBlend::new( + ScopeBlendMode::Screen, + Some(ScopeOpacity::new(0.375).unwrap()), + ); + if let Some(source) = source { + blend = blend.with_source_domain(source); + } + let ramp = + PaintStack::try_from_paints(Paints::new([Paint::LinearGradient(LinearGradientPaint { + stops: vec![ + GradientStop { + offset: 0.0, + color: CGColor::RED.into(), + }, + GradientStop { + offset: 1.0, + color: CGColor::BLUE.into(), + }, + ], + ..Default::default() + })])) + .unwrap(); + Frame { + owner: owner(1), + bounds: Rectangle::from_xywh(0.0, 0.0, 64.0, 64.0), + items: FrameItems::try_new(vec![ + FrameItem::ScopeBegin(Scope { + owner: owner(2), + effect: ScopeEffect::Blend(blend), + }), + node(3, ramp), + node(4, PaintStack::solid(CGColor::from_rgba(40, 80, 160, 128))), + FrameItem::ScopeEnd, + ]) + .unwrap(), + } +} + +/// Attaching a domain adds no boundary and changes neither final operation. +#[test] +fn absence_makes_no_completeness_assertion_and_attachment_preserves_blend_and_opacity() { + for mode in [ + ScopeBlendMode::Normal, + ScopeBlendMode::Multiply, + ScopeBlendMode::Screen, + ] { + for opacity in [None, Some(ScopeOpacity::new(0.375).unwrap())] { + let ordinary = ScopeBlend::new(mode, opacity); + let declared = ordinary.with_source_domain(domain()); + assert_eq!(ordinary.source_domain(), None); + assert_eq!(declared.source_domain(), Some(domain())); + assert_eq!(declared.mode(), ordinary.mode()); + assert_eq!(declared.opacity(), ordinary.opacity()); + assert_ne!(declared, ordinary); + } + } +} + +/// A resolved non-painted extent can distinguish two otherwise identical +/// illustration products without becoming geometry, a paint, or a clip item. +#[test] +fn an_independent_illustration_changes_domain_without_changing_painted_nodes() { + let ordinary = illustration(None); + let enclosed = illustration(Some(domain())); + let wider = illustration(Some( + BlendSourceDomain::new( + Rectangle::from_xywh(-2.0, -2.0, 28.0, 28.0), + AffineTransform::identity(), + ) + .unwrap(), + )); + for frame in [&enclosed, &wider] { + assert_eq!(frame.owner, ordinary.owner); + assert_eq!(frame.bounds, ordinary.bounds); + assert_eq!(frame.nodes(), ordinary.nodes()); + assert_eq!(frame.items.len(), ordinary.items.len()); + assert_eq!( + frame.nodes().iter().map(|n| n.paints.len()).sum::(), + 2 + ); + assert!(frame.nodes().iter().all(|n| n.stroke.is_none())); + assert_ne!(frame, &ordinary); + } + assert_ne!(enclosed, wider); + assert_eq!(enclosed, illustration(Some(domain()))); +} + +/// Enclosure is already resolved in the producer's units. The contract does +/// not round fractional inputs or canonicalize local placement into the map. +#[test] +fn construction_preserves_the_declared_rectangle_and_map_exactly() { + let local = Rectangle::from_xywh(-0.0, -0.5, 10.25, 8.75); + let map = AffineTransform::from_acebdf(-1.0, 0.25, 25.5, 0.125, 2.0, -0.25); + let source = BlendSourceDomain::new(local, map).unwrap(); + let stored = source.rect(); + assert_eq!( + [stored.x, stored.y, stored.width, stored.height].map(f32::to_bits), + [local.x, local.y, local.width, local.height].map(f32::to_bits) + ); + assert_eq!( + source + .source_to_stream() + .matrix + .map(|row| row.map(f32::to_bits)), + map.matrix.map(|row| row.map(f32::to_bits)) + ); +} + +/// Mapping may hide distinctions in where the source was enclosed. A final +/// enclosing box cannot replace either of the two carried facts. +#[test] +fn equal_final_boxes_do_not_identify_different_local_domains() { + let first = BlendSourceDomain::new( + Rectangle::from_xywh(0.0, 0.0, 4.0, 2.0), + AffineTransform::identity(), + ) + .unwrap(); + let second = BlendSourceDomain::new( + Rectangle::from_xywh(0.0, 0.0, 2.0, 4.0), + AffineTransform::from_acebdf(0.0, 1.0, 0.0, 1.0, 0.0, 0.0), + ) + .unwrap(); + assert_eq!( + math2::rect_transform(first.rect(), &first.source_to_stream()), + math2::rect_transform(second.rect(), &second.source_to_stream()) + ); + assert_ne!(first, second); +} + +/// Enclosing two diagonal unit squares before a shear yields a different +/// domain than enclosing them afterwards. Only the former is declared here. +#[test] +fn the_declared_enclosure_precedes_its_map() { + let shear = AffineTransform::from_acebdf(1.0, -1.0, 0.0, 0.0, 1.0, 0.0); + let source = BlendSourceDomain::new(Rectangle::from_xywh(0.0, 0.0, 3.0, 3.0), shear).unwrap(); + assert_eq!(source.rect(), Rectangle::from_xywh(0.0, 0.0, 3.0, 3.0)); + let mapped = math2::rect_transform(source.rect(), &source.source_to_stream()); + assert_eq!(mapped, Rectangle::from_xywh(-3.0, 0.0, 6.0, 3.0)); + let individually_enclosed = Rectangle::from_xywh(-1.0, 0.0, 2.0, 3.0); + assert_ne!(mapped, individually_enclosed); +} + +/// An empty rectangle is not an empty-source API. Finite widths also need +/// ordered representable endpoints rather than overflowing or collapsing. +#[test] +fn empty_nonfinite_and_unrepresentable_local_rectangles_are_refused() { + let map = AffineTransform::identity(); + for rectangle in [ + Rectangle::from_xywh(0.0, 0.0, 0.0, 1.0), + Rectangle::from_xywh(0.0, 0.0, 1.0, -0.0), + Rectangle::from_xywh(0.0, 0.0, -1.0, 1.0), + Rectangle::from_xywh(0.0, 0.0, 1.0, -1.0), + Rectangle::from_xywh(f32::MAX, 0.0, f32::MAX, 1.0), + Rectangle::from_xywh(0.0, f32::MAX, 1.0, f32::MAX), + Rectangle::from_xywh(1.0, 0.0, f32::from_bits(1), 1.0), + Rectangle::from_xywh(0.0, 1.0, 1.0, f32::from_bits(1)), + ] { + assert_eq!( + BlendSourceDomain::new(rectangle, map), + Err(BlendSourceDomainError::InvalidRectangle) + ); + } + for index in 0..4 { + for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let mut members = [0.0, 0.0, 1.0, 1.0]; + members[index] = value; + let [x, y, width, height] = members; + assert_eq!( + BlendSourceDomain::new(Rectangle::from_xywh(x, y, width, height), map), + Err(BlendSourceDomainError::InvalidRectangle) + ); + } + } +} + +/// A finite forward matrix is insufficient when inverse arithmetic is +/// unsupported, overflows, or loses its determinant to non-finite arithmetic. +#[test] +fn unusable_coordinate_maps_are_refused_without_an_identity_fallback() { + for map in [ + AffineTransform::from_acebdf(1.0, 2.0, 0.0, 2.0, 4.0, 0.0), + AffineTransform::from_acebdf(f32::EPSILON / 2.0, 0.0, 0.0, 0.0, 1.0, 0.0), + AffineTransform::from_acebdf(f32::MAX, 0.0, 0.0, 0.0, 2.0, 0.0), + AffineTransform::from_acebdf(f32::MAX, f32::MAX, 0.0, f32::MAX, f32::MAX, 0.0), + AffineTransform::from_acebdf(0.5, 0.0, f32::MAX, 0.0, 1.0, 0.0), + ] { + assert_eq!( + BlendSourceDomain::new(rect(), map), + Err(BlendSourceDomainError::InvalidTransform) + ); + } + for row in 0..2 { + for column in 0..3 { + for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + let mut map = AffineTransform::identity(); + map.matrix[row][column] = value; + assert_eq!( + BlendSourceDomain::new(rect(), map), + Err(BlendSourceDomainError::InvalidTransform) + ); + } + } + } +} + +/// Bounding must not hide a non-finite corner, an overflowing span between +/// finite corners, or a positive domain collapsed by finite translation. +#[test] +fn mapped_corners_and_their_enclosing_rectangle_must_remain_representable() { + for (rectangle, map) in [ + ( + Rectangle::from_xywh(0.0, 0.0, f32::MAX, 1.0), + AffineTransform::from_acebdf(2.0, 0.0, 0.0, 0.0, 1.0, 0.0), + ), + ( + Rectangle::from_xywh(-f32::MAX / 2.0, 0.0, f32::MAX, 1.0), + AffineTransform::from_acebdf(1.5, 0.0, 0.0, 0.0, 1.0, 0.0), + ), + ( + Rectangle::from_xywh(0.0, 0.0, 2.0, 2.0), + AffineTransform::from_acebdf(f32::MAX, -f32::MAX, 0.0, 1.0, -0.5, 0.0), + ), + ( + rect(), + AffineTransform::from_acebdf(1.0, 0.0, 1e20, 0.0, 1.0, 1e20), + ), + ] { + assert_eq!( + BlendSourceDomain::new(rectangle, map), + Err(BlendSourceDomainError::InvalidMappedBounds) + ); + } +} + +/// The numerical boundary imposes neither a minimum pixel nor a maximum +/// allocation size. Neither belongs to source-local units. +#[test] +fn representable_domains_have_no_device_size_limit() { + for size in [f32::from_bits(1), f32::MIN_POSITIVE, 1.0, f32::MAX] { + let rectangle = Rectangle::from_xywh(0.0, 0.0, size, size); + let source = BlendSourceDomain::new(rectangle, AffineTransform::identity()).unwrap(); + assert_eq!(source.rect(), rectangle); + } +} + +/// Extent is not paint and cannot satisfy stream content validation. +#[test] +fn a_declared_domain_creates_neither_paint_nor_an_empty_group_exception() { + let zero = PaintAlphaFactor::new(0.0).unwrap(); + assert!( + PaintStack::solid(CGColor::RED) + .with_alpha_factor(zero) + .is_empty() + ); + assert_eq!( + Stroke::new( + PaintStack::solid(CGColor::TRANSPARENT), + 8.0, + StrokeCap::Butt, + StrokeJoin::Miter, + 4.0, + ), + Ok(None) + ); + assert_eq!( + FrameItems::try_new(vec![begin(1, domain()), FrameItem::ScopeEnd]), + Err(FrameItemsError::EmptyScope { index: 0 }) + ); +} + +/// One stream owns completion and order. The source map does not rebase +/// children, collapse clip scopes into isolation, or combine mask phases. +#[test] +fn nested_domains_preserve_owners_maps_nodes_clips_and_mask_phases() { + let clip = ClipPath::new_with_edge_mode( + vec![ + ClipLayer::new(vec![ + ClipGeometry::new(AffineTransform::identity(), Geometry::Rect(domain().rect())) + .unwrap(), + ]) + .unwrap(), + ], + ClipEdgeMode::Hard, + ) + .unwrap(); + let child = BlendSourceDomain::new( + Rectangle::from_xywh(0.0, 0.0, 20.0, 20.0), + AffineTransform::from_acebdf(1.0, 0.0, 2.0, 0.0, 1.0, 3.0), + ) + .unwrap(); + let items = vec![ + begin(1, domain()), + FrameItem::ScopeBegin(Scope { + owner: owner(2), + effect: ScopeEffect::Clip(clip.clone()), + }), + FrameItem::MaskBegin(Mask::new(owner(3), MaskMode::Alpha, clip)), + node(4, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + begin(5, child), + node(6, PaintStack::solid(CGColor::WHITE)), + FrameItem::ScopeEnd, + FrameItem::MaskEnd, + FrameItem::ScopeEnd, + FrameItem::ScopeEnd, + ]; + let checked = FrameItems::try_new(items.clone()).unwrap(); + assert_eq!(checked.iter().cloned().collect::>(), items); + assert_eq!( + checked.nodes().map(|n| n.owner).collect::>(), + [owner(4), owner(6)] + ); + assert!( + checked + .nodes() + .all(|n| n.transform == AffineTransform::identity()) + ); + + assert_eq!( + FrameItems::try_new(vec![ + FrameItem::MaskBegin(Mask::new( + owner(1), + MaskMode::Alpha, + ClipPath::new(vec![ClipLayer::new(Vec::new()).unwrap()]).unwrap(), + )), + begin(2, child), + node(3, PaintStack::solid(CGColor::RED)), + FrameItem::MaskSource, + ]), + Err(FrameItemsError::UnexpectedMaskSource { index: 3 }) + ); +} + +/// The declaration adds no structural nesting and earns no additional depth. +#[test] +fn domains_preserve_the_existing_scope_depth_bound() { + let begins = || { + (0..MAX_SCOPE_DEPTH) + .map(|id| begin(id as u64, domain())) + .collect::>() + }; + let mut maximum = begins(); + maximum.push(node(100, PaintStack::solid(CGColor::RED))); + maximum.extend(std::iter::repeat_n(FrameItem::ScopeEnd, MAX_SCOPE_DEPTH)); + FrameItems::try_new(maximum).unwrap(); + let mut overflow = begins(); + overflow.push(begin(100, domain())); + assert_eq!( + FrameItems::try_new(overflow), + Err(FrameItemsError::ScopeTooDeep { + index: MAX_SCOPE_DEPTH + }) + ); +} + +fn repeating_illustration(paints: PaintStack) -> Result { + let items = Arc::new( + FrameItems::try_new(vec![ + begin(1, domain()), + node(2, paints), + FrameItem::ScopeEnd, + ]) + .unwrap(), + ); + let pattern = PatternPaint::new( + 24.0, + 24.0, + AffineTransform::from_acebdf(2.0, 0.0, 12.0, 0.0, 2.0, 16.0), + Arc::clone(&items), + 1.0, + )?; + assert!(Arc::ptr_eq(pattern.items(), &items)); + assert_eq!(pattern.items().iter().next(), Some(&begin(1, domain()))); + Ok(pattern) +} + +/// Program placement never rewrites a contained source-to-tile declaration, +/// and declarations cannot hide recursive programs from the depth check. +#[test] +fn repeating_programs_keep_tile_local_domains_immutable_and_recursion_bounded() { + let mut pattern = repeating_illustration(PaintStack::solid(CGColor::RED)).unwrap(); + for expected in 2..=MAX_PATTERN_DEPTH { + pattern = repeating_illustration(PaintStack::from_pattern(pattern)).unwrap(); + assert_eq!(pattern.depth(), expected); + } + assert_eq!( + repeating_illustration(PaintStack::from_pattern(pattern)), + Err(PatternPaintError::TooDeep) + ); +} diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index 43c4213b..3da66b62 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -3425,6 +3425,19 @@ fn compile_svg_element( } let has_root_blend_source = adds_root_blend_boundary || (root_facts.has_blend && root_patrol.opacity > 0.0 && root_patrol.opacity < 1.0); + // Only compile_child consumes complete source domains. Do not let its + // omitted-stroke exemption reach the separately materialized root source. + // Even a bare ramp with no omitted stroke exposes a different root raster + // profile under authored non-normal blending. A full-canvas background + // masking that difference does not establish the general admission. + if (has_root_blend_source && root_facts.needs_source_domain) + || (root_facts.has_linear_source + && root_composite.is_some_and(|scope| scope.mode() != ScopeBlendMode::Normal)) + { + return Err(blend_linear_source_refusal( + "with an unproved root linear source", + )); + } if has_root_blend_source && let Some(reason) = blend_linear_source_boundary(root_facts) { return Err(blend_linear_source_refusal(reason)); } @@ -3542,6 +3555,11 @@ struct SpanFacts { has_linear_source: bool, /// Contributor facts that cannot be recovered from the resolved drawlist. linear_source_boundary: Option<&'static str>, + /// Complete local drawable enclosures for the untransformed rectangle + /// profile. These are separate from painted geometry and opacity passes. + source_domain_bounds: Option, + source_domain_incomplete: bool, + needs_source_domain: bool, } impl SpanFacts { @@ -3560,6 +3578,12 @@ impl SpanFacts { .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); + self.source_domain_bounds = match (self.source_domain_bounds, other.source_domain_bounds) { + (Some(left), Some(right)) => Some(math2::union(&[left, right])), + (left, right) => left.or(right), + }; + self.source_domain_incomplete |= other.source_domain_incomplete; + self.needs_source_domain |= other.needs_source_domain; } } @@ -3567,6 +3591,9 @@ fn blend_linear_source_boundary(facts: SpanFacts) -> Option<&'static str> { if !facts.has_linear_source { return None; } + if facts.needs_source_domain && facts.source_domain_incomplete { + return Some("with an unrepresented rectangular source domain"); + } facts.linear_source_boundary.or_else(|| { (facts.transformed || facts.has_scope || facts.has_opacity) .then_some("with a transformed or nested linear-gradient source") @@ -3585,6 +3612,58 @@ fn blend_precision_refusal(reason: &str) -> CompileError { )) } +/// The bounded rectangle's local visual enclosure, before source composition. +/// Blink encloses its local decorated box before effect-space mapping. The +/// untransformed profile retains that resolved domain without inventing paint. +/// The integer envelope is restricted so both endpoints and unions remain +/// exactly representable in f32; wider arithmetic retains the named patrol. +fn blend_rect_source_domain(node: &FrameNode, omitted_width: Option) -> Option { + let Geometry::Rect(rect) = node.geometry else { + return None; + }; + if node.transform != AffineTransform::identity() + || node.paints.pattern().is_some() + || blend_node_precision_boundary(node).is_some() + || node + .stroke + .as_ref() + .is_some_and(|stroke| stroke.paints().pattern().is_some()) + || rect.width <= 0.0 + || rect.height <= 0.0 + { + return None; + } + let width = omitted_width + .or_else(|| node.stroke.as_ref().map(Stroke::width)) + .unwrap_or(0.0); + let half = width / 2.0; + let x = rect.x - half; + let y = rect.y - half; + let right = x + (rect.width + width); + let bottom = y + (rect.height + width); + if [x, y, right, bottom] + .into_iter() + .any(|v| !v.is_finite() || v.abs() > 8_388_608.0) + { + return None; + } + let (x, y, right, bottom) = (x.floor(), y.floor(), right.ceil(), bottom.ceil()); + // The consumer's complete-domain contract also requires enclosure of the + // painted rectangle. f32 decoration can round an endpoint inward across + // an integer even when the authored numbers themselves are unaliased. + // Keep that conservative boundary attributable here, not a late renderer + // error that best-effort could no longer roll back by SVG owner path. + let painted_half = f64::from(node.stroke.as_ref().map_or(0.0, Stroke::width)) / 2.0; + if f64::from(rect.x) - painted_half < f64::from(x) + || f64::from(rect.y) - painted_half < f64::from(y) + || f64::from(rect.x) + f64::from(rect.width) + painted_half > f64::from(right) + || f64::from(rect.y) + f64::from(rect.height) + painted_half > f64::from(bottom) + { + return None; + } + (right > x && bottom > y).then(|| Rectangle::from_xywh(x, y, right - x, bottom - y)) +} + fn blend_node_precision_boundary(node: &FrameNode) -> Option<&'static str> { if node.paints.is_empty() && node.stroke.is_none() { return None; @@ -5665,6 +5744,17 @@ impl<'a> ChildWalk<'a> { self.elide_blend(blend_id); return Ok(facts); } + if facts.needs_source_domain { + let bounds = facts.source_domain_bounds.ok_or_else(|| { + blend_linear_source_refusal("without a complete rectangular source domain") + })?; + let domain = rframe::BlendSourceDomain::new(bounds, AffineTransform::identity()) + .map_err(|_| blend_linear_source_refusal("with an unrepresentable rectangular source domain"))?; + let FrameItem::ScopeBegin(scope) = &mut self.items[checkpoint.0] else { + unreachable!("the blend boundary precedes its content") + }; + scope.effect = ScopeEffect::Blend(composite.with_source_domain(domain)); + } facts.draws = 0; facts.opacity_passes = 0; facts.has_scope = true; @@ -5675,6 +5765,7 @@ impl<'a> ChildWalk<'a> { // it with a ramp rasterized directly into the parent. facts.has_linear_source = false; facts.linear_source_boundary = None; + facts.needs_source_domain = false; } } Ok(facts) @@ -6535,7 +6626,30 @@ impl<'a> ChildWalk<'a> { .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 { + // Only a live linear fill with a fully resolved local solid stroke + // omission earns this domain. Missing/context servers and non-painted + // siblings keep their existing, independently named boundary. + let retained_omission = outcome.omitted_local_stroke_width.is_some() + && facts.has_linear_source + && outcome.draws > 0 + && outcome.nodes.len() == 1 + && blend_rect_source_domain(&outcome.nodes[0], outcome.omitted_local_stroke_width) + .is_some(); + facts.needs_source_domain = retained_omission; + for node in &outcome.nodes { + if outcome.draws > 0 { + match blend_rect_source_domain(node, outcome.omitted_local_stroke_width) { + Some(bounds) => { + facts.source_domain_bounds = Some(match facts.source_domain_bounds { + Some(prior) => math2::union(&[prior, bounds]), + None => bounds, + }) + } + None => facts.source_domain_incomplete = true, + } + } + } + facts.linear_source_boundary = if outcome.omitted_stroke_extent && !retained_omission { Some("with a non-painted stroke extent") } else if outcome.has_geometry && outcome.draws == 0 { Some("with a non-painted source contributor") @@ -11669,6 +11783,7 @@ fn compile_tspan_text( has_geometry: true, transformed: false, omitted_stroke_extent: false, + omitted_local_stroke_width: None, })); } let one_pass_fold = (replay_opacity < 1.0 && paths.len() == 1).then_some(replay_opacity); @@ -11716,6 +11831,7 @@ fn compile_tspan_text( has_geometry: true, transformed: false, omitted_stroke_extent: false, + omitted_local_stroke_width: None, })) } @@ -12408,6 +12524,7 @@ struct ShapeOutcome { /// A selected stroke can enlarge Chromium's drawable bounds without a /// paint pass. Keep this separate from opacity-fold participation. omitted_stroke_extent: bool, + omitted_local_stroke_width: Option, /// The shape's own opacity composites fill and stroke through one /// isolated layer — the walk wraps the node in a scope. scope_opacity: Option, @@ -12772,6 +12889,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; + let omitted_local_stroke_width = resolved_stroke.omitted_local_width; patrol_mixed_contour_cap(&geometry, stroke.as_ref())?; debug_assert!( @@ -12810,6 +12928,7 @@ fn shape_node( has_geometry: true, transformed: false, omitted_stroke_extent, + omitted_local_stroke_width, }); } if opacity < 1.0 && has_geometry { @@ -12881,6 +13000,7 @@ fn shape_node( draws, opacity_passes, omitted_stroke_extent, + omitted_local_stroke_width, scope_opacity, has_opacity, has_geometry, @@ -14126,6 +14246,9 @@ struct StrokeResolution { /// 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, + /// A resolved local solid stroke's width remains a drawable contribution + /// even when zero alpha normalizes its paint to absence. + omitted_local_width: Option, } /// Blink's `markerUnits="strokeWidth"` scale for a non-scaling-stroke client. @@ -14252,6 +14375,7 @@ impl StrokeResolution { stroke: None, opacity_pass: false, omitted_extent: false, + omitted_local_width: None, } } } @@ -14275,6 +14399,7 @@ fn resolve_stroke( // 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); + let computed_stroke_is_color = matches!(style.clone_stroke().kind, SVGPaintKind::Color(_)); // Direct colours, valid paint servers, and invalid-reference fallbacks // stage element opacity exactly as [`resolve_fill`] describes. @@ -14295,6 +14420,7 @@ fn resolve_stroke( stroke: None, opacity_pass: false, omitted_extent: !computed_stroke_is_none, + omitted_local_width: None, }); }; let owner_data = selected @@ -14318,6 +14444,7 @@ fn resolve_stroke( stroke: None, opacity_pass: false, omitted_extent: !computed_stroke_is_none, + omitted_local_width: None, }); } SVGPaintKind::Color(ref color) => { @@ -14363,6 +14490,7 @@ fn resolve_stroke( stroke: None, opacity_pass: false, omitted_extent: true, + omitted_local_width: None, }); } @@ -14377,6 +14505,11 @@ fn resolve_stroke( stroke: None, opacity_pass: true, omitted_extent: true, + // A previously inert vector-effect grammar is not newly admitted: + // an unproved spelling simply leaves the composition patrol intact. + omitted_local_width: (computed_stroke_is_color + && matches!(resolve_vector_effect_space(el), Ok(StrokeSpace::Local))) + .then_some(width), }); } @@ -14538,6 +14671,7 @@ fn resolve_stroke( stroke, opacity_pass: true, omitted_extent: false, + omitted_local_width: None, }) } diff --git a/crates/websem/tests/svg_blending.rs b/crates/websem/tests/svg_blending.rs index 8dfb84dc..6e6e74a0 100644 --- a/crates/websem/tests/svg_blending.rs +++ b/crates/websem/tests/svg_blending.rs @@ -37,8 +37,11 @@ fn linear_source_extent_patrol_is_transactional_and_names_the_owner() { 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'/>"), + RAMP_RECT.replace( + "/>", + " stroke='transparent' stroke-width='4' vector-effect='non-scaling-stroke'/>", + ), + RAMP_RECT.replace("/>", " stroke='transparent' stroke-width='100000000'/>"), format!( "{}", RAMP_RECT.replace("/>", " stroke='url(#empty)' stroke-width='4'/>") @@ -91,6 +94,176 @@ fn linear_source_extent_patrol_is_transactional_and_names_the_owner() { } } +#[test] +fn omitted_solid_stroke_changes_only_the_complete_source_domain() { + use math2::{Rectangle, transform::AffineTransform}; + let source = |attrs: &str| { + svg(&format!( + "{RAMP}{}", + RAMP_RECT.replace("/>", &format!(" {attrs}/>")) + )) + }; + let ordinary = compile_standalone_svg( + &source("stroke='none' stroke-width='4'"), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + for (attrs, expected) in [ + ( + "stroke='transparent' stroke-width='4'", + Rectangle::from_xywh(6.0, 10.0, 43.0, 34.0), + ), + ( + "stroke='red' stroke-opacity='0' stroke-width='4'", + Rectangle::from_xywh(6.0, 10.0, 43.0, 34.0), + ), + ( + "stroke='transparent' stroke-width='8'", + Rectangle::from_xywh(4.0, 8.0, 47.0, 38.0), + ), + ] { + let source = source(attrs); + let strict = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)).unwrap(); + let best = SvgFrameSource::from_standalone_svg_best_effort( + source.as_str(), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!(strict, best.base_frame()); + assert!( + !best + .degradations() + .iter() + .any(|d| d.action() == websem::DegradationAction::Skipped) + ); + assert_eq!(strict.owner, ordinary.owner); + assert_eq!(strict.bounds, ordinary.bounds); + assert_eq!( + strict.nodes(), + ordinary.nodes(), + "no fake stroke, paint, or geometry bounds" + ); + assert_ne!( + strict, ordinary, + "the former full-Frame collapse is impossible" + ); + let mut domains = Vec::new(); + let without_domain = strict + .items + .iter() + .cloned() + .map(|item| match item { + FrameItem::ScopeBegin(mut scope) => { + if let ScopeEffect::Blend(blend) = scope.effect { + if let Some(domain) = blend.source_domain() { + domains.push(domain); + } + scope.effect = + ScopeEffect::Blend(ScopeBlend::new(blend.mode(), blend.opacity())); + } + FrameItem::ScopeBegin(scope) + } + item => item, + }) + .collect(); + assert_eq!( + rframe::FrameItems::try_new(without_domain).unwrap(), + ordinary.items + ); + assert_eq!(domains.len(), 1, "root isolation makes no declaration"); + assert_eq!(domains[0].rect(), expected); + assert_eq!(domains[0].source_to_stream(), AffineTransform::identity()); + } + let zero = compile_standalone_svg( + &source("stroke='transparent' stroke-width='0'"), + InitialViewport::new(64.0, 64.0), + ) + .unwrap(); + assert_eq!(zero, ordinary, "zero width contributes no source extent"); +} + +#[test] +fn source_domain_is_complete_order_independent_and_absent_on_elided_isolation() { + let omitted = RAMP_RECT.replace("/>", " stroke='transparent' stroke-width='4'/>"); + let sibling = ""; + for content in [ + format!("{omitted}{sibling}"), + format!("{sibling}{omitted}"), + ] { + let result = frame(&format!( + "{RAMP}{content}" + )); + let domain = blends(&result) + .into_iter() + .find_map(|blend| blend.source_domain()) + .unwrap(); + assert_eq!( + domain.rect(), + math2::Rectangle::from_xywh(3.0, 5.0, 46.0, 39.0) + ); + } + for content in [ + omitted.clone(), + format!("{omitted}"), + ] { + assert!(blends(&frame(&format!("{RAMP}{content}"))).is_empty()); + } + let result = frame(&format!( + "{RAMP}{omitted}" + )); + assert_eq!( + blends(&result) + .into_iter() + .filter(|blend| blend.source_domain().is_some()) + .count(), + 1 + ); +} + +#[test] +fn an_omitted_stroke_cannot_make_an_unknown_sibling_domain_complete() { + let source = svg(&format!( + "{RAMP}{}{RECT}", + RAMP_RECT.replace("/>", " stroke='transparent' stroke-width='4'/>") + )); + let error = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(error.contains("linear-gradient source-extent"), "{error}"); + 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); + assert!(best.degradations().iter().any(|d| d.path() == "svg/g[1]" && d.reason().contains("linear-gradient source-extent"))); +} + +#[test] +fn source_enclosure_rounding_refuses_at_the_svg_owner_before_consumer_preflight() { + let narrow = RAMP_RECT + .replace("width='38.2'", "width='.7'") + .replace("/>", " stroke='transparent' stroke-width='.0000001'/>"); + for mode in ["multiply", "screen"] { + let source = svg(&format!( + "{RAMP}{narrow}{RECT}" + )); + let error = compile_standalone_svg(&source, InitialViewport::new(64.0, 64.0)) + .unwrap_err() + .to_string(); + assert!(error.contains("linear-gradient source-extent"), "{error}"); + 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); + assert!(best.degradations().iter().any( + |d| d.path() == "svg/g[1]" && d.reason().contains("linear-gradient source-extent") + )); + } +} + #[test] fn root_linear_source_extent_refusal_cannot_silently_fall_back() { let base = svg(&format!( @@ -115,6 +288,49 @@ fn root_linear_source_extent_refusal_cannot_silently_fall_back() { } } +#[test] +fn root_linear_blend_sources_cannot_borrow_the_child_domain_exemption() { + for (style, stroke) in [ + ("mix-blend-mode:multiply", "none"), + ("mix-blend-mode:screen", "none"), + ("mix-blend-mode:multiply", "transparent"), + ("mix-blend-mode:screen", "transparent"), + ("isolation:isolate", "transparent"), + ] { + for backdrop in ["", ""] { + let source = svg(&format!( + "{RAMP}{backdrop}{}", + RAMP_RECT.replace("/>", &format!(" stroke='{stroke}' stroke-width='4'/>")) + )) + .replacen(" 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. +> The [B2b source-domain rung](./svg-engine-of-record.md#b2b-complete-blend-source-domains) +> now carries a resolved invisible solid stroke's source contribution beside a +> live rectangular linear fill. Its forty-four exact cells remove two bounded +> refusals; wider source spaces and blend values still prevent a tick. ### CSS fonts diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 7b0404ac..bcc0097a 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,423 primitive cells plus 16 sampled frames, those twenty-four text -witnesses, and 325 named refusal rows. Pixel cells use byte equality: what each +estate is 1,467 primitive cells plus 16 sampled frames, those twenty-four text +witnesses, and 329 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,7 +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-source-{multiply,screen}-{fractional,zeroalpha,integer,width-one,width-fractional,pixel-boundary,edge,sibling,sibling-reversed,opacity-half,opacity-near,outer-opacity,leaf,nested-neutral}.svg` | Twenty-eight exact source-domain cells: a live linear fill retains the source contribution of a transparent or zero-alpha solid stroke. Fractional placement, width changes, source union/order, combined versus outer opacity and boundary ownership exercise the complete enclosure without adding visible stroke paint. | +| `svg-group-blend-source-multiply-{dashed,bevel,width-px,currentcolor,normal-elided}.svg` · `svg-group-blend-source-screen-{round,width-percent,inherited}.svg` · `svg-group-blend-source-{multiply,screen}-{two-ramps,painted-stroke,outer-clip,use}.svg` | Sixteen exact adjacent controls: alternate omitted-stroke spellings, an elided redundant isolation, multiple drawable enclosures, a live stroke contributor, ancestor clipping and unpositioned local instances. The [source-domain evidence](../../docs/wg/consolidation/svg-engine-of-record.md#b2b-complete-blend-source-domains) separates these admissions from the retained source-space refusals. | +| `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. The newer source-domain cells carry resolved transparent solid strokes; unresolved/context omissions remain separately refused. | | `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. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 0368ec65..7dc10b48 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 (1423) +## Chromium-baked cells (1467) Cells are checked against their committed Chromium oracles using exact bytes unless a manifest entry declares a measured, bounded @@ -732,6 +732,50 @@ to its fixture source. No new image is committed for this view. svg-group-blend-screen-opacity-small-partial svg-group-blend-screen-stroke svg-group-blend-screen-transparent +svg-group-blend-source-multiply-bevel +svg-group-blend-source-multiply-currentcolor +svg-group-blend-source-multiply-dashed +svg-group-blend-source-multiply-edge +svg-group-blend-source-multiply-fractional +svg-group-blend-source-multiply-integer +svg-group-blend-source-multiply-leaf +svg-group-blend-source-multiply-nested-neutral +svg-group-blend-source-multiply-normal-elided +svg-group-blend-source-multiply-opacity-half +svg-group-blend-source-multiply-opacity-near +svg-group-blend-source-multiply-outer-clip +svg-group-blend-source-multiply-outer-opacity +svg-group-blend-source-multiply-painted-stroke +svg-group-blend-source-multiply-pixel-boundary +svg-group-blend-source-multiply-sibling +svg-group-blend-source-multiply-sibling-reversed +svg-group-blend-source-multiply-two-ramps +svg-group-blend-source-multiply-use +svg-group-blend-source-multiply-width-fractional +svg-group-blend-source-multiply-width-one +svg-group-blend-source-multiply-width-px +svg-group-blend-source-multiply-zeroalpha +svg-group-blend-source-screen-edge +svg-group-blend-source-screen-fractional +svg-group-blend-source-screen-inherited +svg-group-blend-source-screen-integer +svg-group-blend-source-screen-leaf +svg-group-blend-source-screen-nested-neutral +svg-group-blend-source-screen-opacity-half +svg-group-blend-source-screen-opacity-near +svg-group-blend-source-screen-outer-clip +svg-group-blend-source-screen-outer-opacity +svg-group-blend-source-screen-painted-stroke +svg-group-blend-source-screen-pixel-boundary +svg-group-blend-source-screen-round +svg-group-blend-source-screen-sibling +svg-group-blend-source-screen-sibling-reversed +svg-group-blend-source-screen-two-ramps +svg-group-blend-source-screen-use +svg-group-blend-source-screen-width-fractional +svg-group-blend-source-screen-width-one +svg-group-blend-source-screen-width-percent +svg-group-blend-source-screen-zeroalpha svg-group-blend-translate svg-group-blend-translate-isolated svg-group-inherited-fill @@ -1451,7 +1495,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (325) +## The refusal register (329) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1584,9 +1628,7 @@ its row into the cells above. | `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 | @@ -1616,6 +1658,12 @@ its row into the cells above. | `svg-group-blend-root-filter-sibling` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with a filter or mask needs its own image-effect composition profile | | `svg-group-blend-root-opacity` | **both refuse** | unsupported computed style: mix-blend-mode with partial opacity on the root crosses the root-layer precision boundary | | `svg-group-blend-source-clip` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with curved, subpixel, or rotated clip coverage crosses the group-source precision boundary | +| `svg-group-blend-source-domain-enclosure` | 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-source-domain-non-scaling` | 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-source-domain-pattern-sibling` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with an unrepresented rectangular source domain needs the linear-gradient source-extent profile | +| `svg-group-blend-source-domain-range` | 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-source-domain-root-bare` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with an unproved root linear source needs the linear-gradient source-extent profile | +| `svg-group-blend-source-domain-root-transparent` | **both refuse** | unsupported computed style: mix-blend-mode/isolation with an unproved root linear source needs the linear-gradient source-extent profile | | `svg-group-blend-source-ellipse` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | | `svg-group-blend-source-isolation` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | | `svg-group-blend-source-path` | declared | skipped svg/g[1]: unsupported computed style: mix-blend-mode/isolation with non-rectangular source geometry crosses the group-source precision boundary | diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-bevel.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-bevel.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-bevel.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-currentcolor.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-currentcolor.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-currentcolor.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-dashed.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-dashed.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-dashed.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-edge.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-edge.png new file mode 100644 index 00000000..9510fe66 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-edge.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-fractional.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-fractional.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-integer.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-integer.png new file mode 100644 index 00000000..181025f6 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-integer.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-leaf.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-leaf.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-leaf.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-nested-neutral.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-nested-neutral.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-nested-neutral.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-normal-elided.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-normal-elided.png new file mode 100644 index 00000000..70ec02e2 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-normal-elided.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-half.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-half.png new file mode 100644 index 00000000..c269b6c4 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-half.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-near.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-near.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-opacity-near.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-clip.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-clip.png new file mode 100644 index 00000000..0cd64d3a Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-clip.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-opacity.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-opacity.png new file mode 100644 index 00000000..012ec176 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-outer-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-painted-stroke.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-painted-stroke.png new file mode 100644 index 00000000..6acbcdd4 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-painted-stroke.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-pixel-boundary.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-pixel-boundary.png new file mode 100644 index 00000000..3eb3b247 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-pixel-boundary.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling-reversed.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling-reversed.png new file mode 100644 index 00000000..53a631aa Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling-reversed.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling.png new file mode 100644 index 00000000..6793f068 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-sibling.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-two-ramps.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-two-ramps.png new file mode 100644 index 00000000..ff815459 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-two-ramps.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-use.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-use.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-use.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-fractional.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-fractional.png new file mode 100644 index 00000000..6f916390 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-one.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-one.png new file mode 100644 index 00000000..a214b6d4 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-one.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-px.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-px.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-width-px.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-multiply-zeroalpha.png b/fixtures/web-first/chromium/svg-group-blend-source-multiply-zeroalpha.png new file mode 100644 index 00000000..de5227ee Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-multiply-zeroalpha.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-edge.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-edge.png new file mode 100644 index 00000000..912b478d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-edge.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-fractional.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-fractional.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-inherited.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-inherited.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-inherited.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-integer.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-integer.png new file mode 100644 index 00000000..81800dbf Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-integer.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-leaf.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-leaf.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-leaf.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-nested-neutral.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-nested-neutral.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-nested-neutral.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-half.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-half.png new file mode 100644 index 00000000..2e8d1ea8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-half.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-near.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-near.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-opacity-near.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-clip.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-clip.png new file mode 100644 index 00000000..0cd64d3a Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-clip.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-opacity.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-opacity.png new file mode 100644 index 00000000..012ec176 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-outer-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-painted-stroke.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-painted-stroke.png new file mode 100644 index 00000000..1df3b211 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-painted-stroke.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-pixel-boundary.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-pixel-boundary.png new file mode 100644 index 00000000..4750853a Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-pixel-boundary.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-round.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-round.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-round.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling-reversed.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling-reversed.png new file mode 100644 index 00000000..1478dbce Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling-reversed.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling.png new file mode 100644 index 00000000..125d421b Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-sibling.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-two-ramps.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-two-ramps.png new file mode 100644 index 00000000..9b6e31c0 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-two-ramps.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-use.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-use.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-use.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-width-fractional.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-fractional.png new file mode 100644 index 00000000..c7c867ea Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-fractional.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-width-one.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-one.png new file mode 100644 index 00000000..015765d5 Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-one.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-width-percent.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-percent.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-width-percent.png differ diff --git a/fixtures/web-first/chromium/svg-group-blend-source-screen-zeroalpha.png b/fixtures/web-first/chromium/svg-group-blend-source-screen-zeroalpha.png new file mode 100644 index 00000000..b9e5502d Binary files /dev/null and b/fixtures/web-first/chromium/svg-group-blend-source-screen-zeroalpha.png differ diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index 15fa8f24..fc9acf8b 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": "29e60218917c4e34c0b6be81178e8524b0d856c7f85863049e1b1d393c926694", + "suite_sha256": "55efa526c68d14a8595087f50ab0361a0653be68fdd038e4db7f9fffbdea275b", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -6364,6 +6364,402 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-source-multiply-bevel", + "source": "svg-group-blend-source-multiply-bevel.svg", + "source_sha256": "374d4b7df3ee88bec05310b39a34d0d4a6ddb7a32e3a7debe436c0bdfde40765", + "oracle": "chromium/svg-group-blend-source-multiply-bevel.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-currentcolor", + "source": "svg-group-blend-source-multiply-currentcolor.svg", + "source_sha256": "1b4bcf9f6075515e8eee669be49b0dc15b3810ecba55316b24d0a6145ed25682", + "oracle": "chromium/svg-group-blend-source-multiply-currentcolor.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-dashed", + "source": "svg-group-blend-source-multiply-dashed.svg", + "source_sha256": "0358e390ea60a6950df29c0475d4840b6f68af46be8bbb1d167008de51f2467d", + "oracle": "chromium/svg-group-blend-source-multiply-dashed.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-edge", + "source": "svg-group-blend-source-multiply-edge.svg", + "source_sha256": "59b03a3bde3d602f2c7801fa7c37bd6d16ca8daab51a80d697534f5cd1bd22de", + "oracle": "chromium/svg-group-blend-source-multiply-edge.png", + "oracle_sha256": "4279cbc6df2b354ad146bcd0cf29ebaebfc1bece0c72698f29744f4fad283582", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-fractional", + "source": "svg-group-blend-source-multiply-fractional.svg", + "source_sha256": "e45a52ba5b1721b287e6ab36400905c7803650c82708e16c02b405b3a11ffd4d", + "oracle": "chromium/svg-group-blend-source-multiply-fractional.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-integer", + "source": "svg-group-blend-source-multiply-integer.svg", + "source_sha256": "8803b493f45a611e3688cbd8ded2ebcae94234b23e6cfa82991cc675dd76b190", + "oracle": "chromium/svg-group-blend-source-multiply-integer.png", + "oracle_sha256": "8002086d3d82f65bfdd4538d8393f907a44f26a8752e1a4bedfb17eba070627a", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-leaf", + "source": "svg-group-blend-source-multiply-leaf.svg", + "source_sha256": "bbe8a896ad3448745f1a4f88b62a498e744293eee12978c4b1989bc581e9c39c", + "oracle": "chromium/svg-group-blend-source-multiply-leaf.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-nested-neutral", + "source": "svg-group-blend-source-multiply-nested-neutral.svg", + "source_sha256": "fb04067ece3bc6ed3ccee1797521861570dddb64e635ab06556ce018beb01004", + "oracle": "chromium/svg-group-blend-source-multiply-nested-neutral.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-normal-elided", + "source": "svg-group-blend-source-multiply-normal-elided.svg", + "source_sha256": "d3f60a89cd40add8efdb6670fa282a1c503826be404cfeba512cb29a6d0dd552", + "oracle": "chromium/svg-group-blend-source-multiply-normal-elided.png", + "oracle_sha256": "8fd768559ce902ae57f2f30cfbd47b99c3cb1d0236a462e23c115877f0e47f4d", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-opacity-half", + "source": "svg-group-blend-source-multiply-opacity-half.svg", + "source_sha256": "c445550fabbf4e0e2a056c96b5d3f227bf058848c209d37a676441923b19f7e1", + "oracle": "chromium/svg-group-blend-source-multiply-opacity-half.png", + "oracle_sha256": "07c90bc7fba48eae9a5499758b8d62a01189b12669107aba19209b63df694f84", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-opacity-near", + "source": "svg-group-blend-source-multiply-opacity-near.svg", + "source_sha256": "169073df4bce1dfc38f54a8ba55040eba83b2543b7699e0d38aeb61fd41ec73c", + "oracle": "chromium/svg-group-blend-source-multiply-opacity-near.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-outer-clip", + "source": "svg-group-blend-source-multiply-outer-clip.svg", + "source_sha256": "74e116b4406b291870eff1c4db15a4815e74d5380076addb0f0c9bfe05592b5d", + "oracle": "chromium/svg-group-blend-source-multiply-outer-clip.png", + "oracle_sha256": "954d76357350ea1a04fd9b427d429707bfbc90c95f9afb3fc6702fc3b2bda166", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-outer-opacity", + "source": "svg-group-blend-source-multiply-outer-opacity.svg", + "source_sha256": "f9c5c8c7d956bdfc10f8143e5ba67bffb860142e072a2bb08b802d86e04e21da", + "oracle": "chromium/svg-group-blend-source-multiply-outer-opacity.png", + "oracle_sha256": "722e52ca22f8a8cb9c6dbd06b70f0caa51d80e7cb9d15fced7fdc874ff2cb33d", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-painted-stroke", + "source": "svg-group-blend-source-multiply-painted-stroke.svg", + "source_sha256": "f3e18400d1f89840453b4bd811ec9bacf355c949115c407d99cad5e6887ee034", + "oracle": "chromium/svg-group-blend-source-multiply-painted-stroke.png", + "oracle_sha256": "0009ffb47ea5923152a73fc87a63843938cb2457da7bddfcc985a20cd533e1b5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-pixel-boundary", + "source": "svg-group-blend-source-multiply-pixel-boundary.svg", + "source_sha256": "7439687ceaa4e20bd5e35da23efe29b09467de7d7201f164353ff78ef6faad77", + "oracle": "chromium/svg-group-blend-source-multiply-pixel-boundary.png", + "oracle_sha256": "d68f366cb43f4375dcf382c9679ecd06df43e8ea8cc77f8300cb80f11ab13c3b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-sibling", + "source": "svg-group-blend-source-multiply-sibling.svg", + "source_sha256": "96f7897bec2498a6c063fe943d1934998dbe8a15e704ebcc23a0caf7a429b7c7", + "oracle": "chromium/svg-group-blend-source-multiply-sibling.png", + "oracle_sha256": "fce33f93cd01657f85e7e2dc35a85a8de787971fa73f136d8c9cf37c75677eb0", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-sibling-reversed", + "source": "svg-group-blend-source-multiply-sibling-reversed.svg", + "source_sha256": "0f1eb9ad67782200de4ac0392d2513986d775e2d7cbe19338da8376f803a108d", + "oracle": "chromium/svg-group-blend-source-multiply-sibling-reversed.png", + "oracle_sha256": "d0790a91796e1347cd80b8b4b24db43aee6dbbdec3c0a0f105470bea52cb478b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-two-ramps", + "source": "svg-group-blend-source-multiply-two-ramps.svg", + "source_sha256": "004b1f11f87f29aea9cee97b0d307d03c0b38598decd5c5370d6a6ab18b16e57", + "oracle": "chromium/svg-group-blend-source-multiply-two-ramps.png", + "oracle_sha256": "88e1a460e2d33899c88e0841f798257d3842acef70fe8f447f8fc14ea5a4d8c5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-use", + "source": "svg-group-blend-source-multiply-use.svg", + "source_sha256": "757c500153829f23ba3af15547837e15ca55679b9264ac9821089591e9d7a11a", + "oracle": "chromium/svg-group-blend-source-multiply-use.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-fractional", + "source": "svg-group-blend-source-multiply-width-fractional.svg", + "source_sha256": "5d87671b5e31a620ce2a097c38b2560d4ee242b544ab9839ffd29be289362850", + "oracle": "chromium/svg-group-blend-source-multiply-width-fractional.png", + "oracle_sha256": "e2d97912e11850ab8a20e2be635ca79b212b412a5371c90715f8b17b734deeaf", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-one", + "source": "svg-group-blend-source-multiply-width-one.svg", + "source_sha256": "021d390a39e1c965495e5e6b0093d3dad91a89eb52b90fbffd96e71ac169b260", + "oracle": "chromium/svg-group-blend-source-multiply-width-one.png", + "oracle_sha256": "52c594327cd7c50c7b2adee72abbce1ef62a15da9a829e7f95e8a01991734ec5", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-px", + "source": "svg-group-blend-source-multiply-width-px.svg", + "source_sha256": "af2d07de7effe8a5e9ce0ba5b99dc9f32b0c30d560650fa6071b1e51065463fc", + "oracle": "chromium/svg-group-blend-source-multiply-width-px.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-zeroalpha", + "source": "svg-group-blend-source-multiply-zeroalpha.svg", + "source_sha256": "c403461bddfd2f208186af56b80cf867bd7fec48d5c4e91ca0667baa9a5d4170", + "oracle": "chromium/svg-group-blend-source-multiply-zeroalpha.png", + "oracle_sha256": "485f9a51950e3b21e8922c23877a300554b75906cc0ded35365bd51e09bfba9b", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-edge", + "source": "svg-group-blend-source-screen-edge.svg", + "source_sha256": "5090e5a2895ad4a0614373c7276a55a1b08ac4092aaaaa8660905881a2805794", + "oracle": "chromium/svg-group-blend-source-screen-edge.png", + "oracle_sha256": "a0b221dd9d3f2d73d577aa160eb3d706fa59d4e5327c21627bff7912e5969fe7", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-fractional", + "source": "svg-group-blend-source-screen-fractional.svg", + "source_sha256": "bf5242b53c4df6c17c86a24643f6957005869d5ed4c2cb44a837e6f69452951b", + "oracle": "chromium/svg-group-blend-source-screen-fractional.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-inherited", + "source": "svg-group-blend-source-screen-inherited.svg", + "source_sha256": "35c7cc1dedf685be6a85d96047831f5d8d4cb03316b131d72b7a01d070aeed01", + "oracle": "chromium/svg-group-blend-source-screen-inherited.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-integer", + "source": "svg-group-blend-source-screen-integer.svg", + "source_sha256": "e2118f27dfd41b3901d1dab68d7cdf45ec7c8d0aad231116d170b3338a1b982b", + "oracle": "chromium/svg-group-blend-source-screen-integer.png", + "oracle_sha256": "22c92563ae005164b5d44aeb3ffd12d30c240564b82f9c2d51d22cf91decaf32", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-leaf", + "source": "svg-group-blend-source-screen-leaf.svg", + "source_sha256": "a6c7a5c5e9e864735644d900a924cfe773ef730db5ae783cf6eacb83d2d2c8e8", + "oracle": "chromium/svg-group-blend-source-screen-leaf.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-nested-neutral", + "source": "svg-group-blend-source-screen-nested-neutral.svg", + "source_sha256": "e2cbb4b1df14c8f29d165979bc911e41526983b58defd5800248b0e97885f2e7", + "oracle": "chromium/svg-group-blend-source-screen-nested-neutral.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-opacity-half", + "source": "svg-group-blend-source-screen-opacity-half.svg", + "source_sha256": "c597c90ba7bdd002c27079555392cee547970b8c1070d5551da94997a4a0ace2", + "oracle": "chromium/svg-group-blend-source-screen-opacity-half.png", + "oracle_sha256": "7aa59d2ee1918b2f73c9ccfde468dd0ef2182a74e30c062a4d3da7f60ea69435", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-opacity-near", + "source": "svg-group-blend-source-screen-opacity-near.svg", + "source_sha256": "c3931c340a3f06a034f684ee9f44a000ecbc1753b509bd0c7ef92f07de4f99d9", + "oracle": "chromium/svg-group-blend-source-screen-opacity-near.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-outer-clip", + "source": "svg-group-blend-source-screen-outer-clip.svg", + "source_sha256": "6db25591b8acbf3c0a44a8012879343495bf3a2235e1022e32f8b96d7e474152", + "oracle": "chromium/svg-group-blend-source-screen-outer-clip.png", + "oracle_sha256": "954d76357350ea1a04fd9b427d429707bfbc90c95f9afb3fc6702fc3b2bda166", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-outer-opacity", + "source": "svg-group-blend-source-screen-outer-opacity.svg", + "source_sha256": "ce463e1b9d8ec8ee4550c3cefc2ba815a495fb7cd10994e73d17cad8268a02f2", + "oracle": "chromium/svg-group-blend-source-screen-outer-opacity.png", + "oracle_sha256": "722e52ca22f8a8cb9c6dbd06b70f0caa51d80e7cb9d15fced7fdc874ff2cb33d", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-painted-stroke", + "source": "svg-group-blend-source-screen-painted-stroke.svg", + "source_sha256": "8f3bf55e74c055fd6c859917d3af8e7f6395858a40ac45618dc992d325a8c717", + "oracle": "chromium/svg-group-blend-source-screen-painted-stroke.png", + "oracle_sha256": "a80bee95742eccf1fad8dcf4b7130b55ef772546de7980ba4e8b5942d70c5c51", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-pixel-boundary", + "source": "svg-group-blend-source-screen-pixel-boundary.svg", + "source_sha256": "eb15a884c71f472d599857c74aa53d7c56fac72da17d3729362a484152f4b64f", + "oracle": "chromium/svg-group-blend-source-screen-pixel-boundary.png", + "oracle_sha256": "d93f0269b3c75372538bd27b53f19995dafe315751345b6e14f084bd0f93f4fd", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-round", + "source": "svg-group-blend-source-screen-round.svg", + "source_sha256": "e79fa07b527510a07d08a7be89b25a0e15ae396c2e4a449b7acf3a08966021a5", + "oracle": "chromium/svg-group-blend-source-screen-round.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-sibling", + "source": "svg-group-blend-source-screen-sibling.svg", + "source_sha256": "f4898051b87f35280a31b9707fcdda85e21a013673f997dca0e7e3f51a710f91", + "oracle": "chromium/svg-group-blend-source-screen-sibling.png", + "oracle_sha256": "50ca2deb7a0f5babdb3793f2d3786aa60cf4bdca1f093ff24f6dbf56f9db93b0", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-sibling-reversed", + "source": "svg-group-blend-source-screen-sibling-reversed.svg", + "source_sha256": "b99707d2d555e176d06be3c7a0b32ac883155d9f93563891d401d97c14ad9003", + "oracle": "chromium/svg-group-blend-source-screen-sibling-reversed.png", + "oracle_sha256": "656f76cdb15f5eab80163ebd79029853e62725dad1f592fe6f887fe3ba259140", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-two-ramps", + "source": "svg-group-blend-source-screen-two-ramps.svg", + "source_sha256": "a8030bf6cb06ebd2d7905b7d6b77bbe0690221bceb4573994da3f94077f57154", + "oracle": "chromium/svg-group-blend-source-screen-two-ramps.png", + "oracle_sha256": "41fb3b027012e022ef24a19431cf3cbd1c8bbb855046dede428bf7934d80c667", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-use", + "source": "svg-group-blend-source-screen-use.svg", + "source_sha256": "5e69b3912da97a57f1afe9c60581ebce6513d01a6afe90f5d9df17d3bbd24500", + "oracle": "chromium/svg-group-blend-source-screen-use.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-fractional", + "source": "svg-group-blend-source-screen-width-fractional.svg", + "source_sha256": "1146feb3098e2c59e944a5c6bc2a13da24982cf501c5e089c0515d38841023c2", + "oracle": "chromium/svg-group-blend-source-screen-width-fractional.png", + "oracle_sha256": "8dc3b719c3080763e53b64a940dfe8a813d1d9d25a2780e230f42dc33668df45", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-one", + "source": "svg-group-blend-source-screen-width-one.svg", + "source_sha256": "f80863b3f8e3afb3b5aeae59bf0a5a46ea458dfad0ea0eca2de080c9c9c0fd0c", + "oracle": "chromium/svg-group-blend-source-screen-width-one.png", + "oracle_sha256": "73cc57ac512434276655286e7bf0fd4ee4c3a4b4adbc394f5804eaf3e71a8bab", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-percent", + "source": "svg-group-blend-source-screen-width-percent.svg", + "source_sha256": "28550cc073ac3bb3f1a1d6eb0ef2037a4c2a464da88831404da1ae45abf59e3b", + "oracle": "chromium/svg-group-blend-source-screen-width-percent.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-zeroalpha", + "source": "svg-group-blend-source-screen-zeroalpha.svg", + "source_sha256": "705838962ecd75ec45e6938531e17af9d23b1e9f8129a94204226ea93c4437ad", + "oracle": "chromium/svg-group-blend-source-screen-zeroalpha.png", + "oracle_sha256": "facb046f0262098671f33936c27972390801dd8a96cd9eb5fe9f1c4fef40b9fe", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-translate", "source": "svg-group-blend-translate.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index 367f82ab..ca0c4a2b 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -5729,6 +5729,358 @@ "width": 64, "height": 64 }, + { + "id": "svg-group-blend-source-multiply-bevel", + "source": "svg-group-blend-source-multiply-bevel.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-bevel.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-currentcolor", + "source": "svg-group-blend-source-multiply-currentcolor.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-currentcolor.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-dashed", + "source": "svg-group-blend-source-multiply-dashed.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-dashed.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-edge", + "source": "svg-group-blend-source-multiply-edge.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-edge.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-fractional", + "source": "svg-group-blend-source-multiply-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-integer", + "source": "svg-group-blend-source-multiply-integer.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-integer.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-leaf", + "source": "svg-group-blend-source-multiply-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-nested-neutral", + "source": "svg-group-blend-source-multiply-nested-neutral.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-nested-neutral.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-normal-elided", + "source": "svg-group-blend-source-multiply-normal-elided.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-normal-elided.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-opacity-half", + "source": "svg-group-blend-source-multiply-opacity-half.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-opacity-half.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-opacity-near", + "source": "svg-group-blend-source-multiply-opacity-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-opacity-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-outer-clip", + "source": "svg-group-blend-source-multiply-outer-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-outer-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-outer-opacity", + "source": "svg-group-blend-source-multiply-outer-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-outer-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-painted-stroke", + "source": "svg-group-blend-source-multiply-painted-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-painted-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-pixel-boundary", + "source": "svg-group-blend-source-multiply-pixel-boundary.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-pixel-boundary.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-sibling", + "source": "svg-group-blend-source-multiply-sibling.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-sibling.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-sibling-reversed", + "source": "svg-group-blend-source-multiply-sibling-reversed.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-sibling-reversed.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-two-ramps", + "source": "svg-group-blend-source-multiply-two-ramps.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-two-ramps.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-use", + "source": "svg-group-blend-source-multiply-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-use.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-fractional", + "source": "svg-group-blend-source-multiply-width-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-width-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-one", + "source": "svg-group-blend-source-multiply-width-one.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-width-one.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-width-px", + "source": "svg-group-blend-source-multiply-width-px.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-width-px.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-multiply-zeroalpha", + "source": "svg-group-blend-source-multiply-zeroalpha.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-multiply-zeroalpha.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-edge", + "source": "svg-group-blend-source-screen-edge.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-edge.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-fractional", + "source": "svg-group-blend-source-screen-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-inherited", + "source": "svg-group-blend-source-screen-inherited.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-inherited.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-integer", + "source": "svg-group-blend-source-screen-integer.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-integer.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-leaf", + "source": "svg-group-blend-source-screen-leaf.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-leaf.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-nested-neutral", + "source": "svg-group-blend-source-screen-nested-neutral.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-nested-neutral.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-opacity-half", + "source": "svg-group-blend-source-screen-opacity-half.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-opacity-half.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-opacity-near", + "source": "svg-group-blend-source-screen-opacity-near.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-opacity-near.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-outer-clip", + "source": "svg-group-blend-source-screen-outer-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-outer-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-outer-opacity", + "source": "svg-group-blend-source-screen-outer-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-outer-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-painted-stroke", + "source": "svg-group-blend-source-screen-painted-stroke.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-painted-stroke.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-pixel-boundary", + "source": "svg-group-blend-source-screen-pixel-boundary.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-pixel-boundary.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-round", + "source": "svg-group-blend-source-screen-round.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-round.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-sibling", + "source": "svg-group-blend-source-screen-sibling.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-sibling.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-sibling-reversed", + "source": "svg-group-blend-source-screen-sibling-reversed.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-sibling-reversed.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-two-ramps", + "source": "svg-group-blend-source-screen-two-ramps.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-two-ramps.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-use", + "source": "svg-group-blend-source-screen-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-use.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-fractional", + "source": "svg-group-blend-source-screen-width-fractional.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-width-fractional.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-one", + "source": "svg-group-blend-source-screen-width-one.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-width-one.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-width-percent", + "source": "svg-group-blend-source-screen-width-percent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-width-percent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-group-blend-source-screen-zeroalpha", + "source": "svg-group-blend-source-screen-zeroalpha.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-group-blend-source-screen-zeroalpha.png", + "width": 64, + "height": 64 + }, { "id": "svg-group-blend-translate", "source": "svg-group-blend-translate.svg", diff --git a/fixtures/web-first/svg-group-blend-source-multiply-bevel.svg b/fixtures/web-first/svg-group-blend-source-multiply-bevel.svg new file mode 100644 index 00000000..ed5d31b6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-bevel.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-currentcolor.svg b/fixtures/web-first/svg-group-blend-source-multiply-currentcolor.svg new file mode 100644 index 00000000..8bd89742 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-currentcolor.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-dashed.svg b/fixtures/web-first/svg-group-blend-source-multiply-dashed.svg new file mode 100644 index 00000000..221fc22f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-dashed.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-edge.svg b/fixtures/web-first/svg-group-blend-source-multiply-edge.svg new file mode 100644 index 00000000..7054f36f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-edge.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-fractional.svg b/fixtures/web-first/svg-group-blend-source-multiply-fractional.svg new file mode 100644 index 00000000..9025e6d6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-integer.svg b/fixtures/web-first/svg-group-blend-source-multiply-integer.svg new file mode 100644 index 00000000..aa926009 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-integer.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-leaf.svg b/fixtures/web-first/svg-group-blend-source-multiply-leaf.svg new file mode 100644 index 00000000..a532ef45 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-nested-neutral.svg b/fixtures/web-first/svg-group-blend-source-multiply-nested-neutral.svg new file mode 100644 index 00000000..e0b4a35a --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-nested-neutral.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-normal-elided.svg b/fixtures/web-first/svg-group-blend-source-multiply-normal-elided.svg new file mode 100644 index 00000000..ba5a27af --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-normal-elided.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-opacity-half.svg b/fixtures/web-first/svg-group-blend-source-multiply-opacity-half.svg new file mode 100644 index 00000000..2309f3e0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-opacity-half.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-opacity-near.svg b/fixtures/web-first/svg-group-blend-source-multiply-opacity-near.svg new file mode 100644 index 00000000..9ef22505 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-opacity-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-outer-clip.svg b/fixtures/web-first/svg-group-blend-source-multiply-outer-clip.svg new file mode 100644 index 00000000..b47bfbb6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-outer-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-outer-opacity.svg b/fixtures/web-first/svg-group-blend-source-multiply-outer-opacity.svg new file mode 100644 index 00000000..c41ff4d7 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-outer-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-painted-stroke.svg b/fixtures/web-first/svg-group-blend-source-multiply-painted-stroke.svg new file mode 100644 index 00000000..8885f121 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-painted-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-pixel-boundary.svg b/fixtures/web-first/svg-group-blend-source-multiply-pixel-boundary.svg new file mode 100644 index 00000000..65e735b0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-pixel-boundary.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-sibling-reversed.svg b/fixtures/web-first/svg-group-blend-source-multiply-sibling-reversed.svg new file mode 100644 index 00000000..a5840ac3 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-sibling-reversed.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-sibling.svg b/fixtures/web-first/svg-group-blend-source-multiply-sibling.svg new file mode 100644 index 00000000..1f2835c7 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-two-ramps.svg b/fixtures/web-first/svg-group-blend-source-multiply-two-ramps.svg new file mode 100644 index 00000000..66bb8d51 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-two-ramps.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-use.svg b/fixtures/web-first/svg-group-blend-source-multiply-use.svg new file mode 100644 index 00000000..1a45e1c4 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-width-fractional.svg b/fixtures/web-first/svg-group-blend-source-multiply-width-fractional.svg new file mode 100644 index 00000000..518880dc --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-width-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-width-one.svg b/fixtures/web-first/svg-group-blend-source-multiply-width-one.svg new file mode 100644 index 00000000..3b0b6a56 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-width-one.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-width-px.svg b/fixtures/web-first/svg-group-blend-source-multiply-width-px.svg new file mode 100644 index 00000000..ca503ea1 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-width-px.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-multiply-zeroalpha.svg b/fixtures/web-first/svg-group-blend-source-multiply-zeroalpha.svg new file mode 100644 index 00000000..41eb9457 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-multiply-zeroalpha.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-edge.svg b/fixtures/web-first/svg-group-blend-source-screen-edge.svg new file mode 100644 index 00000000..000fe127 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-edge.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-fractional.svg b/fixtures/web-first/svg-group-blend-source-screen-fractional.svg new file mode 100644 index 00000000..e264eb14 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-inherited.svg b/fixtures/web-first/svg-group-blend-source-screen-inherited.svg new file mode 100644 index 00000000..568a6bea --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-inherited.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-integer.svg b/fixtures/web-first/svg-group-blend-source-screen-integer.svg new file mode 100644 index 00000000..20fb2ae0 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-integer.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-leaf.svg b/fixtures/web-first/svg-group-blend-source-screen-leaf.svg new file mode 100644 index 00000000..48bcd234 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-leaf.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-nested-neutral.svg b/fixtures/web-first/svg-group-blend-source-screen-nested-neutral.svg new file mode 100644 index 00000000..99769696 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-nested-neutral.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-opacity-half.svg b/fixtures/web-first/svg-group-blend-source-screen-opacity-half.svg new file mode 100644 index 00000000..2693e3cb --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-opacity-half.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-opacity-near.svg b/fixtures/web-first/svg-group-blend-source-screen-opacity-near.svg new file mode 100644 index 00000000..be990f34 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-opacity-near.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-outer-clip.svg b/fixtures/web-first/svg-group-blend-source-screen-outer-clip.svg new file mode 100644 index 00000000..5d8c6a87 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-outer-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-outer-opacity.svg b/fixtures/web-first/svg-group-blend-source-screen-outer-opacity.svg new file mode 100644 index 00000000..aedfd341 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-outer-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-painted-stroke.svg b/fixtures/web-first/svg-group-blend-source-screen-painted-stroke.svg new file mode 100644 index 00000000..d360689b --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-painted-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-pixel-boundary.svg b/fixtures/web-first/svg-group-blend-source-screen-pixel-boundary.svg new file mode 100644 index 00000000..dc6506b3 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-pixel-boundary.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-round.svg b/fixtures/web-first/svg-group-blend-source-screen-round.svg new file mode 100644 index 00000000..813954d6 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-round.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-sibling-reversed.svg b/fixtures/web-first/svg-group-blend-source-screen-sibling-reversed.svg new file mode 100644 index 00000000..68727fba --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-sibling-reversed.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-sibling.svg b/fixtures/web-first/svg-group-blend-source-screen-sibling.svg new file mode 100644 index 00000000..03b5b83f --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-two-ramps.svg b/fixtures/web-first/svg-group-blend-source-screen-two-ramps.svg new file mode 100644 index 00000000..745d23a5 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-two-ramps.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-use.svg b/fixtures/web-first/svg-group-blend-source-screen-use.svg new file mode 100644 index 00000000..948df921 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-width-fractional.svg b/fixtures/web-first/svg-group-blend-source-screen-width-fractional.svg new file mode 100644 index 00000000..c69a99bc --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-width-fractional.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-width-one.svg b/fixtures/web-first/svg-group-blend-source-screen-width-one.svg new file mode 100644 index 00000000..b98f9592 --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-width-one.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-width-percent.svg b/fixtures/web-first/svg-group-blend-source-screen-width-percent.svg new file mode 100644 index 00000000..b817fd9c --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-width-percent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-group-blend-source-screen-zeroalpha.svg b/fixtures/web-first/svg-group-blend-source-screen-zeroalpha.svg new file mode 100644 index 00000000..d3af596d --- /dev/null +++ b/fixtures/web-first/svg-group-blend-source-screen-zeroalpha.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index 63227ab2..2a8552e2 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -24,7 +24,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-stroke-{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. The former transparent/zero-alpha solid-stroke refusals are now exact source-domain cells. | +| `svg-group-blend-source-domain-{non-scaling,range,pattern-sibling}.svg` | The first complete source-domain profile excludes non-scaling invisible strokes, unrepresentable local enclosures and a repeating-program sibling whose source contribution is not represented by the rectangular domain. Each retains `linear-gradient source-extent` in strict and best-effort; these are conservative refusal witnesses, not claims of measured wrong pixels. | +| `svg-group-blend-source-domain-enclosure.svg` | f32 decoration of `x=8.3`, `width=.7`, `stroke-width=.0000001` rounds the far edge to 9, which does not enclose the exact sum of the resolved painted values. Name `linear-gradient source-extent` and roll back the SVG group before the glyphless consumer's stricter complete-domain preflight. The prototype failed late in both CLI admissions without producing pixels (measured, not celled as a positive case). | +| `svg-group-blend-source-domain-root-{transparent,bare}.svg` | The separately materialized outer root cannot borrow a child blend's complete-domain exemption. Transparent-stroke and bare root ramps under authored Multiply/Screen each silently differed from Chromium at 85 pixels, maximum delta 6, in both admissions; a full-canvas backdrop hid the difference. Both admissions now refuse the unproved root linear source by `linear-gradient source-extent` (measured, not celled as a positive case). | | `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. | 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 deleted file mode 100644 index 7156537d..00000000 --- a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-opacity-zero.svg +++ /dev/null @@ -1 +0,0 @@ - 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 deleted file mode 100644 index b6f612c4..00000000 --- a/fixtures/web-first/unsupported/svg-group-blend-linear-extent-stroke-transparent.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-enclosure.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-enclosure.svg new file mode 100644 index 00000000..9a708d80 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-enclosure.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-non-scaling.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-non-scaling.svg new file mode 100644 index 00000000..b81a6af3 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-non-scaling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-pattern-sibling.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-pattern-sibling.svg new file mode 100644 index 00000000..7ebd1ed9 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-pattern-sibling.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-range.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-range.svg new file mode 100644 index 00000000..fa884e41 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-range.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-bare.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-bare.svg new file mode 100644 index 00000000..c127c756 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-bare.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-transparent.svg b/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-transparent.svg new file mode 100644 index 00000000..cca80baa --- /dev/null +++ b/fixtures/web-first/unsupported/svg-group-blend-source-domain-root-transparent.svg @@ -0,0 +1 @@ +