diff --git a/crates/csscascade/src/dom.rs b/crates/csscascade/src/dom.rs index 88ec343e..bbabef9d 100644 --- a/crates/csscascade/src/dom.rs +++ b/crates/csscascade/src/dom.rs @@ -782,15 +782,15 @@ fn svg_presentation_hints( if name.ns != markup5ever::ns!(svg) { return None; } - // On a gradient element the transform property's presentation attribute - // is `gradientTransform`, and the plain `transform` attribute is inert - // (measured: it changes no pixel in Chromium). Both spellings share one - // grammar and one measured rewrite; the computed value is applied about - // the raw origin in gradient space, identically for the attribute and an - // author `transform` declaration (measured with non-quarter rotations and - // scales — byte-identical). + // On a paint-server element the transform property's presentation + // attribute has the resource-specific spelling: `gradientTransform` or + // `patternTransform`. The plain `transform` attribute is inert on both + // families (measured in Chromium), while an author `transform` + // declaration still wins through the ordinary cascade. The special + // attributes share the transform grammar and measured rewrite below. let transform_attribute = match name.local.as_ref() { "linearGradient" | "radialGradient" => "gradientTransform", + "pattern" => "patternTransform", _ => "transform", }; let mut block = PropertyDeclarationBlock::new(); diff --git a/crates/csscascade/tests/svg_presentation_hints.rs b/crates/csscascade/tests/svg_presentation_hints.rs index 01c64f80..a38bfcb6 100644 --- a/crates/csscascade/tests/svg_presentation_hints.rs +++ b/crates/csscascade/tests/svg_presentation_hints.rs @@ -34,6 +34,7 @@ const STANDALONE: &str = r##" @@ -96,6 +97,11 @@ const STANDALONE: &str = r##"X + + + + "##; #[test] @@ -426,6 +432,25 @@ fn standalone_svg_presentation_hints_enter_below_author_rules() { ), "none" ); + // Patterns use the sibling resource spelling. It enters the same + // transform longhand below author rules and style attributes; the plain + // transform attribute remains inert on the resource element. + assert_eq!( + property(root, "pattern-transform-hint", LonghandId::Transform), + "translate(10px, 10px)" + ); + assert_eq!( + property(root, "pattern-plain-transform-inert", LonghandId::Transform), + "none" + ); + assert_eq!( + property(root, "pattern-rule-beats-hint", LonghandId::Transform), + "translate(30px)" + ); + assert_eq!( + property(root, "pattern-style-beats-hint", LonghandId::Transform), + "translate(40px)" + ); } #[test] diff --git a/crates/n0/src/drawlist.rs b/crates/n0/src/drawlist.rs index 76ba8ad2..eec8025e 100644 --- a/crates/n0/src/drawlist.rs +++ b/crates/n0/src/drawlist.rs @@ -251,6 +251,31 @@ impl GlyphlessOwnerSlot { } } +/// One private source-neutral repeating vector program. +/// +/// The nested drawlist is already compiled and preflighted. Its frame clip is +/// the tile cell `(0, 0, width, height)`; `transform` maps that tile-local +/// coordinate system into the consuming geometry's local space before both +/// axes repeat. +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvedPattern { + pub(crate) width: f32, + pub(crate) height: f32, + pub(crate) transform: Affine, + pub(crate) program: Arc>, + pub(crate) opacity: f32, +} + +/// Absolute geometry in the consuming node's local coordinate system. +/// Pattern items keep that origin in geometry instead of folding it into +/// `world`, because the pattern mapping is stated in the same local space. +#[derive(Debug, Clone, PartialEq)] +pub enum ResolvedPatternGeometry { + Rect { x: f32, y: f32, w: f32, h: f32 }, + Oval { x: f32, y: f32, w: f32, h: f32 }, + Path(Arc), +} + /// One canonical, source-neutral dash phase carried by private stroke /// material. /// @@ -410,6 +435,20 @@ pub enum ItemKind { filter: Arc, }, EndFilter, + /// Fill absolute local geometry through one checked repeat program. + PatternFill { + geometry: ResolvedPatternGeometry, + pattern: Arc, + post_paint_opacity: PostPaintOpacity, + }, + /// Stroke absolute local geometry through one checked repeat program. + PatternStroke { + geometry: ResolvedPatternGeometry, + pattern: Arc, + stroke: Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + }, RectFill { w: f32, h: f32, diff --git a/crates/n0/src/frame.rs b/crates/n0/src/frame.rs index 8fa13992..5cd5e4ea 100644 --- a/crates/n0/src/frame.rs +++ b/crates/n0/src/frame.rs @@ -139,6 +139,7 @@ impl From for FrameBuildError { pub enum FrameExecutionError { Environment(PaintEnvironmentMismatch), Image(crate::paint::ImagePreflightError), + Pattern(crate::paint::PatternPreflightError), } impl std::fmt::Display for FrameExecutionError { @@ -146,6 +147,7 @@ impl std::fmt::Display for FrameExecutionError { match self { FrameExecutionError::Environment(error) => error.fmt(f), FrameExecutionError::Image(error) => error.fmt(f), + FrameExecutionError::Pattern(error) => error.fmt(f), } } } @@ -164,6 +166,12 @@ impl From for FrameExecutionError { } } +impl From for FrameExecutionError { + fn from(error: crate::paint::PatternPreflightError) -> Self { + FrameExecutionError::Pattern(error) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum FrameError { Build(FrameBuildError), diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index 26e51327..1b3cd224 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -2,10 +2,10 @@ //! //! [`rframe::Frame`] is the backend-free resolved contract. It carries no //! authored n0 document, HTML/CSS/SVG syntax, parser binding, backend object, -//! I/O handle, or clock. This module admits its current solid- and -//! gradient-filled rectangle, ellipse, and path slice plus checked opacity, -//! clip, mask, and image-filter effects, compiles them into n0's one private -//! drawlist, and executes them through n0's one private painter. +//! I/O handle, or clock. This module admits its current solid-, gradient-, and +//! resolved-pattern-painted rectangle, ellipse, and path slice plus checked +//! opacity, clip, mask, and image-filter effects, compiles them into n0's one +//! private drawlist, and executes them through n0's one private painter. //! //! The resulting [`FrameProduct`] is intentionally separate from //! [`crate::frame::FrameProduct`]. The latter owns an n0-model @@ -35,7 +35,8 @@ use crate::drawlist::{ ResolvedFilterBlend, ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterLightSource, ResolvedFilterMorphology, ResolvedFilterNode, - ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, StrokeDashPhase, + ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, ResolvedMaskMode, ResolvedPattern, + ResolvedPatternGeometry, StrokeDashPhase, }; use crate::frame::FrameExecutionError; use crate::paint::PaintCtx; @@ -153,8 +154,10 @@ impl std::error::Error for BuildError {} /// One immutable source-neutral frame, its private compiled material, and its /// opaque provenance projection. /// -/// The admitted solid/geometry slice is resource-free, so this product neither -/// captures nor checks a [`crate::paint::PaintEnvironmentKey`]. +/// Every admitted paint is already resolved and carries no resource handle, so +/// this product neither captures nor checks a +/// [`crate::paint::PaintEnvironmentKey`]. A repeating vector program is nested +/// immutable draw material, not a late resource lookup. #[derive(Debug, Clone)] pub struct FrameProduct { resolved: Frame, @@ -176,6 +179,7 @@ impl FrameProduct { ctx: &PaintCtx, ) -> Result<(), FrameExecutionError> { self.assert_provenance_complete(); + crate::paint::preflight_patterns(&self.drawlist, ctx)?; crate::paint::execute_unchecked(canvas, &self.drawlist, &to_affine(*view), ctx); Ok(()) } @@ -190,6 +194,7 @@ impl FrameProduct { ctx: &PaintCtx, ) -> Result, FrameExecutionError> { self.assert_provenance_complete(); + crate::paint::preflight_patterns(&self.drawlist, ctx)?; Ok(crate::paint::raster_to_bytes_unchecked( &self.drawlist, &to_affine(*view), @@ -287,9 +292,9 @@ fn damage_input(product: &FrameProduct) -> FrameDamageInput<'_, VisualRef, (), G /// slice is rectangles, ellipses (each carried as its local-space bounding /// rectangle) and paths, the contract's admitted `cg` paints (solids, linear /// and radial gradients — every gradient preflighted against its resolved -/// paint box before the product exists), a centred stroke over the fill, -/// isolated opacity scopes, resolved geometric clip scopes, and the -/// frame-bounds clip. +/// paint box before the product exists), checked repeating vector programs, a +/// centred stroke over the fill, isolated opacity scopes, resolved geometric +/// clip scopes, and the frame-bounds clip. /// /// The contract's item stream is a checked type ([`rframe::FrameItems`]): /// balance, non-emptiness, and bounded nesting were proven at construction, @@ -591,6 +596,11 @@ pub fn compile(resolved: Frame) -> Result { _ => None, }; let paints = compile_paints(&node.paints, unit_offset); + let fill_pattern = node + .paints + .pattern() + .map(|pattern| compile_pattern(pattern, node.owner)) + .transpose()?; let fill_post_paint_opacity = PostPaintOpacity::from_resolved(node.paints.alpha_factor().get()); let owner = GlyphlessOwnerSlot::new( @@ -645,7 +655,17 @@ pub fn compile(resolved: Frame) -> Result { Geometry::Path(path) => Some(compile_path(path)), _ => None, }; - if !paints.is_empty() { + if let Some(pattern) = fill_pattern { + items.push(Item { + node: owner, + world: to_affine(node.transform), + kind: ItemKind::PatternFill { + geometry: compile_pattern_geometry(&node.geometry), + pattern, + post_paint_opacity: fill_post_paint_opacity, + }, + }); + } else if !paints.is_empty() { let kind = match &node.geometry { Geometry::Rect(_) => ItemKind::RectFill { w, @@ -679,6 +699,11 @@ pub fn compile(resolved: Frame) -> Result { // other in the same private drawlist, which is why a stroke needs no // group scope. if let Some(stroke) = &node.stroke { + let stroke_pattern = stroke + .paints() + .pattern() + .map(|pattern| compile_pattern(pattern, node.owner)) + .transpose()?; // A resolved dashed oval must preserve the exact local conic // stream over which its producer resolved the dash facts. Skia's // path measurement and dash traversal are f32 @@ -693,6 +718,20 @@ pub fn compile(resolved: Frame) -> Result { && rect.height > 0.0 && stroke.dash().is_some(); let (stroke, dash_phase, post_paint_opacity) = compile_stroke(stroke, unit_offset); + if let Some(pattern) = stroke_pattern { + items.push(Item { + node: owner, + world: to_affine(node.transform), + kind: ItemKind::PatternStroke { + geometry: compile_pattern_geometry(&node.geometry), + pattern, + stroke, + dash_phase, + post_paint_opacity, + }, + }); + continue; + } let kind = match &node.geometry { Geometry::Rect(_) => ItemKind::RectStroke { w, @@ -1224,6 +1263,50 @@ fn compile_gradient_transform( affine } +fn compile_pattern_geometry(geometry: &Geometry) -> ResolvedPatternGeometry { + match geometry { + Geometry::Rect(rect) => ResolvedPatternGeometry::Rect { + x: rect.x, + y: rect.y, + w: rect.width, + h: rect.height, + }, + Geometry::Ellipse(rect) => ResolvedPatternGeometry::Oval { + x: rect.x, + y: rect.y, + w: rect.width, + h: rect.height, + }, + Geometry::Path(path) => ResolvedPatternGeometry::Path(compile_path(path)), + } +} + +/// Compile a checked nested frame program without issuing raster commands. +/// Recursive programs re-enter this same proving shell; `rframe` already +/// bounds their depth, and every nested gradient/effect receives the same +/// deterministic preflight as a top-level frame. +fn compile_pattern( + pattern: &rframe::PatternPaint, + owner: VisualRef, +) -> Result, BuildError> { + let nested = Frame { + owner: VisualRef::new(rframe::Identity::new(0), rframe::Provenance::new(0)), + bounds: math2::Rectangle::from_xywh(0.0, 0.0, pattern.width(), pattern.height()), + items: pattern.items().as_ref().clone(), + }; + let product = compile(nested).map_err(|error| BuildError::Paint { + owner, + reason: format!("nested pattern program failed projection: {error}"), + })?; + Ok(Arc::new(ResolvedPattern { + width: pattern.width(), + height: pattern.height(), + transform: to_affine(pattern.transform()), + program: Arc::new(product.drawlist), + opacity: pattern.opacity(), + })) +} + fn compile_paints(paints: &PaintStack, unit_offset: Option<(f32, f32)>) -> Paints { let mut compiled = Vec::with_capacity(paints.len()); for paint in paints.iter() { @@ -1685,7 +1768,13 @@ mod tests { fn post_paint_opacity(kind: &ItemKind) -> Option { match kind { - ItemKind::RectFill { + ItemKind::PatternFill { + post_paint_opacity, .. + } + | ItemKind::PatternStroke { + post_paint_opacity, .. + } + | ItemKind::RectFill { post_paint_opacity, .. } | ItemKind::OvalFill { diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 85963eff..6e0bfbf9 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -27,10 +27,10 @@ use skia_safe::canvas::{SaveLayerFlags, SaveLayerRec}; use skia_safe::gradient::{Colors as GradientColors, Gradient, Interpolation}; use skia_safe::{ image::CachingHint, path_effect::PathEffect, shaders, stroke_rec::InitStyle, Blender, Canvas, - ClipOp, Color, Color4f, ColorChannel, ColorMatrix, ColorSpace, CubicResampler, Data, Font, - ISize, Image, ImageFilter, ImageInfo, Matrix, OpBuilder, Paint, PaintCap, PaintJoin, - PaintStyle, Path, PathBuilder, PathDirection, PathFillType, PathOp, Point, Point3, RRect, Rect, - SamplingOptions, Shader, StrokeRec, + ClipOp, Color, Color4f, ColorChannel, ColorMatrix, ColorSpace, CubicResampler, Data, + FilterMode, Font, ISize, Image, ImageFilter, ImageInfo, Matrix, OpBuilder, Paint, PaintCap, + PaintJoin, PaintStyle, Path, PathBuilder, PathDirection, PathFillType, PathOp, PictureRecorder, + Point, Point3, RRect, Rect, SamplingOptions, Shader, StrokeRec, }; use crate::drawlist::{ @@ -39,7 +39,7 @@ use crate::drawlist::{ ResolvedFilterColorSpace, ResolvedFilterComposite, ResolvedFilterConvolveEdgeMode, ResolvedFilterDisplacementChannel, ResolvedFilterInput, ResolvedFilterLightSource, ResolvedFilterMorphology, ResolvedFilterPrimitive, ResolvedFilterTurbulenceKind, - ResolvedMaskMode, StrokeDashPhase, + ResolvedMaskMode, ResolvedPattern, ResolvedPatternGeometry, StrokeDashPhase, }; /// The gradient family whose local matrix could not be represented by the @@ -186,6 +186,25 @@ impl std::fmt::Display for ImagePreflightError { impl std::error::Error for ImagePreflightError {} +/// A checked vector-pattern program could not be recorded into the backend's +/// repeat shader before replay began. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PatternPreflightError { + pub draw_item: usize, +} + +impl std::fmt::Display for PatternPreflightError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "vector pattern in draw item {} could not construct its repeat shader", + self.draw_item + ) + } +} + +impl std::error::Error for PatternPreflightError {} + static NEXT_PAINT_CONTEXT_ID: AtomicU64 = AtomicU64::new(1); /// Opaque identity of one complete host paint environment. @@ -773,7 +792,9 @@ pub(crate) fn preflight_gradients( )?; } } - ItemKind::BeginOpacity { .. } + ItemKind::PatternFill { .. } + | ItemKind::PatternStroke { .. } + | ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } @@ -960,7 +981,9 @@ pub(crate) fn preflight_images( )?; } } - ItemKind::BeginOpacity { .. } + ItemKind::PatternFill { .. } + | ItemKind::PatternStroke { .. } + | ItemKind::BeginOpacity { .. } | ItemKind::BeginIsolatedOpacity { .. } | ItemKind::EndOpacity | ItemKind::BeginClipRect { .. } @@ -1074,6 +1097,95 @@ fn image_shader(paint: &ImagePaint, paint_box: PaintBox, ctx: &PaintCtx) -> Opti Some(shader) } +/// Record one already-compiled tile program and expose it as an infinitely +/// repeating shader. The nested list starts with its own hard frame clip, so +/// content outside `(0, 0, width, height)` cannot leak into a neighbouring +/// tile before repetition. +fn pattern_shader(pattern: &ResolvedPattern, ctx: &PaintCtx) -> Option { + let tile = Rect::from_wh(pattern.width, pattern.height); + let mut recorder = PictureRecorder::new(); + let canvas = recorder.begin_recording(tile, false); + execute_unchecked(canvas, &pattern.program, &Affine::IDENTITY, ctx); + let picture = recorder.finish_recording_as_picture(Some(&tile))?; + Some(picture.to_shader( + Some((skia_safe::TileMode::Repeat, skia_safe::TileMode::Repeat)), + FilterMode::Linear, + Some(&skia_matrix(&pattern.transform)), + Some(&tile), + )) +} + +fn pattern_paint( + pattern: &ResolvedPattern, + post_paint_opacity: PostPaintOpacity, + ctx: &PaintCtx, +) -> Option { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_shader(pattern_shader(pattern, ctx)?); + // Pattern paint opacity follows the same byte-alpha materialization as a + // gradient shader. A one-draw element-opacity fold then multiplies that + // materialized alpha without another quantization. + let opacity = pattern.opacity.clamp(0.0, 1.0); + paint.set_alpha_f((opacity * 255.0).round() / 255.0); + let factor = post_paint_opacity.value(); + if factor != 1.0 { + paint.set_alpha_f(paint.alpha_f() * factor); + } + Some(paint) +} + +fn pattern_stroke_paint( + pattern: &ResolvedPattern, + stroke: &Stroke, + dash_phase: StrokeDashPhase, + post_paint_opacity: PostPaintOpacity, + ctx: &PaintCtx, +) -> Option { + let width = uniform_stroke_width(stroke)?; + let mut paint = pattern_paint(pattern, post_paint_opacity, ctx)?; + paint.set_style(PaintStyle::Stroke); + paint.set_stroke_width(width); + paint.set_stroke_cap(sk_stroke_cap(stroke.cap)); + paint.set_stroke_join(sk_stroke_join(stroke.join)); + paint.set_stroke_miter(stroke.miter_limit); + if let Some(values) = stroke.dash_array.as_deref() { + if !values.is_empty() { + let intervals = normalized_dash_array(values)?; + paint.set_path_effect(PathEffect::dash(&intervals, dash_phase.value())?); + } + } + Some(paint) +} + +fn preflight_pattern(pattern: &ResolvedPattern, ctx: &PaintCtx) -> bool { + if preflight_patterns(&pattern.program, ctx).is_err() { + return false; + } + pattern_shader(pattern, ctx).is_some() +} + +/// Prove every nested picture/repeat shader before the first target draw. +/// This keeps backend refusal outside replay: an unavailable shader cannot +/// silently turn a valid pattern into transparent paint. +pub(crate) fn preflight_patterns( + list: &DrawList, + ctx: &PaintCtx, +) -> Result<(), PatternPreflightError> { + for (draw_item, item) in list.items.iter().enumerate() { + let pattern = match &item.kind { + ItemKind::PatternFill { pattern, .. } | ItemKind::PatternStroke { pattern, .. } => { + pattern + } + _ => continue, + }; + if !preflight_pattern(pattern, ctx) { + return Err(PatternPreflightError { draw_item }); + } + } + Ok(()) +} + /// Materialize one model paint. The caller draws these in list order instead /// of precomposing a stack: each entry's blend mode must see the actual canvas /// result of the paints below it, including the scene backdrop. @@ -3892,6 +4004,68 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, canvas.restore(); } } + ItemKind::PatternFill { + geometry, + pattern, + post_paint_opacity, + } => { + with_local_transform(canvas, view, &item.world, || { + let paint = pattern_paint(pattern, *post_paint_opacity, ctx) + .expect("preflighted pattern shader construction failed"); + match geometry { + ResolvedPatternGeometry::Rect { x, y, w, h } => { + canvas.draw_rect(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Oval { x, y, w, h } => { + canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Path(path) => { + canvas.draw_path(&backend_path(path), &paint); + } + } + }); + } + ItemKind::PatternStroke { + geometry, + pattern, + stroke, + dash_phase, + post_paint_opacity, + } => { + with_local_transform(canvas, view, &item.world, || { + debug_assert_eq!(stroke.align, StrokeAlign::Center); + let adjusted = match geometry { + ResolvedPatternGeometry::Oval { w, h, .. } if *w > 0.0 && *h > 0.0 => { + stroke_cap_for_closed_contours(stroke) + } + ResolvedPatternGeometry::Path(path) + if path.all_contours_closed && !any_contour_may_be_degenerate(path) => + { + stroke_cap_for_closed_contours(stroke) + } + _ => stroke.clone(), + }; + let paint = pattern_stroke_paint( + pattern, + &adjusted, + *dash_phase, + *post_paint_opacity, + ctx, + ) + .expect("preflighted pattern stroke shader construction failed"); + match geometry { + ResolvedPatternGeometry::Rect { x, y, w, h } => { + canvas.draw_rect(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Oval { x, y, w, h } => { + canvas.draw_oval(Rect::from_xywh(*x, *y, *w, *h), &paint); + } + ResolvedPatternGeometry::Path(path) => { + canvas.draw_path(&backend_path(path), &paint); + } + } + }); + } ItemKind::RectFill { w, h, diff --git a/crates/n0/tests/drawlist.rs b/crates/n0/tests/drawlist.rs index 6715b175..31a4f7e7 100644 --- a/crates/n0/tests/drawlist.rs +++ b/crates/n0/tests/drawlist.rs @@ -27,6 +27,8 @@ fn tag(k: &ItemKind) -> &'static str { ItemKind::EndMaskContent => "mask-content-end", ItemKind::BeginFilter { .. } => "filter-begin", ItemKind::EndFilter => "filter-end", + ItemKind::PatternFill { .. } => "patternfill", + ItemKind::PatternStroke { .. } => "patternstroke", ItemKind::RectFill { .. } => "rectfill", ItemKind::RectStroke { .. } => "rectstroke", ItemKind::OvalFill { .. } => "ovalfill", diff --git a/crates/n0/tests/patterns.rs b/crates/n0/tests/patterns.rs new file mode 100644 index 00000000..06b6cd4f --- /dev/null +++ b/crates/n0/tests/patterns.rs @@ -0,0 +1,142 @@ +//! The n0 projection and replay laws for source-neutral pattern programs. +//! +//! These are producer-independent contract fixtures. Chromium remains the +//! external pixel oracle for Web meaning; here the asserted colors are the +//! literal hand-built inputs and therefore pin projection, repetition, +//! recursive replay, deterministic freshness, and damage ownership. + +use std::sync::Arc; + +use cg::CGColor; +use math2::transform::AffineTransform; +use math2::Rectangle; +use n0::glyphless::{compile, diff_frame}; +use n0::paint::PaintCtx; +use rframe::{ + Frame, FrameItems, FrameNode, Geometry, Identity, PaintStack, PatternPaint, Provenance, + VisualRef, +}; + +fn owner(value: u64) -> VisualRef { + VisualRef::new(Identity::new(value), Provenance::new(value)) +} + +fn node(value: u64, rect: Rectangle, paints: PaintStack) -> FrameNode { + FrameNode { + owner: owner(value), + transform: AffineTransform::identity(), + geometry: Geometry::Rect(rect), + bounds: rect, + paints, + stroke: None, + } +} + +fn stripe_pattern(left: CGColor, right: CGColor) -> PatternPaint { + PatternPaint::new( + 8.0, + 8.0, + AffineTransform::identity(), + Arc::new(FrameItems::from_nodes(vec![ + node( + 1, + Rectangle::from_xywh(0.0, 0.0, 4.0, 8.0), + PaintStack::solid(left), + ), + node( + 2, + Rectangle::from_xywh(4.0, 0.0, 4.0, 8.0), + PaintStack::solid(right), + ), + ])), + 1.0, + ) + .expect("checked stripe program") +} + +fn frame(pattern: PatternPaint) -> Frame { + let target = Rectangle::from_xywh(0.0, 0.0, 32.0, 16.0); + Frame { + owner: owner(100), + bounds: Rectangle::from_xywh(0.0, 0.0, 64.0, 64.0), + items: FrameItems::from_nodes(vec![node(101, target, PaintStack::from_pattern(pattern))]), + } +} + +fn raster(frame: &Frame) -> Vec { + compile(frame.clone()) + .expect("compile checked pattern frame") + .raster_to_bytes(&AffineTransform::identity(), 64, 64, &PaintCtx::new(None)) + .expect("preflight and replay pattern") +} + +fn at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] { + let offset = (y * 64 + x) * 4; + pixels[offset..offset + 4].try_into().expect("RGBA pixel") +} + +#[test] +fn a_vector_tile_repeats_and_fresh_replay_is_identical() { + let resolved = frame(stripe_pattern( + CGColor::from_rgb(0xef, 0x44, 0x44), + CGColor::from_rgb(0x22, 0xc5, 0x5e), + )); + let pixels = raster(&resolved); + + assert_eq!(at(&pixels, 1, 4), [0xef, 0x44, 0x44, 0xff]); + assert_eq!(at(&pixels, 5, 4), [0x22, 0xc5, 0x5e, 0xff]); + assert_eq!(at(&pixels, 9, 4), [0xef, 0x44, 0x44, 0xff]); + assert_eq!(at(&pixels, 29, 12), [0x22, 0xc5, 0x5e, 0xff]); + assert_eq!( + pixels, + raster(&resolved), + "a fresh picture program and shader produce the same bytes" + ); +} + +#[test] +fn a_nested_pattern_reenters_the_same_projection_and_replay() { + let inner = stripe_pattern( + CGColor::from_rgb(0xef, 0x44, 0x44), + CGColor::from_rgb(0x22, 0xc5, 0x5e), + ); + let outer = PatternPaint::new( + 16.0, + 16.0, + AffineTransform::identity(), + Arc::new(FrameItems::from_nodes(vec![node( + 3, + Rectangle::from_xywh(0.0, 0.0, 16.0, 16.0), + PaintStack::from_pattern(inner), + )])), + 1.0, + ) + .expect("bounded nested program"); + + let pixels = raster(&frame(outer)); + assert_eq!(at(&pixels, 1, 4), [0xef, 0x44, 0x44, 0xff]); + assert_eq!(at(&pixels, 5, 4), [0x22, 0xc5, 0x5e, 0xff]); + assert_eq!(at(&pixels, 17, 12), [0xef, 0x44, 0x44, 0xff]); +} + +#[test] +fn changed_tile_content_damages_the_outer_client_only() { + let before = compile(frame(stripe_pattern( + CGColor::from_rgb(0xef, 0x44, 0x44), + CGColor::from_rgb(0x22, 0xc5, 0x5e), + ))) + .expect("before product"); + let after = compile(frame(stripe_pattern( + CGColor::from_rgb(0xef, 0x44, 0x44), + CGColor::from_rgb(0x25, 0x63, 0xeb), + ))) + .expect("after product"); + + let damage = diff_frame(&before, &after); + assert_eq!(damage.changed, [owner(101)]); + assert_eq!( + damage.union_frame, + Some(Rectangle::from_xywh(0.0, 0.0, 32.0, 16.0)), + "the repeated source has no independent scene damage owner" + ); +} diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index f367c3ec..9fc4eda4 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -59,8 +59,9 @@ cargo run -p n0_cli --bin n0 -- \ document-level refusal until a host-level oracle can bake it. - Resources: self-contained input only; external images and stylesheets are not resolved. -- Capability: the admitted slice is deliberately narrow — solid- or - gradient-filled and -stroked `` (rounded corners included: `rx`/`ry` +- Capability: the admitted slice is deliberately narrow — `` filled and + stroked with solid, gradient, or admitted repeating-pattern paint (rounded + corners included: `rx`/`ry` resolve by the measured auto/clamp matrix and lower to the conics Chromium draws them through), ``, ``, `` (the complete `none | ` presentation-attribute grammar with `fill-rule`): source @@ -116,9 +117,9 @@ cargo run -p n0_cli --bin n0 -- \ are measured, not celled; their corresponding refusals are registered. Root `auto` remains admitted as the absent dimension, while root percentage sizing and CSS sizing remain the document-level contracts above. `` - and `` retain their own element/resource refusals; mask-region - geometry is admitted only by the separately bounded mask slice below, so - this rect evidence does not close the generic `x`/`y`/`width`/`height` rows. + retains its own element/resource refusal; pattern tile and mask-region + geometry are admitted only by their separately bounded slices below, so this + rect evidence does not close the generic `x`/`y`/`width`/`height` rows. `transform` is consumed in both spellings: the attribute is a presentation attribute of the one CSS `transform` property (CSS Transforms L1 §7), entering the cascade at hint level, so author CSS beats it — @@ -538,8 +539,8 @@ 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, 32 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete corpus contains 812 Chromium-baked cells plus 10 sampled - frames, with 152 named + cells. The complete corpus contains 874 Chromium-baked cells plus 10 sampled + frames, with 170 named refusal rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, @@ -767,7 +768,62 @@ cargo run -p n0_cli --bin n0 -- \ declaring one is a document-level declaration and a stop's style attribute refuses the paint), font-relative units in gradient geometry, a percentage in a gradient's computed transform (Chromium resolves it - against mismatched spaces), an external reference, and ``. + against mismatched spaces), and an external reference. + `` paint servers are consumed in a bounded static profile. A + same-document `url(#…)` resolves first-id-wins for each consuming fill or + stroke. Plain `href` beats `xlink:href`; template chains inherit each missing + attribute independently and take children all-or-nothing from the first + owner that has them, while cycles remove only their cyclic edge. An invalid + server activates the authored paint fallback. A valid pattern with no + painting children instead paints transparent and leaves that fallback inert. + Source compilation is transactional, so an unsupported child refuses the + complete affected client instead of leaking a partial tile. + `patternUnits` defaults to `objectBoundingBox` and + `patternContentUnits` defaults to `userSpaceOnUse`; both complete + `userSpaceOnUse | objectBoundingBox` grammars and invalid fallback are + admitted. Tile numbers and percentages resolve per client against the + correct independent axes. A pattern `viewBox` uses the complete + `preserveAspectRatio` resolver and supersedes `patternContentUnits`. + `patternTransform` is the transform property's presentation hint: CSS beats + it and a plain `transform` attribute is inert. Translation, axis scale, + reflection, and exact quarter turns are admitted. The repeating source can + contain admitted rectangles, gradients, ``, masks, and a pattern nested + alone; pattern paint covers admitted rect, ellipse, and path fills and + strokes, and target opacity, clip, mask, and filter scopes retain their + established order. Sixty-two Chromium-baked cells cover that profile, + including independent object-box clients and one inline-HTML SVG entry; all + are exact without a new tolerance. + What refuses by stable name: a pattern selected through + `context-fill`/`context-stroke`; an external template dependency; a non-`px` + unit, CSS math, `var()`, CSS-wide tile value, or CSS comments around an + otherwise valid tile length; a source child outside the admitted element + slice; filter composition inside the source program; curved source + coverage; isolated multi-draw source + opacity or a geometric source clip; another source draw mixed with a nested + pattern; a fractional final tile extent; and a final tile map carrying a + general rotation or shear. Those last five are measured picture-shader + precision boundaries, not guessed omissions. Before its patrol, the valid + comment spelling silently selected fallback in both admissions and changed + all 2,304 target pixels at maximum delta 202. A derived template whose + author stylesheet may contribute `transform:none` also refuses because the + pinned computed value loses the provenance needed to decide inheritance. + A CSS percentage transform on the pattern resource refuses until its + reference box can be carried without invention. Chromium resolves inline + `translate(50%, 0px)` against the 64-unit viewport; the former tile-width + basis changed 1,008 target pixels at maximum delta 205 (measured, not + celled). A ninth distinct nested pattern likewise refuses before its source + walk begins; the resolved contract admits at most eight programs, while + cycles retain their separate active-id refusal. + Finite tile coordinates beyond Chromium's Web used-length clamp refuse too: + the former raw route changed 768–2,112 pixels at maximum delta 205 for the + signed huge, adjacent, and beyond-binary32 witnesses instead of selecting + Chromium's clamped repetition phase (measured, not celled). + The source-number alias probe found no discriminating pattern raster at + 64×64: Chromium's adjacent controls were pixel-identical (measured, not + celled), but the raw decoder still cannot prove Blink's used value, so the + conservative provenance patrol remains. The `` and + `patternTransform` checklist rows therefore stay open; only `patternUnits` + and `patternContentUnits` close. `` is consumed (the text rung), and its font environment is the host's: text resolves only against fonts declared with `--font FAMILY=PATH@sha256:HEX` (repeatable), whose bytes are **verified diff --git a/crates/rframe/README.md b/crates/rframe/README.md index b725d787..d4b2b5b9 100644 --- a/crates/rframe/README.md +++ b/crates/rframe/README.md @@ -19,13 +19,13 @@ producer (e.g. websem, from SVG) ## What it holds -| Module | Ownership | -| -------- | ---------------------------------------------------------------------------------------------------------------- | -| `frame` | `Frame`, `FrameNode`, `Geometry`, the admitted paint stack and its post-paint alpha factor, and product identity | -| `path` | `PathData` — checked absolute commands, fill rule, tight bounds solved once | -| `stroke` | `Stroke` — centred width, cap, join, miter limit, optional checked dash pattern, and finite `f64` `outset` | -| `scope` | A checked painter-order scope stream: isolated opacity or source-neutral geometric clipping | -| `clip` | `ClipPath` — bounded path unions intersected in layers, with resolved transforms and conservative bounds | +| Module | Ownership | +| -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `frame` | `Frame`, `FrameNode`, `Geometry`, leaf paint stacks or checked repeating vector programs, their post-paint alpha factor, and product identity | +| `path` | `PathData` — checked absolute commands, fill rule, tight bounds solved once | +| `stroke` | `Stroke` — centred width, cap, join, miter limit, optional checked dash pattern, and finite `f64` `outset` | +| `scope` | A checked painter-order scope stream: isolated opacity or source-neutral geometric clipping | +| `clip` | `ClipPath` — bounded path unions intersected in layers, with resolved transforms and conservative bounds | Two details are load-bearing enough to state here. A node's `bounds` is the **geometry's** box, never the ink's: a stroke paints outside it, so a consumer @@ -33,14 +33,17 @@ that needs covered area inflates by `Stroke::outset()`. And a resolved value is resolved — a stroke that would paint nothing is `None` rather than a stroke with zero width, so no consumer re-derives "is this visible". -A `PaintStack` has one source-neutral `PaintAlphaFactor`. Each paint's own -alpha materializes first; the factor then modulates that entry before coverage -and source-over. On a multi-paint stack it applies independently to every entry -without changing their order. It is deliberately not opacity over the stack's -composite and creates no layer — that byte-distinct group meaning is a -`Scope`. Identity is the default, and zero resolves the complete stack to no -paint. Because a `Stroke` owns the same `PaintStack`, fill and stroke cross the -contract with one meaning and no source-specific duplicate field. +A `PaintStack` holds either its admitted `cg` leaves or one checked repeating +vector program; the two meanings cannot be mixed. It has one source-neutral +`PaintAlphaFactor`. Each leaf's own alpha materializes first; on a pattern, the +tile program's composite materializes first. The factor then modulates that +entry before coverage and source-over. On a multi-leaf stack it applies +independently to every entry without changing their order. It is deliberately +not opacity over the stack's composite and creates no layer — that +byte-distinct group meaning is a `Scope`. Identity is the default, and zero +resolves the complete stack to no paint. Because a `Stroke` owns the same +`PaintStack`, fill and stroke cross the contract with one meaning and no +source-specific duplicate field. `Stroke::outset()` widens only the arithmetic for that derived, direction-free bound. The resolved width and miter limit remain exact `f32` @@ -64,23 +67,26 @@ helper never understates the mathematical bound. ## Boundaries -The vocabulary is deliberately narrower than SVG or CSS. Solid, linear- and -radial-gradient paints only — a gradient is a self-contained normal-blend -color ramp stated in the unit square of the geometry's own box, so a paint -that still _references_ something (a pattern, an image resource, or an -unresolved context-paint relationship) or needs a focal geometry the shared -radial leaf cannot state remains inexpressible here. Source-level context paint -is not a new render fact: a producer must select and fully rebase its eventual -no-paint, solid, or gradient result before this boundary, without carrying the -context relation or its reference-box ownership into the frame. Beyond paint: -one stroke width and an optional immutable dash pattern. The pattern is an -even-length cycle of finite non-negative local-space intervals paired with one -finite local-space phase. Construction canonicalizes the phase modulo the -positive cycle; positive phase advances into the cycle, and the same phase +The vocabulary is deliberately narrower than SVG or CSS. Its ordinary leaves +are solid, linear-gradient, and radial-gradient paints. A gradient is a +self-contained normal-blend color ramp stated in the unit square of the +geometry's own box. The alternative pattern value is likewise resolved: one +bounded immutable `FrameItems` program in tile-local coordinates, one positive +tile extent, one finite tile-to-consumer transform, and no lookup key or +resource handle. A paint that still _references_ an authored pattern, image +resource, or unresolved context-paint relationship remains inexpressible here; +so does a focal geometry the shared radial leaf cannot state. Source-level +context paint is not a new render fact: a producer must select and fully rebase +its eventual no-paint, leaf, or pattern result before this boundary, without +carrying the context relation or its reference-box ownership into the frame. +Beyond paint: one stroke width and an optional immutable dash cycle. The cycle +is an even-length sequence of finite non-negative local-space intervals paired +with one finite local-space phase. Construction canonicalizes the phase modulo +the positive cycle; positive phase advances into the cycle, and the same phase restarts at every contour. Source units, percentages, and authored odd-list -repetition resolve before this boundary; the node, rather than the dash -pattern, owns the transform. Path-length calibration remains inexpressible here -rather than being ignored or approximated. Geometry is rect, ellipse or path. +repetition resolve before this boundary; the node, rather than the dash cycle, +owns the transform. Path-length calibration remains inexpressible here rather +than being ignored or approximated. Geometry is rect, ellipse or path. A geometric clip reuses that vocabulary after its source has resolved every resource lookup and coordinate system: one layer unions contributors, and layers intersect. It carries no URL, element, paint, alpha mask, or backend diff --git a/crates/rframe/src/frame.rs b/crates/rframe/src/frame.rs index 54662304..09847e26 100644 --- a/crates/rframe/src/frame.rs +++ b/crates/rframe/src/frame.rs @@ -115,9 +115,154 @@ impl Default for PaintAlphaFactor { } } -/// A validated ordered stack of visible normal-blend `cg` paints: solids, -/// linear gradients, and radial gradients, plus one uniform post-paint alpha -/// factor. +/// The deepest pattern-program nesting the resolved contract admits. +/// +/// Pattern programs are immutable item streams and can therefore contain a +/// node painted by another pattern program. Keeping the bound here makes the +/// recursive contract finite before any consumer compiles or replays it. +pub const MAX_PATTERN_DEPTH: usize = 8; + +/// Why a resolved repeating pattern program cannot enter the contract. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PatternPaintError { + /// A tile must have a finite, strictly positive extent. + InvalidTile, + /// The tile-to-item mapping must be finite and invertible. + InvalidTransform, + /// Paint opacity is a finite factor in the closed unit interval. + InvalidOpacity, + /// A nested pattern would exceed [`MAX_PATTERN_DEPTH`]. + TooDeep, +} + +impl std::fmt::Display for PatternPaintError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidTile => { + f.write_str("resolved pattern tile extent must be finite and positive") + } + Self::InvalidTransform => { + f.write_str("resolved pattern tile mapping must be finite and invertible") + } + Self::InvalidOpacity => { + f.write_str("resolved pattern opacity must be in the closed unit interval") + } + Self::TooDeep => write!( + f, + "resolved pattern programs may nest at most {MAX_PATTERN_DEPTH} levels" + ), + } + } +} + +impl std::error::Error for PatternPaintError {} + +/// One checked source-neutral repeating vector program. +/// +/// `items` is recorded in tile-local coordinates. The tile occupies +/// `(0, 0)–(width, height)` and clips the program there before repetition. +/// `transform` maps those tile-local coordinates into the consuming node's +/// local geometry space. A producer has already resolved every source-side +/// unit system, template relation, viewport mapping, and transform into these +/// two facts; the program carries no lookup key or source handle. +/// +/// An empty item stream remains meaningful: a source can select a valid local +/// content owner whose children paint nothing. That transparent tile is +/// distinct from an invalid paint-server reference, which never constructs a +/// `PatternPaint` and lets the producer apply its authored fallback instead. +#[derive(Clone, Debug, PartialEq)] +pub struct PatternPaint { + width: f32, + height: f32, + transform: AffineTransform, + items: Arc, + opacity: f32, + depth: usize, +} + +impl PatternPaint { + /// Check one finite repeating program. + pub fn new( + width: f32, + height: f32, + transform: AffineTransform, + items: Arc, + opacity: f32, + ) -> Result { + if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 { + return Err(PatternPaintError::InvalidTile); + } + if !transform + .matrix + .iter() + .flatten() + .all(|component| component.is_finite()) + || transform.inverse().is_none() + { + return Err(PatternPaintError::InvalidTransform); + } + if !opacity.is_finite() || !(0.0..=1.0).contains(&opacity) { + return Err(PatternPaintError::InvalidOpacity); + } + let child_depth = items + .nodes() + .flat_map(|node| { + node.paints.pattern().into_iter().chain( + node.stroke + .as_ref() + .and_then(|stroke| stroke.paints().pattern()), + ) + }) + .map(|pattern| pattern.depth) + .max() + .unwrap_or(0); + let depth = child_depth + 1; + if depth > MAX_PATTERN_DEPTH { + return Err(PatternPaintError::TooDeep); + } + Ok(Self { + width, + height, + transform, + items, + opacity, + depth, + }) + } + + #[must_use] + pub const fn width(&self) -> f32 { + self.width + } + + #[must_use] + pub const fn height(&self) -> f32 { + self.height + } + + #[must_use] + pub const fn transform(&self) -> AffineTransform { + self.transform + } + + #[must_use] + pub const fn items(&self) -> &Arc { + &self.items + } + + #[must_use] + pub const fn opacity(&self) -> f32 { + self.opacity + } + + #[must_use] + pub const fn depth(&self) -> usize { + self.depth + } +} + +/// A validated ordered stack of visible normal-blend leaf paints, or one +/// checked repeating pattern program, plus one uniform post-paint alpha factor. /// /// This is an admitted subset of the shared leaf vocabulary, not a competing /// paint vocabulary. Construction removes paints with no visual effect and @@ -133,15 +278,17 @@ impl Default for PaintAlphaFactor { /// source's coordinate systems into these unit-box facts; no source vocabulary /// (units, references, spread keywords) crosses the contract. /// -/// The alpha factor applies independently to every entry, after that entry's -/// own alpha materializes and before it composites over the entries below it. -/// It is therefore not opacity over the already-composited stack and creates -/// no isolated group. A producer that needs to modulate the stack's composite -/// states a [`Scope`] instead. This order is equally defined for a one-paint -/// and a multi-paint stack; the factor never changes paint order. +/// The alpha factor applies independently to every leaf entry, or to the one +/// complete pattern program after its tile composite materializes, and before +/// that entry composites over anything below it. It is therefore not opacity +/// over an already-composited multi-entry stack and creates no isolated group. +/// A producer that needs to modulate the stack's composite states a [`Scope`] +/// instead. This order is equally defined for a one-paint and a multi-paint +/// stack; the factor never changes paint order. #[derive(Clone, Debug, PartialEq)] pub struct PaintStack { paints: cg::Paints, + pattern: Option>, alpha_factor: PaintAlphaFactor, } @@ -149,6 +296,7 @@ impl Default for PaintStack { fn default() -> Self { Self { paints: cg::Paints::default(), + pattern: None, alpha_factor: PaintAlphaFactor::IDENTITY, } } @@ -165,6 +313,7 @@ impl PaintStack { } Self { paints: cg::Paints::new([cg::Paint::Solid(cg::SolidPaint::new_color(color))]), + pattern: None, alpha_factor: PaintAlphaFactor::IDENTITY, } } @@ -189,10 +338,27 @@ impl PaintStack { } Ok(Self { paints: cg::Paints::new(admitted), + pattern: None, alpha_factor: PaintAlphaFactor::IDENTITY, }) } + /// Carry one checked repeating program as the complete paint value. + /// + /// No current producer needs to mix a composite pattern program with the + /// ordered `cg` leaf stack, so construction keeps those two meanings + /// exclusive instead of inventing an unproved interleaving rule. + pub fn from_pattern(pattern: PatternPaint) -> Self { + if pattern.opacity() == 0.0 { + return Self::empty(); + } + Self { + paints: cg::Paints::default(), + pattern: Some(Arc::new(pattern)), + alpha_factor: PaintAlphaFactor::IDENTITY, + } + } + /// Attach the factor applied after every entry's intrinsic paint alpha. /// /// A zero factor, or a factor attached to an already-empty stack, @@ -216,16 +382,27 @@ impl PaintStack { self.alpha_factor } + /// Iterate the ordinary `cg` leaves. + /// + /// A pattern is a mutually exclusive composite program and is exposed by + /// [`PaintStack::pattern`] rather than pretending to be a `cg` leaf. pub fn iter(&self) -> impl Iterator { self.paints.iter() } + /// The checked repeating program, when this stack carries that composite + /// paint meaning instead of `cg` leaves. + #[must_use] + pub fn pattern(&self) -> Option<&PatternPaint> { + self.pattern.as_deref() + } + pub fn len(&self) -> usize { - self.paints.len() + self.paints.len() + usize::from(self.pattern.is_some()) } pub fn is_empty(&self) -> bool { - self.paints.is_empty() + self.paints.is_empty() && self.pattern.is_none() } } diff --git a/crates/rframe/src/lib.rs b/crates/rframe/src/lib.rs index ba655af6..d4b201ee 100644 --- a/crates/rframe/src/lib.rs +++ b/crates/rframe/src/lib.rs @@ -31,8 +31,9 @@ pub use filter::{ FilterTurbulenceKind, MAX_FILTER_CONVOLVE_KERNEL_VALUES, MAX_FILTER_NODES, }; pub use frame::{ - Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, MAX_SCOPE_DEPTH, - PaintAlphaFactor, PaintAlphaFactorError, PaintStack, PaintStackError, Provenance, VisualRef, + Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, + MAX_PATTERN_DEPTH, MAX_SCOPE_DEPTH, PaintAlphaFactor, PaintAlphaFactorError, PaintStack, + PaintStackError, PatternPaint, PatternPaintError, Provenance, VisualRef, }; pub use mask::{Mask, MaskMode}; pub use path::{FillRule, PathCommand, PathData, PathDataError}; diff --git a/crates/rframe/tests/pattern_contract.rs b/crates/rframe/tests/pattern_contract.rs new file mode 100644 index 00000000..4935fe65 --- /dev/null +++ b/crates/rframe/tests/pattern_contract.rs @@ -0,0 +1,124 @@ +//! Construction laws for the source-neutral repeating-program paint fact. +//! +//! The producer has already resolved every source relation before this +//! contract is built. These tests pin what remains: finite tile geometry, an +//! invertible local mapping, immutable checked items, one exclusive paint +//! value, and bounded recursive programs. + +use std::sync::Arc; + +use cg::CGColor; +use math2::Rectangle; +use math2::transform::AffineTransform; +use rframe::{ + FrameItems, FrameNode, Geometry, Identity, MAX_PATTERN_DEPTH, PaintStack, PatternPaint, + PatternPaintError, Provenance, VisualRef, +}; + +fn owner(value: u64) -> VisualRef { + VisualRef::new(Identity::new(value), Provenance::new(value)) +} + +fn rect_items(paints: PaintStack) -> FrameItems { + let rect = Rectangle::from_xywh(0.0, 0.0, 8.0, 8.0); + FrameItems::from_nodes(vec![FrameNode { + owner: owner(1), + transform: AffineTransform::identity(), + geometry: Geometry::Rect(rect), + bounds: rect, + paints, + stroke: None, + }]) +} + +fn pattern(items: FrameItems) -> Result { + PatternPaint::new(8.0, 8.0, AffineTransform::identity(), Arc::new(items), 1.0) +} + +#[test] +fn one_checked_program_is_the_complete_paint_value() { + let items = rect_items(PaintStack::solid(CGColor::RED)); + let resolved = pattern(items.clone()).expect("finite pattern"); + let stack = PaintStack::from_pattern(resolved.clone()); + + assert_eq!(resolved.width(), 8.0); + assert_eq!(resolved.height(), 8.0); + assert_eq!(resolved.transform(), AffineTransform::identity()); + assert_eq!(resolved.items().as_ref(), &items); + assert_eq!(resolved.opacity(), 1.0); + assert_eq!(resolved.depth(), 1); + assert_eq!(stack.len(), 1); + assert_eq!(stack.iter().count(), 0, "a pattern is not a cg leaf"); + assert_eq!(stack.pattern(), Some(&resolved)); +} + +#[test] +fn construction_rejects_unusable_tile_mapping_and_opacity() { + let items = Arc::new(rect_items(PaintStack::solid(CGColor::RED))); + for (width, height) in [ + (0.0, 8.0), + (-1.0, 8.0), + (8.0, f32::INFINITY), + (f32::NAN, 8.0), + ] { + assert_eq!( + PatternPaint::new( + width, + height, + AffineTransform::identity(), + Arc::clone(&items), + 1.0, + ), + Err(PatternPaintError::InvalidTile) + ); + } + + let singular = AffineTransform::from_acebdf(1.0, 0.0, 0.0, 0.0, 0.0, 0.0); + assert_eq!( + PatternPaint::new(8.0, 8.0, singular, Arc::clone(&items), 1.0), + Err(PatternPaintError::InvalidTransform) + ); + for opacity in [-0.1, 1.1, f32::NAN, f32::INFINITY] { + assert_eq!( + PatternPaint::new( + 8.0, + 8.0, + AffineTransform::identity(), + Arc::clone(&items), + opacity, + ), + Err(PatternPaintError::InvalidOpacity) + ); + } +} + +#[test] +fn recursive_programs_stop_at_the_contract_bound() { + let mut current = + pattern(rect_items(PaintStack::solid(CGColor::RED))).expect("depth-one pattern"); + assert_eq!(current.depth(), 1); + + for expected_depth in 2..=MAX_PATTERN_DEPTH { + current = pattern(rect_items(PaintStack::from_pattern(current))) + .expect("within the recursive bound"); + assert_eq!(current.depth(), expected_depth); + } + + assert_eq!( + pattern(rect_items(PaintStack::from_pattern(current))), + Err(PatternPaintError::TooDeep) + ); +} + +#[test] +fn a_zero_opacity_program_normalizes_to_the_empty_stack() { + let resolved = PatternPaint::new( + 8.0, + 8.0, + AffineTransform::identity(), + Arc::new(rect_items(PaintStack::solid(CGColor::RED))), + 0.0, + ) + .expect("zero is a valid checked opacity"); + assert!(PaintStack::from_pattern(resolved).is_empty()); +} diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index c2124cb6..84853e48 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -70,7 +70,7 @@ //! | entries and session | `SourceEntry`, `CompileMode`, [`SvgFrameSource`], the two `compile_*` functions, the child walk | //! | departures | [`CompileError`], [`Degradation`], and every `patrol_*` — the attribute tables, the cascaded-property reads, the stylesheet scans, the unit patrol | //! | shapes | `compile_rect`/`_circle`/`_ellipse`/`_path`/`_line` and `shape_node` | -//! | paint | `resolve_fill`, `resolve_stroke`, `resolve_fill_rule`, and the admitted colour surface | +//! | paint | `resolve_fill`, `resolve_stroke`, `resolve_fill_rule`, and the admitted colour and paint-server surface | //! | viewport | [`InitialViewport`], `parse_viewbox`, the `preserveAspectRatio` grammar and its viewport mapping | //! //! Two conversions *are* separate files, because they are value-in/value-out @@ -92,13 +92,14 @@ //! and quantize once. A valid paint server instead keeps element opacity as a //! post-paint factor. The Chromium-baked primitive suite gates both routes //! pixel-exactly — plus same-document linear and radial gradient paint -//! servers (the gradient rung), plus standard `context-fill` / `context-stroke` -//! relationships under expanded `` instances. Context relationships -//! resolve completely here — including recursive selection, currentColor and -//! gradient reference spaces — and never cross `rframe`. Everything else -//! refuses explicitly: Stylo's non-standard context-paint fallback extension, -//! context-valued opacities, non-sRGB color spaces, and `` -//! (`tests/typed_fill.rs` and the translucency contract pin each). +//! servers (the gradient rung), bounded same-document repeating vector +//! patterns, and standard `context-fill` / `context-stroke` relationships +//! under expanded `` instances. Context relationships resolve completely +//! here — including recursive selection, currentColor and gradient reference +//! spaces — and never cross `rframe`. Unsupported pattern composition and +//! precision branches refuse by their own stable names. Other paint gaps also +//! refuse explicitly, including Stylo's non-standard context-paint fallback +//! extension, context-valued opacities, and non-sRGB color spaces. //! //! ## Document lifetime //! Each retained source owns one [`csscascade::adapter::DocumentSession`]. @@ -122,7 +123,8 @@ use style::computed_values::visibility::T as Visibility; use style::dom::TElement; use crate::svg_paint_server::{ - GradientBases, PaintServers, ParsedColorAttribute, ResolvedPaintServer, parse_color_attribute, + ClassifiedServer, GradientBases, PaintServers, ParsedColorAttribute, ResolvedPaintServer, + parse_color_attribute, }; use crate::svg_transform::{TransformRefusal, computed_transform_to_affine}; use style::properties::ComputedValues; @@ -140,8 +142,8 @@ use rframe::{ FilterInput, FilterLightSource, FilterMorphology, FilterNode, FilterPrimitive, FilterProgram, FilterTurbulenceKind, Frame, FrameItem, FrameItems, FrameItemsError, FrameNode, Geometry, Identity, MAX_FILTER_CONVOLVE_KERNEL_VALUES, Mask, MaskMode, PaintAlphaFactor, PaintStack, - PathData, Provenance, Scope, ScopeEffect, ScopeOpacity, Stroke, StrokeCap, StrokeDash, - StrokeDashIntervals, StrokeDashIntervalsError, StrokeJoin, VisualRef, + PathData, PatternPaint, Provenance, Scope, ScopeEffect, ScopeOpacity, Stroke, StrokeCap, + StrokeDash, StrokeDashIntervals, StrokeDashIntervalsError, StrokeJoin, VisualRef, }; use std::sync::Arc; @@ -2552,6 +2554,18 @@ fn compile_svg_element( let clips = clip_path::Resources::collect(document_root(svg), svg); let masks = mask_resource::Resources::collect(document_root(svg), svg); let filters = filter_resource::Resources::collect(document_root(svg), svg); + let has_author_css = document_has_author_css(svg); + let patterns = PatternCompiler { + values, + root_bases: bases, + override_skips, + has_author_css, + servers: &servers, + clips: &clips, + masks: &masks, + filters: &filters, + fonts, + }; let GeometryMeasurements { use_boxes, effect_boxes, @@ -2562,11 +2576,12 @@ fn compile_svg_element( mode, degradations, override_skips, - has_author_css: document_has_author_css(svg), + has_author_css, servers: &servers, clips: &clips, masks: &masks, filters: &filters, + patterns: &patterns, use_boxes, effect_boxes, paint_contexts: Vec::new(), @@ -2575,6 +2590,7 @@ fn compile_svg_element( items: Vec::new(), top_level_shapes: Vec::new(), active_masks: Vec::new(), + active_patterns: Vec::new(), next_id: 0, }; if root_disposition != RenderDisposition::PrunedSubtree || initial_viewport.is_some() { @@ -2662,6 +2678,679 @@ impl SpanFacts { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PatternUnits { + UserSpaceOnUse, + ObjectBoundingBox, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum PatternViewBox { + None, + Degenerate, + Mapped((f32, f32, f32, f32)), +} + +/// The three semantic outcomes of resolving a valid same-document pattern +/// reference. `Invalid` lets the paint property's authored fallback fire; +/// `Paint` may contain an empty program, which is a valid transparent paint +/// and deliberately suppresses that fallback. +enum PatternResolution { + Invalid, + Paint(PatternPaint), +} + +/// Immutable document-side inputs needed to turn one `` into a +/// source-neutral program for one consuming paint box. +/// +/// Resolution is deliberately per client: object-box units and inherited +/// source state can make two references to the same DOM element produce +/// different tile and coordinate facts. The selected source subtree is +/// compiled transactionally through a fresh strict [`ChildWalk`]; no partially +/// compiled tile can escape. +struct PatternCompiler<'a> { + values: &'a EffectiveValues, + root_bases: PercentBases, + override_skips: &'a HashMap, + has_author_css: bool, + servers: &'a PaintServers<'a>, + clips: &'a clip_path::Resources<'a>, + masks: &'a mask_resource::Resources<'a>, + filters: &'a filter_resource::Resources<'a>, + fonts: &'a textlayout::Environment, +} + +impl<'a> PatternCompiler<'a> { + #[allow(clippy::too_many_arguments)] + fn resolve( + &self, + fragment: &str, + first: HtmlElement<'a>, + reference_box: Rectangle, + owner_to_destination: AffineTransform, + paint_opacity: f32, + active_patterns: &[NodeId], + ) -> Result { + if active_patterns.len() >= rframe::MAX_PATTERN_DEPTH { + return Err(format!( + "nested pattern paint chain exceeds the resolved {}-program limit", + rframe::MAX_PATTERN_DEPTH + )); + } + if active_patterns.contains(&first.node_id()) { + return Err(format!( + "nested pattern paint cycle reaches #{fragment} while its tile program is active" + )); + } + + let (chain, external_tail) = self.template_chain(first)?; + for element in &chain { + patrol_rendering_attributes(*element, "pattern", &[]) + .map_err(|error| error.to_string())?; + patrol_style_attribute(*element, "pattern").map_err(|error| error.to_string())?; + } + + let content_owner = chain + .iter() + .copied() + .find(|element| element.first_element_child().is_some()); + + // Crossing the document boundary is harmless only when no fact could + // be inherited through it. Otherwise the external resource might + // change the tile and this resource-free compiler must refuse rather + // than silently use local defaults. + if external_tail + && (content_owner.is_none() + || [ + "x", + "y", + "width", + "height", + "patternUnits", + "patternContentUnits", + "patternTransform", + "viewBox", + "preserveAspectRatio", + ] + .iter() + .any(|name| pattern_chain_attr(&chain, name).is_none())) + { + return Err(format!( + "pattern #{fragment} needs attributes or content from an external template, and external resources are not resolved" + )); + } + + // No selected element child means no content source at all. Chromium + // treats that as an invalid pattern and selects the URL fallback. By + // contrast, a selected ``, ``, or other non-painting + // element child is a valid source owner and compiles to an empty + // program below, suppressing fallback. + let Some(content_owner) = content_owner else { + return Ok(PatternResolution::Invalid); + }; + + let pattern_units = + resolve_pattern_units(&chain, "patternUnits", PatternUnits::ObjectBoundingBox); + let content_units = + resolve_pattern_units(&chain, "patternContentUnits", PatternUnits::UserSpaceOnUse); + let x = pattern_length(&chain, "x", pattern_units, reference_box, self.root_bases)?; + let y = pattern_length(&chain, "y", pattern_units, reference_box, self.root_bases)?; + let width = pattern_length( + &chain, + "width", + pattern_units, + reference_box, + self.root_bases, + )?; + let height = pattern_length( + &chain, + "height", + pattern_units, + reference_box, + self.root_bases, + )?; + if !(width > 0.0 && height > 0.0) { + return Ok(PatternResolution::Invalid); + } + + let (tile_x, tile_y) = match pattern_units { + PatternUnits::UserSpaceOnUse => (x, y), + PatternUnits::ObjectBoundingBox => (reference_box.x + x, reference_box.y + y), + }; + if ![tile_x, tile_y, width, height] + .into_iter() + .all(f32::is_finite) + { + return Err(format!( + "pattern #{fragment} tile geometry resolves outside the finite frame range" + )); + } + + let view_box = pattern_view_box(&chain); + if view_box == PatternViewBox::Degenerate { + return Ok(PatternResolution::Invalid); + } + let par = match pattern_chain_attr(&chain, "preserveAspectRatio") { + Some(value) => parse_preserve_aspect_ratio(&value).map_err(|error| { + format!("pattern #{fragment} preserveAspectRatio is unsupported: {error}") + })?, + None => PreserveAspectRatio::default(), + }; + let pattern_transform = self.pattern_transform(&chain)?; + let tile_origin = AffineTransform::from_acebdf(1.0, 0.0, tile_x, 0.0, 1.0, tile_y); + let tile_to_destination = owner_to_destination + .compose(&pattern_transform) + .compose(&tile_origin); + if !tile_to_destination + .matrix + .iter() + .flatten() + .all(|value| value.is_finite()) + || tile_to_destination.inverse().is_none() + { + // A valid server whose tile cannot establish an invertible + // sampling space is invalid for painting; Chromium selects the + // authored URL fallback for the measured singular cases. + return Ok(PatternResolution::Invalid); + } + + let (content_to_tile, source_bases) = match view_box { + PatternViewBox::Mapped(view_box) => ( + viewbox_to_viewport_transform((width, height), view_box, par), + PercentBases { + width: view_box.2, + height: view_box.3, + }, + ), + PatternViewBox::None => match content_units { + // No viewBox establishes a tile-local origin. Chromium keeps + // user-space content in the referencing element's current + // coordinate system; tile x/y changes phase independently. + PatternUnits::UserSpaceOnUse => (AffineTransform::identity(), self.root_bases), + // Object-box content maps the normalized coordinates by the + // box extents only. Its origin likewise remains independent + // from the pattern tile's x/y phase. + PatternUnits::ObjectBoundingBox => ( + AffineTransform::from_acebdf( + reference_box.width, + 0.0, + 0.0, + 0.0, + reference_box.height, + 0.0, + ), + PercentBases { + width: 1.0, + height: 1.0, + }, + ), + }, + PatternViewBox::Degenerate => unreachable!("handled above"), + }; + if !content_to_tile + .matrix + .iter() + .flatten() + .all(|value| value.is_finite()) + { + return Err(format!( + "pattern #{fragment} content mapping resolves outside the finite frame range" + )); + } + + let items = self.compile_source( + fragment, + content_owner, + content_to_tile, + source_bases, + active_patterns, + first.node_id(), + )?; + let pattern = PatternPaint::new( + width, + height, + tile_to_destination, + Arc::new(items), + paint_opacity, + ) + .map_err(|error| format!("pattern #{fragment} cannot enter the resolved frame: {error}"))?; + Ok(PatternResolution::Paint(pattern)) + } + + fn template_chain( + &self, + first: HtmlElement<'a>, + ) -> Result<(Vec>, bool), String> { + let mut chain = vec![first]; + let mut visited = std::collections::HashSet::from([first.node_id()]); + let mut current = first; + loop { + let Some(reference) = crate::svg_paint_server::paint_server_href(current) else { + return Ok((chain, false)); + }; + let Some(fragment) = reference.strip_prefix('#') else { + return Ok((chain, true)); + }; + let Some(next) = crate::svg_paint_server::pattern_template(self.servers, fragment)? + else { + return Ok((chain, false)); + }; + if !visited.insert(next.node_id()) { + return Ok((chain, false)); + } + chain.push(next); + current = next; + } + } + + fn pattern_transform(&self, chain: &[HtmlElement<'a>]) -> Result { + for (index, element) in chain.iter().copied().enumerate() { + let data = element + .borrow_data() + .ok_or_else(|| "a pattern element has no computed style".to_string())?; + let transform = data.styles.primary().clone_transform(); + drop(data); + let has_attribute = get_attr(element, "patternTransform").is_some(); + let inline_declares = get_attr(element, "style") + .is_some_and(|style| css_declares_property(&style, "transform")); + if transform.0.is_empty() && !has_attribute && !inline_declares { + // A stylesheet `transform:none` on a derived pattern is not + // attributable from the computed empty list. If a later + // template contributes a transform, proceeding would silently + // resurrect it, so quarantine that narrow provenance loss. + if self.has_author_css + && index + 1 < chain.len() + && chain[index + 1..] + .iter() + .any(|later| get_attr(*later, "patternTransform").is_some()) + { + return Err( + "an author stylesheet may set transform:none on a derived pattern; the empty computed value loses the provenance needed to decide template inheritance" + .to_string(), + ); + } + continue; + } + if transform.0.is_empty() { + return Ok(AffineTransform::identity()); + } + let affine = computed_transform_to_affine(&transform, None).map_err(|refusal| match refusal { + TransformRefusal::Function(name) => format!( + "pattern transform uses {name}(), outside the admitted 2D affine function set" + ), + TransformRefusal::Calc => { + "pattern transform uses calc(), which is not yet consumed".to_string() + } + TransformRefusal::Percentage => { + "pattern transform percentage has no proved reference-box basis".to_string() + } + })?; + if !affine + .matrix + .iter() + .flatten() + .all(|value| value.is_finite()) + { + return Err("pattern transform is not finite".to_string()); + } + return Ok(affine); + } + Ok(AffineTransform::identity()) + } + + #[allow(clippy::too_many_arguments)] + fn compile_source( + &self, + fragment: &str, + content_owner: HtmlElement<'a>, + content_to_tile: AffineTransform, + source_bases: PercentBases, + active_patterns: &[NodeId], + pattern_id: NodeId, + ) -> Result { + let GeometryMeasurements { + use_boxes, + effect_boxes, + } = measure_geometry( + content_owner, + self.values, + source_bases, + self.fonts, + self.override_skips, + ) + .map_err(|error| format!("pattern #{fragment} source geometry: {error}"))?; + let mut degradations = Vec::new(); + let mut source_active_patterns = active_patterns.to_vec(); + source_active_patterns.push(pattern_id); + let mut walk = ChildWalk { + values: self.values, + bases: source_bases, + mode: CompileMode::Strict, + degradations: &mut degradations, + override_skips: self.override_skips, + has_author_css: self.has_author_css, + servers: self.servers, + clips: self.clips, + masks: self.masks, + filters: self.filters, + patterns: self, + use_boxes, + effect_boxes, + paint_contexts: Vec::new(), + context_paint_transform: content_to_tile, + fonts: self.fonts, + items: Vec::new(), + top_level_shapes: Vec::new(), + active_masks: Vec::new(), + active_patterns: source_active_patterns, + next_id: 0, + }; + walk.compile_children( + content_owner, + content_to_tile, + &format!("pattern-source(#{fragment})"), + 0, + 1.0, + ) + .map_err(|error| { + format!("pattern #{fragment} source cannot compile completely: {error}") + })?; + let items = std::mem::take(&mut walk.items); + drop(walk); + if let Some(degradation) = degradations.first() { + return Err(format!( + "pattern #{fragment} source cannot compile completely: {}", + degradation.reason() + )); + } + let items = FrameItems::try_new(items).map_err(|error| { + format!("pattern #{fragment} source item stream is invalid: {error}") + })?; + patrol_pattern_source_program(&items) + .map_err(|reason| format!("pattern #{fragment} {reason}"))?; + Ok(items) + } +} + +/// Keep the admitted picture-shader source on the exact side of the measured +/// Chromium/pinned-Skia boundary. +/// +/// Rectangular content whose mapped edges land on tile pixels is exact across +/// solids, gradients, strokes, masks, filters, and ``. Curved or +/// subpixel source geometry changes antialias coverage; an isolated source +/// opacity or geometric clip changes the recorded layer's byte-domain route; +/// and composing another draw over a nested pattern changes picture sampling. +/// Those are backend-generation facts, not SVG grammar defaults, so each +/// refuses the complete affected paint instead of letting a plausible tile +/// escape. +fn patrol_pattern_source_program(items: &FrameItems) -> Result<(), &'static str> { + let draw_count = items + .nodes() + .map(|node| { + usize::from(!node.paints.is_empty()) + + usize::from( + node.stroke + .as_ref() + .is_some_and(|stroke| !stroke.paints().is_empty()), + ) + }) + .sum::(); + let has_nested_pattern = items.nodes().any(|node| { + node.paints.pattern().is_some() + || node + .stroke + .as_ref() + .is_some_and(|stroke| stroke.paints().pattern().is_some()) + }); + let has_compositing_commands = items.iter().any(|item| !matches!(item, FrameItem::Node(_))); + if has_nested_pattern && (draw_count != 1 || has_compositing_commands) { + return Err( + "source mixes a nested pattern with another draw at the pinned-backend picture-shader composition precision boundary", + ); + } + + for item in items { + match item { + FrameItem::Node(node) => { + if !matches!(node.geometry, Geometry::Rect(_)) { + return Err( + "source carries curved/vector geometry at the pinned-backend picture-shader source-coverage precision boundary", + ); + } + let [[a, c, _], [b, d, _]] = node.transform.matrix; + let axis_or_quarter_turn = (b == 0.0 && c == 0.0) || (a == 0.0 && d == 0.0); + if !axis_or_quarter_turn { + return Err( + "source carries a general rotation or shear at the pinned-backend picture-shader source-coverage precision boundary", + ); + } + let bounds = node.bounds; + if ![bounds.x, bounds.y, bounds.width, bounds.height] + .into_iter() + .all(|value| value.is_finite() && value.fract() == 0.0) + { + return Err( + "source carries subpixel geometry or a subpixel transform at the pinned-backend picture-shader source-coverage precision boundary", + ); + } + } + FrameItem::ScopeBegin(scope) + if matches!(scope.effect, ScopeEffect::Opacity(_) | ScopeEffect::Clip(_)) => + { + return Err( + "source carries an isolated opacity or geometric clip at the pinned-backend picture-shader source-effect precision boundary", + ); + } + FrameItem::ScopeBegin(scope) if matches!(scope.effect, ScopeEffect::Filter(_)) => { + return Err( + "source uses a filter composition outside the admitted pattern source slice", + ); + } + FrameItem::ScopeBegin(_) + | FrameItem::ScopeEnd + | FrameItem::MaskBegin(_) + | FrameItem::MaskSource + | FrameItem::MaskEnd => {} + } + } + Ok(()) +} + +/// Picture repetition is exact for translations, reflections, independent +/// axis scales, and exact quarter turns while the final tile extents land on +/// whole pixels. General rotations can agree for one source subdivision and +/// differ for another by one to three code values; shears and fractional tile +/// extents expose wider instances of the same backend-generation boundary. +fn patrol_pattern_target_mapping( + pattern: &PatternPaint, + node_to_frame: AffineTransform, +) -> Result<(), &'static str> { + let tile_to_frame = node_to_frame.compose(&pattern.transform()); + let [[a, c, _], [b, d, _]] = tile_to_frame.matrix; + if ![a, b, c, d].into_iter().all(f32::is_finite) { + return Err("tile mapping is not finite"); + } + + let axis_or_quarter_turn = (b == 0.0 && c == 0.0) || (a == 0.0 && d == 0.0); + if !axis_or_quarter_turn { + return Err( + "target mapping carries a general rotation or shear at the pinned-backend picture-shader affine precision boundary", + ); + } + + let x_length = a.hypot(b); + let y_length = c.hypot(d); + let near_integer = |value: f32| { + value.is_finite() + && (value - value.round()).abs() <= value.abs().max(1.0) * f32::EPSILON * 8.0 + }; + if !near_integer(pattern.width() * x_length) || !near_integer(pattern.height() * y_length) { + return Err( + "tile has a fractional final device extent at the pinned-backend picture-shader sampling precision boundary", + ); + } + Ok(()) +} + +fn pattern_chain_attr(chain: &[HtmlElement<'_>], name: &str) -> Option { + chain + .iter() + .find_map(|element| get_attr(*element, name)) + .map(|value| trim_svg_whitespace(&value).to_string()) +} + +fn resolve_pattern_units( + chain: &[HtmlElement<'_>], + name: &str, + initial: PatternUnits, +) -> PatternUnits { + match pattern_chain_attr(chain, name).as_deref() { + Some("userSpaceOnUse") => PatternUnits::UserSpaceOnUse, + Some("objectBoundingBox") => PatternUnits::ObjectBoundingBox, + // Missing, empty, malformed, and wrong-case values all select the + // initial member in current Chromium. + _ => initial, + } +} + +fn pattern_length( + chain: &[HtmlElement<'_>], + name: &str, + units: PatternUnits, + reference_box: Rectangle, + root_bases: PercentBases, +) -> Result { + let Some(text) = pattern_chain_attr(chain, name) else { + return Ok(0.0); + }; + let text = trim_svg_whitespace(&text); + if text.is_empty() { + return Ok(0.0); + } + let lower = text.to_ascii_lowercase(); + if text.contains("/*") { + return Err(format!( + "pattern {name} contains a CSS comment; the direct length decoder does not tokenize comments" + )); + } + if text.contains('\\') { + return Err(format!( + "pattern {name} carries a CSS escape whose length meaning this direct decoder cannot prove" + )); + } + if lower.contains("var(") { + return Err(format!( + "pattern {name} resolves through var(), which this direct decoder cannot follow" + )); + } + if ["inherit", "initial", "unset", "revert", "revert-layer"].contains(&lower.as_str()) { + return Err(format!( + "pattern {name} uses the CSS-wide value {text:?}, whose cascaded length route is not represented" + )); + } + if lower.contains('(') { + return Err(format!( + "pattern {name} uses a CSS function in {text:?}, which this direct decoder does not consume" + )); + } + + let (number, percentage) = match text.strip_suffix('%') { + Some(number) => (trim_svg_whitespace(number), true), + None => { + let number = if lower.ends_with("px") { + &text[..text.len() - 2] + } else { + text + }; + (trim_svg_whitespace(number), false) + } + }; + if !dots_carry_digits(number) { + return Ok(0.0); + } + let parsed = match number.parse::() { + Ok(value) if value.is_finite() => value, + _ => { + if number.parse::().is_ok_and(f64::is_finite) { + return Err(format!( + "pattern {name} exceeds the admitted Web used-value range" + )); + } + let numeric_prefix = number + .bytes() + .next() + .is_some_and(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-' | b'.')); + if numeric_prefix + && number + .bytes() + .any(|byte| byte.is_ascii_alphabetic() && !matches!(byte, b'e' | b'E')) + { + return Err(format!( + "pattern {name}={text:?} uses a length unit whose basis this slice does not consume" + )); + } + return Ok(0.0); + } + }; + let axis = match name { + "x" | "width" => reference_box.width, + _ => reference_box.height, + }; + let resolved = match (units, percentage) { + (PatternUnits::ObjectBoundingBox, false) => parsed * axis, + (PatternUnits::ObjectBoundingBox, true) => resolve_geometry_percentage(parsed, axis), + (PatternUnits::UserSpaceOnUse, false) => parsed, + (PatternUnits::UserSpaceOnUse, true) => { + resolve_geometry_percentage(parsed, root_bases.axis(name)) + } + }; + if !resolved.is_finite() { + return Err(format!( + "pattern {name} resolves outside the finite frame range" + )); + } + let outside_used_range = match name { + "x" | "y" => !(WEB_USED_LENGTH_MIN..=WEB_USED_LENGTH_MAX).contains(&resolved), + "width" | "height" => resolved > WEB_USED_LENGTH_MAX, + _ => false, + }; + if outside_used_range { + return Err(format!( + "pattern {name} exceeds the admitted Web used-value range" + )); + } + if (matches!(name, "x" | "y") || resolved > 0.0) + && geometry_number_source_loses_provenance(number, percentage) + { + return Err(format!( + "pattern {name} numeric precision alias loses Chromium used-value provenance" + )); + } + Ok(resolved) +} + +fn pattern_view_box(chain: &[HtmlElement<'_>]) -> PatternViewBox { + let Some(text) = pattern_chain_attr(chain, "viewBox") else { + return PatternViewBox::None; + }; + let Some(values) = crate::svg_number_list::parse(&text) else { + return PatternViewBox::None; + }; + let [x, y, width, height] = values.as_slice() else { + return PatternViewBox::None; + }; + if ![*x, *y, *width, *height].into_iter().all(f32::is_finite) { + return PatternViewBox::None; + } + if *width == 0.0 || *height == 0.0 { + PatternViewBox::Degenerate + } else if *width < 0.0 || *height < 0.0 { + PatternViewBox::None + } else { + PatternViewBox::Mapped((*x, *y, *width, *height)) + } +} + /// The recursive descent that materializes shapes in painter order. /// /// Containers are **flattened** wherever flattening is exact: a `` @@ -2705,6 +3394,9 @@ struct ChildWalk<'a> { /// Same-document filter graphs. Authored lookup and named results resolve /// here; only checked numeric operation nodes cross into `rframe`. filters: &'a filter_resource::Resources<'a>, + /// Per-client SVG pattern resolver. It compiles each selected source + /// subtree into a nested source-neutral frame program. + patterns: &'a PatternCompiler<'a>, /// Complete geometry boxes of expanded `` instances, in each use /// element's own user space. They are measured before paint resolution so /// a context URL never learns its reference box from whichever leaf @@ -2736,6 +3428,9 @@ struct ChildWalk<'a> { /// Mask resources currently compiling as source images. The stack makes /// descendant cycles and pathological nesting explicit refusals. active_masks: Vec, + /// Pattern resources whose source programs are currently compiling. + /// Nested pattern paints may recurse, but a source cycle never can. + active_patterns: Vec, next_id: u64, } @@ -3055,6 +3750,13 @@ impl<'a> ChildWalk<'a> { child = c.next_element_sibling(); continue; } + // `` is likewise a never-rendered paint resource. Its + // selected content subtree is compiled only for an actual paint + // client, in that client's coordinate system. + if tag == "pattern" { + child = c.next_element_sibling(); + continue; + } // `` renders as a container exactly like `` (SVG2 §16.2: // its `href` is interaction, not paint), so the two share the // one container compiler and its patrols. `` is a @@ -3405,6 +4107,8 @@ impl<'a> ChildWalk<'a> { &mut self.next_id, self.values, self.servers, + self.patterns, + &self.active_patterns, &self.paint_contexts, self.bases, mask.is_some() || filter.is_some(), @@ -7443,6 +8147,8 @@ fn compile_shape( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7465,6 +8171,8 @@ fn compile_shape( next_id, values, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7477,6 +8185,8 @@ fn compile_shape( next_id, values, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7489,6 +8199,8 @@ fn compile_shape( next_id, values, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7500,6 +8212,8 @@ fn compile_shape( context_paint_transform, next_id, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7512,6 +8226,8 @@ fn compile_shape( next_id, values, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7525,6 +8241,8 @@ fn compile_shape( next_id, values, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7537,6 +8255,8 @@ fn compile_shape( next_id, PointsClosure::Closed, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7549,6 +8269,8 @@ fn compile_shape( next_id, PointsClosure::Open, servers, + patterns, + active_patterns, paint_contexts, bases, defer_own_opacity, @@ -7582,6 +8304,8 @@ fn compile_text( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7607,6 +8331,8 @@ fn compile_text( "text", None, servers, + patterns, + active_patterns, paint_contexts, Rectangle::from_xywh(0.0, 0.0, 1.0, 1.0), context_paint_transform, @@ -7693,6 +8419,8 @@ fn compile_text( next_id, Strokable::RenderingDisabled, servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -7713,6 +8441,8 @@ fn compile_rect( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7749,6 +8479,8 @@ fn compile_rect( next_id, box_strokable(w, h), servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -7791,6 +8523,8 @@ fn compile_circle( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7829,6 +8563,8 @@ fn compile_circle( next_id, box_strokable(r, r), servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -7849,6 +8585,8 @@ fn compile_ellipse( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7893,6 +8631,8 @@ fn compile_ellipse( next_id, box_strokable(rx, ry), servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -7923,6 +8663,8 @@ fn compile_path( context_paint_transform: AffineTransform, next_id: &mut u64, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -7967,6 +8709,8 @@ fn compile_path( next_id, Strokable::Yes, servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -8000,6 +8744,8 @@ fn compile_line( next_id: &mut u64, values: &EffectiveValues, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -8039,6 +8785,8 @@ fn compile_line( next_id, Strokable::Yes, servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -8085,6 +8833,8 @@ fn compile_points_shape( next_id: &mut u64, closure: PointsClosure, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, defer_own_opacity: bool, @@ -8165,6 +8915,8 @@ fn compile_points_shape( next_id, Strokable::Yes, servers, + patterns, + active_patterns, paint_contexts, bases, if defer_own_opacity { @@ -8288,6 +9040,8 @@ fn shape_node( next_id: &mut u64, strokable: Strokable, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], bases: PercentBases, own_opacity: f32, @@ -8298,6 +9052,8 @@ fn shape_node( let mut paints = resolve_fill( el, servers, + patterns, + active_patterns, paint_contexts, rect, context_paint_transform, @@ -8310,6 +9066,8 @@ fn shape_node( &el.local_name_string(), Some(&geometry), servers, + patterns, + active_patterns, paint_contexts, rect, context_paint_transform, @@ -8340,6 +9098,8 @@ fn shape_node( paints = resolve_fill( el, servers, + patterns, + active_patterns, paint_contexts, rect, context_paint_transform, @@ -8352,6 +9112,8 @@ fn shape_node( &el.local_name_string(), Some(&geometry), servers, + patterns, + active_patterns, paint_contexts, rect, context_paint_transform, @@ -8363,6 +9125,15 @@ fn shape_node( } } + if let Some(pattern) = paints.pattern() { + patrol_pattern_target_mapping(pattern, viewport) + .map_err(|reason| CompileError::UnsupportedFill(reason.to_string()))?; + } + if let Some(pattern) = stroke.as_ref().and_then(|stroke| stroke.paints().pattern()) { + patrol_pattern_target_mapping(pattern, viewport) + .map_err(|reason| CompileError::UnsupportedStroke(reason.to_string()))?; + } + let visual_id = *next_id + 1; let node = FrameNode { owner: VisualRef::new(Identity::new(visual_id), Provenance::new(visual_id)), @@ -8682,6 +9453,8 @@ fn context_reference_space( fn resolve_fill( el: HtmlElement<'_>, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], consumer_box: Rectangle, destination_to_frame: AffineTransform, @@ -8711,6 +9484,7 @@ fn resolve_fill( else { return Ok(PaintStack::empty()); }; + let selected_through_context = selected.context.is_some(); let owner_data = selected .owner .borrow_data() @@ -8735,12 +9509,15 @@ fn resolve_fill( SVGPaintKind::PaintServer(url) => { match resolve_paint_server_stack( servers, + patterns, + active_patterns, url, || context_reference_space(selected.context, consumer_box, destination_to_frame), consumer_box, bases, paint_opacity, extra_opacity, + selected_through_context, "fill", )? { Some(stack) => Ok(stack.with_alpha_factor( @@ -8762,12 +9539,15 @@ fn resolve_fill( /// nothings paint nothing and deliberately do not fall back). fn resolve_paint_server_stack( servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], url: &style::values::computed::url::ComputedUrl, reference_space: impl FnOnce() -> Result, String>, destination_box: Rectangle, bases: PercentBases, paint_opacity: f32, post_paint_opacity: f32, + selected_through_context: bool, property: &str, ) -> Result, CompileError> { let refusal = |reason: String| match property { @@ -8783,34 +9563,64 @@ fn resolve_paint_server_stack( are not resolved" ))); }; - let valid_gradient = crate::svg_paint_server::classify(servers, fragment) + let classified = crate::svg_paint_server::classify(servers, fragment) .map_err(|reason| refusal(format!("url(#{fragment}): {reason}")))?; - if !valid_gradient { - return Ok(None); + match classified { + ClassifiedServer::Invalid => Ok(None), + ClassifiedServer::Gradient => { + let gradient_bases = GradientBases { + width: bases.width, + height: bases.height, + }; + let resolved = crate::svg_paint_server::resolve( + servers, + fragment, + destination_box, + reference_space, + gradient_bases, + paint_opacity, + post_paint_opacity, + ) + .map_err(|reason| refusal(format!("url(#{fragment}): {reason}")))?; + Ok(match resolved { + ResolvedPaintServer::Invalid => None, + ResolvedPaintServer::Nothing => Some(PaintStack::empty()), + ResolvedPaintServer::Solid(color) => Some(PaintStack::solid(color)), + ResolvedPaintServer::Gradient(paint) => Some( + PaintStack::try_from_paints(cg::Paints::new([paint])) + .map_err(|error| refusal(error.to_string()))?, + ), + }) + } + ClassifiedServer::Pattern(first) => { + if selected_through_context { + return Err(refusal(format!( + "url(#{fragment}) resolves to a pattern paint selected through context-fill/context-stroke, outside the admitted pattern composition slice" + ))); + } + let Some((reference_box, owner_to_destination)) = reference_space() + .map_err(|reason| refusal(format!("url(#{fragment}): {reason}")))? + else { + // A valid server through a singular context destination paints + // nothing and does not select fallback, matching gradients. + return Ok(Some(PaintStack::empty())); + }; + match patterns + .resolve( + fragment, + first, + reference_box, + owner_to_destination, + paint_opacity, + active_patterns, + ) + .map_err(|reason| refusal(format!("url(#{fragment}): {reason}")))? + { + PatternResolution::Invalid => Ok(None), + PatternResolution::Paint(pattern) => Ok(Some(PaintStack::from_pattern(pattern))), + } + } } - let gradient_bases = GradientBases { - width: bases.width, - height: bases.height, - }; - let resolved = crate::svg_paint_server::resolve( - servers, - fragment, - destination_box, - reference_space, - gradient_bases, - paint_opacity, - post_paint_opacity, - ) - .map_err(|reason| refusal(format!("url(#{fragment}): {reason}")))?; - Ok(match resolved { - ResolvedPaintServer::Invalid => None, - ResolvedPaintServer::Nothing => Some(PaintStack::empty()), - ResolvedPaintServer::Solid(color) => Some(PaintStack::solid(color)), - ResolvedPaintServer::Gradient(paint) => Some( - PaintStack::try_from_paints(cg::Paints::new([paint])) - .map_err(|error| refusal(error.to_string()))?, - ), - }) } /// Length units whose basis this build does not have, and which therefore must @@ -9521,6 +10331,8 @@ fn resolve_stroke( element_name: &str, path_length_geometry: Option<&Geometry>, servers: &PaintServers<'_>, + patterns: &PatternCompiler<'_>, + active_patterns: &[NodeId], paint_contexts: &[PaintContext<'_>], consumer_box: Rectangle, destination_to_frame: AffineTransform, @@ -9547,6 +10359,7 @@ fn resolve_stroke( else { return Ok(None); }; + let selected_through_context = selected.context.is_some(); let owner_data = selected .owner .borrow_data() @@ -9573,12 +10386,15 @@ fn resolve_stroke( // inked reach beyond it pads (measured). match resolve_paint_server_stack( servers, + patterns, + active_patterns, url, || context_reference_space(selected.context, consumer_box, destination_to_frame), consumer_box, bases, paint_opacity, extra_opacity, + selected_through_context, "stroke", )? { Some(stack) => stack.with_alpha_factor( diff --git a/crates/websem/src/svg_paint_server.rs b/crates/websem/src/svg_paint_server.rs index 75b3a2b8..341068d7 100644 --- a/crates/websem/src/svg_paint_server.rs +++ b/crates/websem/src/svg_paint_server.rs @@ -117,11 +117,15 @@ enum Server<'d> { element: HtmlElement<'d>, inside_compiled_svg: bool, }, - /// A pattern is a valid SVG paint server, but it is deliberately outside - /// rframe's resolved paint vocabulary. Keeping it in the same first-id - /// table as gradients is load-bearing: otherwise a pattern in `` - /// looks exactly like a missing id and silently becomes fallback/no-paint. - Pattern, + /// A pattern is retained with its element because it resolves per + /// consuming geometry into a checked repeating program. Keeping it in the + /// same first-id table as gradients is load-bearing: otherwise a pattern + /// in `` looks exactly like a missing id and silently becomes + /// fallback/no-paint. + Pattern { + element: HtmlElement<'d>, + inside_compiled_svg: bool, + }, /// An id on any other element makes the reference invalid as a paint /// server. It still occupies the first-id slot, exactly as DOM id lookup /// does, so a later gradient with the same id cannot incorrectly win. @@ -188,7 +192,10 @@ impl<'d> PaintServers<'d> { element: el, inside_compiled_svg: is_inside(el, compiled_svg), }, - "pattern" => Server::Pattern, + "pattern" => Server::Pattern { + element: el, + inside_compiled_svg: is_inside(el, compiled_svg), + }, _ => Server::Other, }; by_fragment.entry(id).or_insert(server); @@ -218,23 +225,60 @@ pub(crate) enum ResolvedPaintServer { Gradient(cg::Paint), } +/// Whole-document classification of one same-document paint-server id. +#[derive(Clone, Copy)] +pub(crate) enum ClassifiedServer<'d> { + Invalid, + Gradient, + Pattern(HtmlElement<'d>), +} + /// Classification that must happen before context-box rebasing. It preserves /// each construct's own outcome when a context relation selects it: an /// external URL stays external, a pattern stays a pattern refusal, and a /// missing/non-server id remains invalid so the authored fallback can fire. -pub(crate) fn classify(servers: &PaintServers<'_>, fragment: &str) -> Result { +pub(crate) fn classify<'d>( + servers: &PaintServers<'d>, + fragment: &str, +) -> Result, String> { match servers.by_fragment.get(fragment) { - None | Some(Server::Other) => Ok(false), - Some(Server::Pattern) => Err(format!( - "url(#{fragment}) resolves to a paint server, which the resolved frame cannot express" + None | Some(Server::Other) => Ok(ClassifiedServer::Invalid), + Some(Server::Pattern { + inside_compiled_svg: false, + .. + }) => Err(format!( + "url(#{fragment}) resolves outside the compiled SVG subtree, which contributes nothing" )), + Some(Server::Pattern { element, .. }) => Ok(ClassifiedServer::Pattern(*element)), Some(Server::Gradient { inside_compiled_svg: false, .. }) => Err(format!( "url(#{fragment}) resolves outside the compiled SVG subtree, which contributes nothing" )), - Some(Server::Gradient { .. }) => Ok(true), + Some(Server::Gradient { .. }) => Ok(ClassifiedServer::Gradient), + } +} + +/// Resolve a template-chain edge only when its first-id target is another +/// in-subtree pattern. A wrong-type or missing edge dies; crossing outside the +/// compiled SVG is a named boundary rather than a partial template. +pub(crate) fn pattern_template<'d>( + servers: &PaintServers<'d>, + fragment: &str, +) -> Result>, String> { + match servers.by_fragment.get(fragment) { + Some(Server::Pattern { + element, + inside_compiled_svg: true, + }) => Ok(Some(*element)), + Some(Server::Pattern { + inside_compiled_svg: false, + .. + }) => Err(format!( + "pattern template #{fragment} resolves outside the compiled SVG subtree" + )), + _ => Ok(None), } } @@ -275,7 +319,7 @@ pub(crate) fn resolve( element, inside_compiled_svg, } => (*element, *inside_compiled_svg), - Server::Pattern => { + Server::Pattern { .. } => { return Err(format!( "url(#{fragment}) resolves to a paint server, which the resolved frame cannot express" )); @@ -381,7 +425,7 @@ fn template_chain<'d>(servers: &PaintServers<'d>, first: HtmlElement<'d>) -> Vec let mut visited: HashSet = HashSet::from([first.node_id()]); let mut current = first; loop { - let Some(reference) = gradient_href(current) else { + let Some(reference) = paint_server_href(current) else { break; }; let Some(fragment) = reference.strip_prefix('#') else { @@ -408,7 +452,7 @@ fn template_chain<'d>(servers: &PaintServers<'d>, first: HtmlElement<'d>) -> Vec } /// `href` beats `xlink:href` when both are present (measured). -fn gradient_href(el: HtmlElement<'_>) -> Option { +pub(crate) fn paint_server_href(el: HtmlElement<'_>) -> Option { if let DemoNodeData::Element(e) = &el.dom_node().data { let mut xlink = None; for attr in &e.attrs { diff --git a/crates/websem/tests/context_paint_contract.rs b/crates/websem/tests/context_paint_contract.rs index af158d76..c3b64e80 100644 --- a/crates/websem/tests/context_paint_contract.rs +++ b/crates/websem/tests/context_paint_contract.rs @@ -330,7 +330,7 @@ fn pattern_and_external_urls_keep_their_own_refusal_through_context() { r##""##, )); assert!( - matches!(pattern, CompileError::UnsupportedFill(ref reason) if reason.contains("")), + matches!(pattern, CompileError::UnsupportedFill(ref reason) if reason.contains("pattern paint selected through context-fill/context-stroke")), "{pattern}" ); diff --git a/crates/websem/tests/mask_contract.rs b/crates/websem/tests/mask_contract.rs index 9f6cf09f..7031b6c9 100644 --- a/crates/websem/tests/mask_contract.rs +++ b/crates/websem/tests/mask_contract.rs @@ -246,6 +246,23 @@ fn the_mask_resources_own_css_filter_is_inert() { ); } +#[test] +fn a_pattern_is_an_admitted_mask_source_paint() { + let frame = admit_both(&document( + r##" + + + + + + + "##, + )); + let pixels = render_through_n0(&frame, 64, 64); + assert_eq!(at(&pixels, 10, 32), [0, 0, 0, 255]); + assert_eq!(at(&pixels, 14, 32), [255, 255, 255, 255]); +} + #[test] fn unsupported_mask_routes_skip_the_whole_target_by_stable_name() { let target = |mask: &str, target_extra: &str| { @@ -277,13 +294,6 @@ fn unsupported_mask_routes_skip_the_whole_target_by_stable_name() { ), "unimplemented Web used-length range", ), - ( - target( - r##""##, - "", - ), - "mask source cannot be compiled completely", - ), ( target( r##""##, diff --git a/crates/websem/tests/pattern_contract.rs b/crates/websem/tests/pattern_contract.rs new file mode 100644 index 00000000..e92e93c9 --- /dev/null +++ b/crates/websem/tests/pattern_contract.rs @@ -0,0 +1,378 @@ +//! SVG pattern laws at the Web-semantic contract boundary. +//! +//! Chromium probes decide URL fallback, template ownership, coordinate +//! systems, cascade precedence, and the pinned picture-shader precision +//! envelope. These tests pin the resulting source-neutral program and every +//! stable refusal; committed Web-first cells separately grade the pixels. + +#[allow(dead_code)] +mod support; + +use cg::{CGColor, Paint}; +use rframe::{Frame, PatternPaint}; +use support::render_through_n0; +use websem::{DegradationAction, InitialViewport, SvgFrameSource}; + +fn viewport() -> InitialViewport { + InitialViewport::new(64.0, 64.0) +} + +fn document(body: &str) -> String { + format!( + r##" +{body} +"## + ) +} + +fn admit_both(source: &str) -> Frame { + let strict = SvgFrameSource::from_standalone_svg(source, viewport()).expect("strict admits"); + let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport()) + .expect("best effort admits"); + let static_degradations: Vec<_> = best + .degradations() + .iter() + .filter(|degradation| degradation.action() != DegradationAction::SamplesAsBase) + .collect(); + assert!( + static_degradations.is_empty(), + "an admitted pattern declares nothing static: {static_degradations:?}" + ); + let frame = strict.base_frame(); + assert_eq!(frame, best.base_frame(), "admissions are frame-identical"); + frame +} + +fn assert_target_skip(source: &str, reason: &str) { + let strict = + SvgFrameSource::from_standalone_svg(source, viewport()).expect_err("strict must refuse"); + assert!(strict.to_string().contains(reason), "{strict}"); + + let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport()) + .expect("best effort declares the affected target"); + let skipped: Vec<_> = best + .degradations() + .iter() + .filter(|degradation| degradation.action() == DegradationAction::Skipped) + .collect(); + assert_eq!(skipped.len(), 1, "one affected target: {skipped:?}"); + assert!( + skipped[0].reason().contains(reason), + "{}", + skipped[0].reason() + ); + assert_eq!( + best.base_frame().nodes().len(), + 1, + "the explicit backdrop survives" + ); +} + +fn pattern_of(frame: &Frame, node: usize) -> &PatternPaint { + frame.nodes()[node] + .paints + .pattern() + .expect("resolved pattern paint") +} + +fn solid_of(paint: &Paint) -> CGColor { + match paint { + Paint::Solid(solid) => solid.color, + other => panic!("expected a solid, got {other:?}"), + } +} + +fn at(pixels: &[u8], x: usize, y: usize) -> [u8; 4] { + let offset = (y * 64 + x) * 4; + pixels[offset..offset + 4].try_into().expect("RGBA pixel") +} + +#[test] +fn a_same_document_pattern_becomes_one_repeating_local_program() { + let frame = admit_both(&document( + r##" + + + + + + "##, + )); + let pattern = pattern_of(&frame, 0); + assert_eq!((pattern.width(), pattern.height()), (8.0, 8.0)); + assert_eq!(pattern.transform().matrix[0][2], 2.0); + assert_eq!(pattern.transform().matrix[1][2], 3.0); + assert_eq!(pattern.items().nodes().count(), 2); + + let pixels = render_through_n0(&frame, 64, 64); + assert_eq!(at(&pixels, 2, 4), [0xef, 0x44, 0x44, 0xff]); + assert_eq!(at(&pixels, 6, 4), [0x22, 0xc5, 0x5e, 0xff]); + assert_eq!(at(&pixels, 10, 4), [0xef, 0x44, 0x44, 0xff]); + assert_eq!( + pixels, + render_through_n0(&frame, 64, 64), + "fresh pattern replay is byte-identical" + ); +} + +#[test] +fn invalid_and_valid_empty_patterns_select_different_fallback_outcomes() { + let invalid = admit_both(&document( + r##" + "##, + )); + let fallback = invalid.nodes()[0] + .paints + .iter() + .next() + .expect("invalid pattern selects the authored fallback"); + assert_eq!(solid_of(fallback), CGColor::from_rgb(0xef, 0x44, 0x44)); + + let valid_empty = admit_both(&document( + r##" selected local content + "##, + )); + let pattern = pattern_of(&valid_empty, 0); + assert!( + pattern.items().is_empty(), + "the selected tile is transparent" + ); + assert_eq!(valid_empty.nodes()[0].paints.iter().count(), 0); +} + +#[test] +fn object_box_units_resolve_once_per_consuming_geometry() { + let frame = admit_both(&document( + r##" + + + + + + "##, + )); + assert_eq!(frame.nodes().len(), 2); + assert_eq!( + ( + pattern_of(&frame, 0).width(), + pattern_of(&frame, 0).height() + ), + (8.0, 8.0) + ); + assert_eq!( + ( + pattern_of(&frame, 1).width(), + pattern_of(&frame, 1).height() + ), + (16.0, 8.0) + ); +} + +#[test] +fn the_pattern_transform_hint_uses_the_one_transform_cascade() { + let frame = admit_both(&document( + r##" + + + + + "##, + )); + assert_eq!( + pattern_of(&frame, 0).transform().matrix, + [[1.0, 0.0, 12.0], [0.0, 1.0, 0.0]], + "the author declaration beats patternTransform; plain transform is inert" + ); +} + +#[test] +fn percentage_pattern_transform_spellings_split_before_the_frame() { + let invalid_attribute = admit_both(&document( + r##" + + + + + + "##, + )); + assert_eq!( + pattern_of(&invalid_attribute, 0).transform().matrix, + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + "the invalid presentation-attribute percentage drops to identity" + ); + + assert_target_skip( + &document( + r##" + + + + + + + "##, + ), + "pattern transform percentage has no proved reference-box basis", + ); +} + +#[test] +fn a_ninth_distinct_pattern_refuses_before_its_source_starts_compiling() { + let mut defs = String::new(); + for index in 0..9 { + let child = if index == 8 { + // If the ninth source starts compiling, the circle reaches the + // independent source-coverage patrol before contract construction. + // The program-depth refusal must win first. + r##""##.to_string() + } else { + format!( + r##""##, + index + 1 + ) + }; + defs.push_str(&format!( + r##"{child}"## + )); + } + + assert_target_skip( + &document(&format!( + r##" + {defs} + "## + )), + "nested pattern paint chain exceeds the resolved 8-program limit", + ); +} + +#[test] +fn href_beats_xlink_and_the_first_local_content_owner_wins() { + let frame = admit_both(&document( + r##" + + + + + "##, + )); + let source = pattern_of(&frame, 0); + let paint = source + .items() + .nodes() + .next() + .unwrap() + .paints + .iter() + .next() + .unwrap(); + assert_eq!(solid_of(paint), CGColor::from_rgb(255, 0, 0)); +} + +#[test] +fn every_measured_picture_shader_boundary_refuses_in_both_admissions() { + let cases = [ + ( + r##""##, + "source-coverage precision boundary", + ), + ( + r##""##, + "source-coverage precision boundary", + ), + ( + r##""##, + "source-effect precision boundary", + ), + ( + r##""##, + "filter composition outside the admitted pattern source slice", + ), + ( + r##""##, + "composition precision boundary", + ), + ( + r##""##, + "composition precision boundary", + ), + ( + r##""##, + "affine precision boundary", + ), + ( + r##""##, + "sampling precision boundary", + ), + ]; + + for (defs, reason) in cases { + assert_target_skip( + &document(&format!( + r##" + {defs} + "## + )), + reason, + ); + } +} + +#[test] +fn unresolved_length_contexts_refuse_by_the_exact_pattern_field() { + for (value, reason) in [ + ( + ".2cm", + "length unit whose basis this slice does not consume", + ), + ("calc(4px + 4px)", "pattern width uses a CSS function"), + ("var(--w)", "pattern width resolves through var()"), + ("inherit", "pattern width uses the CSS-wide value"), + ("/**/8/**/", "pattern width contains a CSS comment"), + ] { + assert_target_skip( + &document(&format!( + r##" + + "## + )), + reason, + ); + } +} + +#[test] +fn external_template_dependency_and_raw_number_aliases_are_named() { + assert_target_skip( + &document( + r##" + + "##, + ), + "external template", + ); + assert_target_skip( + &document( + r##" + + "##, + ), + "numeric precision alias", + ); + for value in ["1000000000", "33554430", "-1000000000", "1e100"] { + assert_target_skip( + &document(&format!( + r##" + + "## + )), + "admitted Web used-value range", + ); + } +} diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index 28a39252..55aca627 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -529,11 +529,6 @@ const CORPUS: &[(&str, Departure, &str)] = &[ "source-side cascade effect is not represented", ), ("svg-mask-root", BothRefuse, "root "), - ( - "svg-mask-source-pattern", - DeclaredByBestEffort, - "mask source cannot be compiled completely", - ), ( "svg-mask-transform-precision", DeclaredByBestEffort, @@ -569,9 +564,104 @@ const CORPUS: &[(&str, Departure, &str)] = &[ // rung consumed defs; the marker attribute itself is the named hole. ("svg-path-marker-end", DeclaredByBestEffort, "marker-end"), ( - "svg-pattern-paint-server", + "svg-pattern-affine-precision", + DeclaredByBestEffort, + "picture-shader affine precision boundary", + ), + ( + "svg-pattern-context-selection", + DeclaredByBestEffort, + "pattern paint selected through context-fill/context-stroke", + ), + ( + "svg-pattern-css-transform-percentage", + DeclaredByBestEffort, + "pattern transform percentage has no proved reference-box basis", + ), + ( + "svg-pattern-external", + DeclaredByBestEffort, + "external template", + ), + ( + "svg-pattern-length-calc", + DeclaredByBestEffort, + "pattern width uses a CSS function", + ), + ( + "svg-pattern-length-css-comments", + DeclaredByBestEffort, + "pattern width contains a CSS comment", + ), + ( + "svg-pattern-length-css-wide", + DeclaredByBestEffort, + "pattern width uses the CSS-wide value", + ), + ( + "svg-pattern-length-unit", + DeclaredByBestEffort, + "length unit whose basis this slice does not consume", + ), + ( + "svg-pattern-length-used-range", + DeclaredByBestEffort, + "pattern x exceeds the admitted Web used-value range", + ), + ( + "svg-pattern-length-var", + DeclaredByBestEffort, + "pattern width resolves through var()", + ), + ( + "svg-pattern-nested-composition-precision", + DeclaredByBestEffort, + "picture-shader composition precision boundary", + ), + ( + "svg-pattern-nesting-too-deep", + DeclaredByBestEffort, + "nested pattern paint chain exceeds the resolved 8-program limit", + ), + ( + "svg-pattern-number-precision-alias", + DeclaredByBestEffort, + "numeric precision alias", + ), + ( + "svg-pattern-source-clip-precision", + DeclaredByBestEffort, + "picture-shader source-effect precision boundary", + ), + ( + "svg-pattern-source-coverage-precision", + DeclaredByBestEffort, + "picture-shader source-coverage precision boundary", + ), + ( + "svg-pattern-source-effect-precision", + DeclaredByBestEffort, + "picture-shader source-effect precision boundary", + ), + ( + "svg-pattern-source-filter", + DeclaredByBestEffort, + "filter composition outside the admitted pattern source slice", + ), + ( + "svg-pattern-source-unsupported", + DeclaredByBestEffort, + "source cannot compile completely", + ), + ( + "svg-pattern-tile-sampling-precision", + DeclaredByBestEffort, + "picture-shader sampling precision boundary", + ), + ( + "svg-pattern-transform-none-provenance", DeclaredByBestEffort, - "", + "transform:none on a derived pattern", ), ( "svg-points-odd-coordinate", diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 03b7a21c..28bcd13c 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -43,8 +43,11 @@ from the dated addenda below: - **The admitted slice** is ``, ``, ``, ``, ``, `` and ``, filled and stroked — solid or gradient paint (``/`` paint servers, the - gradient rung), including `context-fill`/`context-stroke` selected through - same-document use instances and fully resolved before the frame; with + gradient rung), or a bounded same-document repeating vector pattern resolved + per client through both pattern unit systems, template chains, `viewBox`, + and the admitted transform envelope; including + `context-fill`/`context-stroke` selected through same-document use instances + and fully resolved before the frame; with centred stroke geometry, the closed cap/join family, opacity, and resolved dash patterns with a checked cycle, signed local-space phase, and `pathLength` source calibration; @@ -77,14 +80,14 @@ from the dated addenda below: the full `preserveAspectRatio` grammar; and one exact-time `` on a top-level ``. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 812 Chromium-baked primitive cells plus 10 sampled frames. +- **The corpus** is 874 Chromium-baked primitive cells plus 10 sampled frames. All byte-exact except seven curved cells carrying a declared, geometrically confined tolerance (the native-oval/conic boundary) and four gradient cells carrying a declared one-code-value ramp-quantization tolerance (one pixel against Chromium's Skia; 18 knife-edge pixels between this engine's own macOS and Linux Skia builds; 336 ramp pixels under an isolated layer's restore; 576 after a masked ramp becomes luminance alpha). The named refusal - register has 152 rows. + register has 170 rows. - **Not claimed:** no conformance score exists or may be computed — FLIP is unratified. The FLIP record and identity-changing review are prepared, but only the owner act on gridaco/nothing#49 may authorize them and the first @@ -3765,3 +3768,155 @@ primitive corpus moves from 741 to 812 cells; the ten exact-time sampled frames are unchanged. Exactly twelve checklist rows tick: the four elements and eight attributes named above. This records no conformance score and takes no FLIP action. + +## Rung: SVG repeating vector patterns (2026-08-27) + +The verdict is CLOSE/SPLIT. The complete case-sensitive +`userSpaceOnUse | objectBoundingBox` grammars close the `patternUnits` and +`patternContentUnits` rows, including their different initials and invalid +fallback. `` stays open because valid source programs and external +template dependencies remain outside the admitted source envelope. +`patternTransform` stays open because valid general rotations and shears cross +a measured backend precision boundary. The shared `x`, `y`, `width`, `height`, +`href`, `viewBox`, value-type, resource, and dynamics rows remain open for +their wider applicability. No presentation-property row closes. + +The resolved seam carries no SVG resource. `websem` resolves one pattern for +each consuming fill or stroke into a finite positive tile, one finite +invertible tile-to-client map, and a bounded immutable `rframe::FrameItems` +program in tile-local coordinates. `rframe` carries that checked program and +no id, URL, element, cascade fact, or backend object. `n0` compiles the nested +items through its ordinary private drawlist, records them into one tile +picture, and repeats the picture through the same painter. Nested programs are +bounded at eight levels and recursively preflighted before the first raster +command. Thus the contract still refuses a paint that *references* a resource; +the new fact is the fully resolved paint program, not a resource handle. + +Chromium 149.0.7827.55 establishes per-client resolution. Under +`objectBoundingBox`, `x`, `y`, `width`, and `height` map against each consuming +geometry box; under `userSpaceOnUse`, numbers and percentages use the target's +user coordinate system and independent axis bases. Negative origins are +ordinary. Missing, zero, negative, malformed, or non-finite tile extents make +the server invalid and activate the authored paint fallback. A valid pattern +whose selected content owner has no painting children is instead transparent, +with the fallback inert. Dedicated cells distinguish both outcomes. + +Same-document lookup is first-id-wins. Plain `href` takes precedence over +`xlink:href`; a template chain inherits each missing attribute independently, +and children come all-or-nothing from the first pattern in the chain that owns +children. A cycle removes only the cyclic edge. The source is non-rendering in +document position and compiles transactionally: if any participating source +child cannot compile, the affected client refuses rather than painting a +plausible partial tile. An external tail refuses whenever local facts may +depend on it, because this compiler owns no resource I/O. + +Pattern `viewBox` uses the already-admitted complete +`preserveAspectRatio` resolver. When present it supersedes +`patternContentUnits`; without one, user-space content remains in the target's +user system while object-box content scales independently by the target box. +`patternTransform` is the transform property's presentation hint on the +pattern element: author CSS beats it, a plain `transform` attribute is inert, +and template ownership remains per attribute. Translation, axis scale, +reflection, and exact quarter turns are admitted. Fills and strokes on rects, +ellipses, and paths, dashes and caps, target opacity/clip/mask/filter scopes, +source gradients, ``, one-draw folded opacity, and a pattern nested alone +inside another pattern all reproduce Chromium exactly inside that envelope. + +The picture-shader boundary was measured rather than generalized from those +successes. A general rotation is content-dependent: two sampled layouts were +exact, while a six-unit grid changed two pixels at maximum channel delta 1 and +a related non-square grid changed one pixel at delta 3. Shear and skew changed +147–222 pixels at delta 2. Fractional final tile extents changed 164–407 +pixels, up to delta 28; fractional root mappings reached delta 4. Curved source +geometry changed 189–315 pixels, up to delta 32. A multi-draw isolated source +opacity changed 1,152–1,728 pixels at delta 2, a circular source clip changed +216 at delta 9, and mixing another draw with a nested pattern changed 108 at +delta 1. Stable affine, tile-sampling, source-coverage, source-effect, and +nested-composition refusals now guard all of those classes in strict and +best-effort admission. They deliberately over-refuse some exact controls +rather than release content-dependent wrong pixels. + +Filter composition inside the source program also remains split. Chromium +honors the sampled safe blur—the filtered and plain controls differ across all +2,304 target pixels at maximum delta 182—and the current nested replay happened +to match that one Chromium raster exactly. One clean blur does not establish +the complete filter graph inside a second picture-shader composition, so the +route now refuses by name pending its own matrix (measured, not celled). + +Unit-bearing tile lengths, CSS math, `var()`, CSS-wide values, and CSS comments +around an otherwise valid length all paint in Chromium and now refuse by their +exact pattern field. The comment witness was a manual-review finding: before +the patrol, both admissions silently selected fallback and differed from +Chromium across all 2,304 target pixels at maximum channel delta 202. Those +gaps retain their own unchecked syntax/value-type rows, following the own-row refusal precedent of +[gridaco/nothing#75](https://github.com/gridaco/nothing/pull/75) and +[gridaco/nothing#80](https://github.com/gridaco/nothing/pull/80). A derived +template with an author stylesheet that may supply `transform:none` also +refuses: the pinned computed representation loses the provenance needed to +distinguish that value from an absent declaration. That valid no-own-row case, +the general-affine boundary, and the remaining valid source programs keep the +element and transform rows open under the partial-rung precedent of +[gridaco/nothing#81](https://github.com/gridaco/nothing/pull/81) and +[gridaco/nothing#89](https://github.com/gridaco/nothing/pull/89). + +The post-PR review probe found a second transform boundary at the CSS ingress. +On a 64-unit viewport and a 14-unit tile, Chromium resolves inline +`transform:translate(50%, 0px)` on the pattern resource exactly like `32px`. +The former route supplied the tile width to the computed transform and selected +`7px`, changing 1,008 target pixels at maximum channel delta 205. A percentage +inside the `patternTransform` presentation attribute itself is invalid and +drops to identity in Chromium. The computed CSS percentage now refuses by a +focused stable name until its reference box can be carried without invention +(measured, not celled). + +The same review found that the eight-program contract bound was enforced only +after recursively compiling every distinct nested source. `websem` now checks +the active pattern stack before measuring or entering a ninth source walk, so +the stable depth refusal wins before unrelated inner-source errors or excessive +work. The active-id check remains independently responsible for cycles. + +A pattern selected outward through `context-fill` or `context-stroke` remains +the next pattern-composition rung. Chromium propagates the server through that +relation; the direct witness differs from no paint by 1,152 pixels at maximum +delta 255. The implementation briefly admitted this route during the contract +spike, but the independent law pass caught that it had no committed matrix. +It now refuses by a focused stable name instead of turning an ungated clean +render into an accidental capability claim (measured, not celled). + +The raw-number normalization crux produced a negative raster verdict. The established +`57384.267578125007%` normalization alias and a direct midpoint-adjacent pair +were amplified through pattern geometry, but Chromium's adjacent controls were +pixel-identical at 64×64. No second *normalization alias* was found. The direct +binary32 decoder still cannot prove which Blink CSS-parser used value entered +the tile, so a conservative one-way provenance patrol remains; this is +measured, not celled, and is not presented as a demonstrated pixel mismatch. + +The range follow-up did find a separate raw-route divergence. Chromium clamps +`x="1000000000"`, `x="33554430"`, `x="-1000000000"`, and the finite source +`x="1e100"` to its signed Web used-length limits. Before the patrol, both +admissions differed from Chromium by 1,152, 1,152, 768, and 2,112 pixels +respectively, all at maximum channel delta 205. Pattern geometry now shares the +conservative used-range refusal already earned by shape geometry rather than +selecting a wrong repetition phase (measured, not celled). + +The scratch matrices were captured twice through the one hash-pinned Chromium +module, and every candidate also rendered through the actual `n0` command in +strict and best-effort modes. Sixty-two cells entered only through `just add`, +including one inline-HTML SVG cell. All sixty-two are exact without a new +tolerance. They cover defaults and errors, both unit systems and independent +clients, every template edge above, all three `viewBox` mappings, cascade and +transform ownership, fill and stroke geometry, target effects, admitted source +programs, nesting, ``, and invalid-fallback versus valid-empty behavior. + +Gate sensitivity required a discriminating mutation. Changing linear tile +filtering to nearest moved none of the admitted cells and was rejected as +evidence. Changing horizontal repetition to clamp then made `just gate` reject +43 pattern cells, with up to 1,824 differing pixels and maximum channel delta +255. Restoring repeat returned the complete 874-cell gate to green. + +The two former broad pattern refusals graduate into cells, while twenty +focused rows name the measured and bounded remainder; the refusal register +therefore moves from 152 to 170. The primitive corpus moves from 812 to 874 cells; the ten +exact-time sampled frames and the 451-cell filter estate are unchanged. +Exactly two checklist rows tick: `patternUnits` and `patternContentUnits`. +This records no conformance score and takes no FLIP action. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 07e9edba..bb27cd92 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -1706,6 +1706,21 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `` - [x] `` - [ ] `` + +> **2026-08-27 split:** same-document patterns now resolve per consuming fill +> or stroke into a bounded, source-neutral repeating vector program. Both unit +> systems, independent clients, template chains, `viewBox` mapping, cascade, +> transforms, ``, effects around the target, nested repetition, and the +> invalid-reference-fallback versus valid-empty distinction are carried by 62 +> exact Chromium cells. The element stays open: external template facts, +> context-selected pattern paint, unsupported source descendants, curved +> source coverage, source filters, isolated source effects, mixed nested +> composition, used-length saturation, fractional tile sampling, and general +> affine mapping refuse by stable name at measured pixel boundaries. A ninth +> distinct nested program now refuses before source compilation begins. Shared +> `x`/`y`/`width`/`height`, `href`, `viewBox`, CSS-token/value-type, resource, and +> dynamics rows remain independently open. + - [x] `` - [x] `` - [x] `` @@ -2156,9 +2171,25 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `orient` - [ ] `path` - [x] `pathLength` -- [ ] `patternContentUnits` +- [x] `patternContentUnits` - [ ] `patternTransform` -- [ ] `patternUnits` +- [x] `patternUnits` + +> **2026-08-27 close/split:** the complete case-sensitive +> `userSpaceOnUse | objectBoundingBox` grammars, their different initials +> (`userSpaceOnUse` for content, `objectBoundingBox` for the tile), and invalid +> fallback are Chromium-baked, so `patternContentUnits` and `patternUnits` +> close. `patternTransform` stays open: translation, axis scale, reflection, +> and exact quarter turns are admitted through the transform presentation +> hint, but valid general rotations and shears cross the measured +> picture-shader precision boundary; one derived-template stylesheet +> `transform:none` provenance case also refuses by name. The shared CSS +> `transform` route remains separate and open: Chromium resolves an inline +> percentage translation on `` against the root viewport, while the +> former tile-width basis changed 1,008 pixels at maximum delta 205. That +> computed percentage now refuses until its reference box is proved rather +> than invented (measured, not celled). + - [ ] `ping` - [ ] `playbackorder` - [ ] `points` diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 61abdf2f..44114fc0 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -245,6 +245,14 @@ is exactly what the engine renders pixel-for-pixel. | `svg-gradient-href-cross-type.svg` | A radial templated on a linear inherits its stops — the href chain crosses gradient types for everything but geometry. | | `svg-gradient-stroke.svg` · `svg-gradient-path-bbox.svg` | The consumers: a gradient stroke's paint box is the geometry's own box (the stroke's inked reach pads beyond it), and a path's paint box anchors at its tight-bounds origin — the glyphless compile's once-deferred decision, taken and baked. | | `svg-gradient-not-in-defs.svg` · `svg-gradient-use-clone-order.svg` · `svg-gradient-stylesheet-fill.svg` | The table: a gradient outside `` is non-rendering in place and referencable; a `` clone of a gradient earlier in expanded order does not shadow the document's element; a stylesheet-authored `fill: url(#…)` resolves identically to the attribute spelling (the two same-document URL bases). | +| `svg-pattern-{repeat,negative-origin,px-lengths,user-percent,object-number,object-percent,object-per-client,invalid-pattern-units,invalid-content-units}.svg` | Tile geometry and the two unit systems: defaults, signed origin, `px`, user-space percentages, object-box numbers and percentages, invalid-enum fallback, and two differently sized clients proving that object-box facts resolve per consumer. All nine are byte-exact. | +| `svg-pattern-{width-absent-fallback,height-absent-fallback,width-zero-fallback,height-zero-fallback,width-negative-fallback,height-negative-fallback,width-malformed-fallback,singular-transform-fallback,degenerate-viewbox-fallback,valid-empty,descriptive-content-drop}.svg` | Error and nothing semantics. Missing, non-positive, malformed, singular, and degenerate servers are invalid and activate their authored fallback. A valid empty source and a source containing only descriptive children instead paint transparent with the fallback inert. All eleven are byte-exact. | +| `svg-pattern-{href-template,xlink-template,href-beats-xlink,href-cycle-content,href-first-present,template-style-owner,duplicate-id-first}.svg` | The same-document template table: both link spellings and precedence, per-attribute first-present inheritance, first content owner, a cycle dropping only its edge, declaration ownership across a chain, and first-id lookup. All seven are byte-exact. | +| `svg-pattern-viewbox-{none,meet,slice,content-user,content-object}.svg` | Pattern `viewBox`: all three aspect-ratio mappings plus evidence that `viewBox` supersedes either `patternContentUnits` branch. All five are byte-exact. | +| `svg-pattern-{transform-hint,transform-css,plain-transform-inert,transform-quarter-turn,transform-axis-scale,target-axis-transform}.svg` | Transform ownership and the admitted mapping envelope: `patternTransform` as a presentation hint, author CSS precedence, inert plain `transform`, exact quarter turn, axis scale, and a mapped target. All six are byte-exact. | +| `svg-pattern-{fill-circle,fill-path-cubic,fill-path-evenodd,fill-rounded-rect,stroke-rect,stroke-ellipse,stroke-dash-round,target-fill-opacity,target-element-opacity,target-clip,target-mask,target-filter,target-group-opacity,fill-stroke-opacity,overflow-default-clip}.svg` | Pattern consumers and outer effect order: every admitted geometry kind, fill rule, rounded geometry, centred and dashed strokes, both paint-opacity stages, target clip/mask/filter/group opacity, fill plus stroke, and default tile clipping. All fifteen are byte-exact. | +| `svg-pattern-{source-one-draw-opacity,source-gradient,source-use,source-mask,nested-repeat,use-translate,use-scale,mask-source}.svg` · `html-inline-svg-pattern.html` | Admitted source programs and host composition: one-draw folded opacity, a gradient, ``, a mask, bounded nested repetition, translated/scaled use instances, a pattern inside a mask source, and the inline-HTML SVG entry. All nine are byte-exact. | +| *(measured, not celled — pattern split)* | Twice-deterministic Chromium 149 probes and both actual CLI admissions establish the conservative picture-shader envelope. General rotation is content-dependent: sampled layouts were exact, while two grids changed 2px/Δ1 and 1px/Δ3. Shear/skew changed 147–222px/Δ2; fractional final tile extents changed 164–407px up to Δ28; curved source geometry changed 189–315px up to Δ32; isolated multi-draw source opacity changed 1,152–1,728px/Δ2; a circular source clip changed 216px/Δ9; and another draw beside a nested pattern changed 108px/Δ1. Stable refusal fixtures guard each class in strict and best-effort modes. A manual-review probe found CSS comments around a valid tile length were honored by Chromium while both admissions silently selected fallback, changing all 2,304 target pixels at Δ202; `svg-pattern-length-css-comments` now names that route. The law pass also restored the planned boundary for a pattern selected through `context-fill`/`context-stroke`: Chromium paints it 1,152px/Δ255 apart from no paint, but the route has no committed composition matrix yet and now refuses by name. Chromium honors a sampled safe blur inside pattern source content (filtered versus plain: 2,304px/Δ182), and this route happened to match that one Chromium raster exactly; it still refuses pending a complete pattern×filter matrix. The used-range follow-up found Chromium clamps huge positive, adjacent, negative, and finite beyond-binary32 `x` sources; the former raw route changed 1,152, 1,152, 768, and 2,112px respectively, all at Δ205, and now refuses. The `57384.267578125007%` and direct midpoint-adjacent number probes found no second normalization alias at 64×64 because Chromium's adjacent controls were pixel-identical; the conservative raw-decoder provenance patrol remains, but no mismatch is claimed for that alias class. PR review added two producer guards. Chromium resolves inline CSS `transform:translate(50%, 0px)` on a 14-unit pattern against the 64-unit viewport, exact to `32px`; the former tile-width basis selected `7px` and changed 1,008px/Δ205, so that CSS route now refuses until its reference box is proved. A ninth distinct nested pattern now refuses before its source walk begins instead of recursing until the completed contract rejects it. Changing tile filtering to nearest moved no admitted cell and was rejected as a sensitivity proof. Changing horizontal repetition to clamp made 43 pattern cells fail, up to 1,824px/Δ255; restoration returned all 874 cells green. The corpus is now 874 cells plus 10 sampled frames, the filter estate remains 451 cells, and the named register has 170 rows. | | `svg-path-cubic-fill.svg` · `svg-path-smooth-cubic.svg` · `svg-path-quadratic.svg` | Curved path cells: a cubic, an `S` continuation, and a `Q`+`T` pair. All three bake **byte-exact** — see the note below. | | `svg-path-fill-rule-nonzero.svg` · `svg-path-fill-rule-evenodd.svg` · `svg-path-fill-rule-inherited.svg` | One self-intersecting star under each fill rule (core filled vs hollow), and the rule inherited from a `` through the one cascade. | | `svg-path-two-subpaths.svg` · `svg-path-in-scaled-group.svg` | Two closed contours in one `d`, and a path carried by a group's `scale(2)`. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 949ff529..ef1f9abe 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 (812) +## Chromium-baked cells (874) Each renders byte-exact against its committed Chromium oracle (seven curved cells and four gradient ramps carry a declared, bounded @@ -30,6 +30,7 @@ to its fixture source. No new image is committed for this view. html-inline-svg-ancestor-opacity html-inline-svg-currentcolor-rect +html-inline-svg-pattern html-webpage-mockup svg-anchor-container svg-circle-defaults-clip @@ -663,6 +664,67 @@ to its fixture source. No new image is committed for this view. svg-path-smooth-cubic svg-path-two-subpaths svg-path-unclosed-fill +svg-pattern-degenerate-viewbox-fallback +svg-pattern-descriptive-content-drop +svg-pattern-duplicate-id-first +svg-pattern-fill-circle +svg-pattern-fill-path-cubic +svg-pattern-fill-path-evenodd +svg-pattern-fill-rounded-rect +svg-pattern-fill-stroke-opacity +svg-pattern-height-absent-fallback +svg-pattern-height-negative-fallback +svg-pattern-height-zero-fallback +svg-pattern-href-beats-xlink +svg-pattern-href-cycle-content +svg-pattern-href-first-present +svg-pattern-href-template +svg-pattern-invalid-content-units +svg-pattern-invalid-pattern-units +svg-pattern-mask-source +svg-pattern-negative-origin +svg-pattern-nested-repeat +svg-pattern-object-number +svg-pattern-object-per-client +svg-pattern-object-percent +svg-pattern-overflow-default-clip +svg-pattern-plain-transform-inert +svg-pattern-px-lengths +svg-pattern-repeat +svg-pattern-singular-transform-fallback +svg-pattern-source-gradient +svg-pattern-source-mask +svg-pattern-source-one-draw-opacity +svg-pattern-source-use +svg-pattern-stroke-dash-round +svg-pattern-stroke-ellipse +svg-pattern-stroke-rect +svg-pattern-target-axis-transform +svg-pattern-target-clip +svg-pattern-target-element-opacity +svg-pattern-target-fill-opacity +svg-pattern-target-filter +svg-pattern-target-group-opacity +svg-pattern-target-mask +svg-pattern-template-style-owner +svg-pattern-transform-axis-scale +svg-pattern-transform-css +svg-pattern-transform-hint +svg-pattern-transform-quarter-turn +svg-pattern-use-scale +svg-pattern-use-translate +svg-pattern-user-percent +svg-pattern-valid-empty +svg-pattern-viewbox-content-object +svg-pattern-viewbox-content-user +svg-pattern-viewbox-meet +svg-pattern-viewbox-none +svg-pattern-viewbox-slice +svg-pattern-width-absent-fallback +svg-pattern-width-malformed-fallback +svg-pattern-width-negative-fallback +svg-pattern-width-zero-fallback +svg-pattern-xlink-template svg-percent-circle-diagonal svg-percent-ellipse svg-percent-line @@ -841,7 +903,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (152) +## The refusal register (170) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -958,7 +1020,6 @@ its row into the cells above. | `svg-mask-region-var` | declared | skipped svg/rect[2]: unsupported SVG mask: mask region x uses var(), whose computed length is not represented at this Stylo pin | | `svg-mask-resource-style-inheritance` | declared | skipped svg/rect[2]: unsupported SVG mask: inline style on declares shape-rendering, whose source-side cascade effect is not represented; skipped svg/rect[3]: unsupported SVG mask: inline style on declares color-interpolation, whose source-side cascade effect is not represented | | `svg-mask-root` | **both refuse** | unsupported SVG mask: mask on the root uses the host CSS-layer coordinate route | -| `svg-mask-source-pattern` | declared | skipped svg/rect[2]: unsupported SVG mask: mask source cannot be compiled completely: unsupported element | | `svg-mask-transform-precision` | declared | skipped svg/rect[2]: unsupported SVG mask: mask region target transform leaves the measured translation/positive-downscale precision envelope | | `svg-mask-type-css` | declared | skipped svg/rect[2]: unsupported SVG mask: CSS mask-type on is not represented by the pinned cascade; use of it is quarantined from the direct attribute decoder | | `svg-mask-type-inherit` | declared | skipped svg/rect[2]: unsupported SVG mask: mask-type presentation attribute uses inherit, whose parent computed value is not represented at this Stylo pin | @@ -967,7 +1028,26 @@ its row into the cells above. | `svg-nested-svg` | declared | skipped svg/svg[1]: unsupported element | | `svg-path-css-d-property` | declared | declaration ignored at svg/style[1]: a stylesheet declares d, which this cascade does not represent; elements it matches render without it | | `svg-path-marker-end` | declared | skipped svg/path[1]: unsupported rendering attribute marker-end on (not yet consumed) | -| `svg-pattern-paint-server` | declared | skipped svg/pattern[1]: unsupported element ; skipped svg/rect[2]: unsupported fill value "url(#p): url(#p) resolves to a paint server, which the resolved frame cannot express" | +| `svg-pattern-affine-precision` | declared | skipped svg/rect[2]: unsupported fill value "target mapping carries a general rotation or shear at the pinned-backend picture-shader affine precision boundary" | +| `svg-pattern-context-selection` | declared | skipped svg/use[1]/rect[1]: unsupported fill value "url(#p) resolves to a pattern paint selected through context-fill/context-stroke, outside the admitted pattern composition slice" | +| `svg-pattern-css-transform-percentage` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern transform percentage has no proved reference-box basis" | +| `svg-pattern-external` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p needs attributes or content from an external template, and external resources are not resolved" | +| `svg-pattern-length-calc` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern width uses a CSS function in \"calc(4px + 4px)\", which this direct decoder does not consume" | +| `svg-pattern-length-css-comments` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern width contains a CSS comment; the direct length decoder does not tokenize comments" | +| `svg-pattern-length-css-wide` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern width uses the CSS-wide value \"inherit\", whose cascaded length route is not represented" | +| `svg-pattern-length-unit` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern width=\".2cm\" uses a length unit whose basis this slice does not consume" | +| `svg-pattern-length-used-range` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern x exceeds the admitted Web used-value range" | +| `svg-pattern-length-var` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern width resolves through var(), which this direct decoder cannot follow" | +| `svg-pattern-nested-composition-precision` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source mixes a nested pattern with another draw at the pinned-backend picture-shader composition precision boundary" | +| `svg-pattern-nesting-too-deep` | declared | skipped svg/rect[2]: unsupported fill value "url(#p0): pattern #p0 source cannot compile completely: unsupported fill value \"url(#p1): pattern #p1 source cannot compile completely: unsupported fill value \\\"url(#p2): pattern #p2 source cannot compile completely: unsupported fill value \\\\\\\"url(#p3): pattern #p3 source cannot compile completely: unsupported fill value \\\\\\\\\\\\\\\"url(#p4): pattern #p4 source cannot compile completely: unsupported fill value \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"url(#p5): pattern #p5 source cannot compile completely: unsupported fill value \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"url(#p6): pattern #p6 source cannot compile completely: unsupported fill value \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"url(#p7): pattern #p7 source cannot compile completely: unsupported fill value \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"url(#p8): nested pattern paint chain exceeds the resolved 8-program limit\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"\\\\\\\\\\\\\\\"\\\\\\\"\\\"\"" | +| `svg-pattern-number-precision-alias` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern x numeric precision alias loses Chromium used-value provenance" | +| `svg-pattern-source-clip-precision` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source carries an isolated opacity or geometric clip at the pinned-backend picture-shader source-effect precision boundary" | +| `svg-pattern-source-coverage-precision` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source carries curved/vector geometry at the pinned-backend picture-shader source-coverage precision boundary" | +| `svg-pattern-source-effect-precision` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source carries an isolated opacity or geometric clip at the pinned-backend picture-shader source-effect precision boundary" | +| `svg-pattern-source-filter` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source uses a filter composition outside the admitted pattern source slice" | +| `svg-pattern-source-unsupported` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p source cannot compile completely: unsupported element " | +| `svg-pattern-tile-sampling-precision` | declared | skipped svg/rect[2]: unsupported fill value "tile has a fractional final device extent at the pinned-backend picture-shader sampling precision boundary" | +| `svg-pattern-transform-none-provenance` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): an author stylesheet may set transform:none on a derived pattern; the empty computed value loses the provenance needed to decide template inheritance" | | `svg-points-odd-coordinate` | declared | skipped svg/polygon[1]: points on is invalid at byte 17 (near "") | | `svg-preserve-aspect-ratio-case-folded` | **both refuse** | preserveAspectRatio "xmidymid meet" is invalid | | `svg-preserve-aspect-ratio-defer` | **both refuse** | preserveAspectRatio "defer xMidYMid meet" is invalid | diff --git a/fixtures/web-first/chromium/html-inline-svg-pattern.png b/fixtures/web-first/chromium/html-inline-svg-pattern.png new file mode 100644 index 00000000..a51d23b8 Binary files /dev/null and b/fixtures/web-first/chromium/html-inline-svg-pattern.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-degenerate-viewbox-fallback.png b/fixtures/web-first/chromium/svg-pattern-degenerate-viewbox-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-degenerate-viewbox-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-descriptive-content-drop.png b/fixtures/web-first/chromium/svg-pattern-descriptive-content-drop.png new file mode 100644 index 00000000..e57117b8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-descriptive-content-drop.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-duplicate-id-first.png b/fixtures/web-first/chromium/svg-pattern-duplicate-id-first.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-duplicate-id-first.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-fill-circle.png b/fixtures/web-first/chromium/svg-pattern-fill-circle.png new file mode 100644 index 00000000..b7e2a70f Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-fill-circle.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-fill-path-cubic.png b/fixtures/web-first/chromium/svg-pattern-fill-path-cubic.png new file mode 100644 index 00000000..894a3ef3 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-fill-path-cubic.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-fill-path-evenodd.png b/fixtures/web-first/chromium/svg-pattern-fill-path-evenodd.png new file mode 100644 index 00000000..b206d643 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-fill-path-evenodd.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-fill-rounded-rect.png b/fixtures/web-first/chromium/svg-pattern-fill-rounded-rect.png new file mode 100644 index 00000000..b6812bad Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-fill-rounded-rect.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-fill-stroke-opacity.png b/fixtures/web-first/chromium/svg-pattern-fill-stroke-opacity.png new file mode 100644 index 00000000..93a002e5 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-fill-stroke-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-height-absent-fallback.png b/fixtures/web-first/chromium/svg-pattern-height-absent-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-height-absent-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-height-negative-fallback.png b/fixtures/web-first/chromium/svg-pattern-height-negative-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-height-negative-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-height-zero-fallback.png b/fixtures/web-first/chromium/svg-pattern-height-zero-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-height-zero-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-href-beats-xlink.png b/fixtures/web-first/chromium/svg-pattern-href-beats-xlink.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-href-beats-xlink.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-href-cycle-content.png b/fixtures/web-first/chromium/svg-pattern-href-cycle-content.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-href-cycle-content.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-href-first-present.png b/fixtures/web-first/chromium/svg-pattern-href-first-present.png new file mode 100644 index 00000000..fdb44aaf Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-href-first-present.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-href-template.png b/fixtures/web-first/chromium/svg-pattern-href-template.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-href-template.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-invalid-content-units.png b/fixtures/web-first/chromium/svg-pattern-invalid-content-units.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-invalid-content-units.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-invalid-pattern-units.png b/fixtures/web-first/chromium/svg-pattern-invalid-pattern-units.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-invalid-pattern-units.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-mask-source.png b/fixtures/web-first/chromium/svg-pattern-mask-source.png new file mode 100644 index 00000000..984f9c39 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-mask-source.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-negative-origin.png b/fixtures/web-first/chromium/svg-pattern-negative-origin.png new file mode 100644 index 00000000..8a1e9440 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-negative-origin.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-nested-repeat.png b/fixtures/web-first/chromium/svg-pattern-nested-repeat.png new file mode 100644 index 00000000..b2a87e44 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-nested-repeat.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-object-number.png b/fixtures/web-first/chromium/svg-pattern-object-number.png new file mode 100644 index 00000000..0389ae77 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-object-number.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-object-per-client.png b/fixtures/web-first/chromium/svg-pattern-object-per-client.png new file mode 100644 index 00000000..16947a4c Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-object-per-client.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-object-percent.png b/fixtures/web-first/chromium/svg-pattern-object-percent.png new file mode 100644 index 00000000..0389ae77 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-object-percent.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-overflow-default-clip.png b/fixtures/web-first/chromium/svg-pattern-overflow-default-clip.png new file mode 100644 index 00000000..f890183b Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-overflow-default-clip.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-plain-transform-inert.png b/fixtures/web-first/chromium/svg-pattern-plain-transform-inert.png new file mode 100644 index 00000000..1c57b76b Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-plain-transform-inert.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-px-lengths.png b/fixtures/web-first/chromium/svg-pattern-px-lengths.png new file mode 100644 index 00000000..2a08445a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-px-lengths.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-repeat.png b/fixtures/web-first/chromium/svg-pattern-repeat.png new file mode 100644 index 00000000..270ca108 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-repeat.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-singular-transform-fallback.png b/fixtures/web-first/chromium/svg-pattern-singular-transform-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-singular-transform-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-source-gradient.png b/fixtures/web-first/chromium/svg-pattern-source-gradient.png new file mode 100644 index 00000000..dc115b9c Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-source-gradient.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-source-mask.png b/fixtures/web-first/chromium/svg-pattern-source-mask.png new file mode 100644 index 00000000..fc08c188 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-source-mask.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-source-one-draw-opacity.png b/fixtures/web-first/chromium/svg-pattern-source-one-draw-opacity.png new file mode 100644 index 00000000..cf5a96b7 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-source-one-draw-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-source-use.png b/fixtures/web-first/chromium/svg-pattern-source-use.png new file mode 100644 index 00000000..4d30b42f Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-source-use.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-stroke-dash-round.png b/fixtures/web-first/chromium/svg-pattern-stroke-dash-round.png new file mode 100644 index 00000000..67c76f2d Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-stroke-dash-round.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-stroke-ellipse.png b/fixtures/web-first/chromium/svg-pattern-stroke-ellipse.png new file mode 100644 index 00000000..fb577717 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-stroke-ellipse.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-stroke-rect.png b/fixtures/web-first/chromium/svg-pattern-stroke-rect.png new file mode 100644 index 00000000..296ca6cf Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-stroke-rect.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-axis-transform.png b/fixtures/web-first/chromium/svg-pattern-target-axis-transform.png new file mode 100644 index 00000000..bde77470 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-axis-transform.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-clip.png b/fixtures/web-first/chromium/svg-pattern-target-clip.png new file mode 100644 index 00000000..a61e7f83 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-clip.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-element-opacity.png b/fixtures/web-first/chromium/svg-pattern-target-element-opacity.png new file mode 100644 index 00000000..f90b660a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-element-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-fill-opacity.png b/fixtures/web-first/chromium/svg-pattern-target-fill-opacity.png new file mode 100644 index 00000000..572fbf0f Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-fill-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-filter.png b/fixtures/web-first/chromium/svg-pattern-target-filter.png new file mode 100644 index 00000000..7863e971 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-filter.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-group-opacity.png b/fixtures/web-first/chromium/svg-pattern-target-group-opacity.png new file mode 100644 index 00000000..5070aa0c Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-group-opacity.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-target-mask.png b/fixtures/web-first/chromium/svg-pattern-target-mask.png new file mode 100644 index 00000000..a9ea09bd Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-target-mask.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-template-style-owner.png b/fixtures/web-first/chromium/svg-pattern-template-style-owner.png new file mode 100644 index 00000000..85b95492 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-template-style-owner.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-transform-axis-scale.png b/fixtures/web-first/chromium/svg-pattern-transform-axis-scale.png new file mode 100644 index 00000000..49a061ea Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-transform-axis-scale.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-transform-css.png b/fixtures/web-first/chromium/svg-pattern-transform-css.png new file mode 100644 index 00000000..50ec30c3 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-transform-css.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-transform-hint.png b/fixtures/web-first/chromium/svg-pattern-transform-hint.png new file mode 100644 index 00000000..8aa8c5e7 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-transform-hint.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-transform-quarter-turn.png b/fixtures/web-first/chromium/svg-pattern-transform-quarter-turn.png new file mode 100644 index 00000000..a2b90139 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-transform-quarter-turn.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-use-scale.png b/fixtures/web-first/chromium/svg-pattern-use-scale.png new file mode 100644 index 00000000..fd7b7df5 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-use-scale.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-use-translate.png b/fixtures/web-first/chromium/svg-pattern-use-translate.png new file mode 100644 index 00000000..a33ce2a9 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-use-translate.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-user-percent.png b/fixtures/web-first/chromium/svg-pattern-user-percent.png new file mode 100644 index 00000000..96ba9502 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-user-percent.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-valid-empty.png b/fixtures/web-first/chromium/svg-pattern-valid-empty.png new file mode 100644 index 00000000..e57117b8 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-valid-empty.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-viewbox-content-object.png b/fixtures/web-first/chromium/svg-pattern-viewbox-content-object.png new file mode 100644 index 00000000..e7a310b1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-viewbox-content-object.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-viewbox-content-user.png b/fixtures/web-first/chromium/svg-pattern-viewbox-content-user.png new file mode 100644 index 00000000..e7a310b1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-viewbox-content-user.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-viewbox-meet.png b/fixtures/web-first/chromium/svg-pattern-viewbox-meet.png new file mode 100644 index 00000000..b29cee77 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-viewbox-meet.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-viewbox-none.png b/fixtures/web-first/chromium/svg-pattern-viewbox-none.png new file mode 100644 index 00000000..e7a310b1 Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-viewbox-none.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-viewbox-slice.png b/fixtures/web-first/chromium/svg-pattern-viewbox-slice.png new file mode 100644 index 00000000..3bc862af Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-viewbox-slice.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-width-absent-fallback.png b/fixtures/web-first/chromium/svg-pattern-width-absent-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-width-absent-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-width-malformed-fallback.png b/fixtures/web-first/chromium/svg-pattern-width-malformed-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-width-malformed-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-width-negative-fallback.png b/fixtures/web-first/chromium/svg-pattern-width-negative-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-width-negative-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-width-zero-fallback.png b/fixtures/web-first/chromium/svg-pattern-width-zero-fallback.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-width-zero-fallback.png differ diff --git a/fixtures/web-first/chromium/svg-pattern-xlink-template.png b/fixtures/web-first/chromium/svg-pattern-xlink-template.png new file mode 100644 index 00000000..03797b0a Binary files /dev/null and b/fixtures/web-first/chromium/svg-pattern-xlink-template.png differ diff --git a/fixtures/web-first/html-inline-svg-pattern.html b/fixtures/web-first/html-inline-svg-pattern.html new file mode 100644 index 00000000..d52e658d --- /dev/null +++ b/fixtures/web-first/html-inline-svg-pattern.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index a21e0a86..0046842e 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": "15ba5c3156f3ed0bfd0f32b6ad773e1d228c0f678396db08c8de1d972cf5bced", "suite": "primitives.json", - "suite_sha256": "7b182ef872ad3dfe4c4baa3a0c07cba9cc7be7fa9acd891d61d7030044ea3125", + "suite_sha256": "25d5f7f1423060a73cbe6b9378e82b92ecf6b5ba93e9edb5555d75d59a34f6ae", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -37,6 +37,15 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-pattern", + "source": "html-inline-svg-pattern.html", + "source_sha256": "e01dc21e24bcb30e8af1cf9ab43f8d5613c56242cba00b06759d04f7ea7eacba", + "oracle": "chromium/html-inline-svg-pattern.png", + "oracle_sha256": "0ac29fe0ee43f998a4dc30d0ee0d4764013d4cb7833cf17f4d6df15ef617cf8c", + "width": 64, + "height": 64 + }, { "id": "html-webpage-mockup", "source": "html-webpage-mockup.html", @@ -5734,6 +5743,555 @@ "width": 64, "height": 64 }, + { + "id": "svg-pattern-degenerate-viewbox-fallback", + "source": "svg-pattern-degenerate-viewbox-fallback.svg", + "source_sha256": "50ed0225e02ac0a0d2107603c56676a9e85c28d94d2702c90913ffa081e42288", + "oracle": "chromium/svg-pattern-degenerate-viewbox-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-descriptive-content-drop", + "source": "svg-pattern-descriptive-content-drop.svg", + "source_sha256": "48fe20380558c8d24d68c4c581c0db418bec049581487e3387a76a91a49abcbe", + "oracle": "chromium/svg-pattern-descriptive-content-drop.png", + "oracle_sha256": "1502272898b22a79c129dc16ab37a1280c353a18767753769bc82532dddd72a7", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-duplicate-id-first", + "source": "svg-pattern-duplicate-id-first.svg", + "source_sha256": "01a2b7ea51bd26aeaf2ce65fa347ada84a437a3a477478c28314c4ac42bc26e5", + "oracle": "chromium/svg-pattern-duplicate-id-first.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-circle", + "source": "svg-pattern-fill-circle.svg", + "source_sha256": "f1456bfb0f6b43519036a42328fe16f896f4aff4732f228ce5b3b6d32460d8cf", + "oracle": "chromium/svg-pattern-fill-circle.png", + "oracle_sha256": "b33f33fdf0861b7f66dfb3e1ea620eca05436c762d978c2e5003ea6ceb0a6067", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-path-cubic", + "source": "svg-pattern-fill-path-cubic.svg", + "source_sha256": "3b03d9782bbb2b9d28d7966d479bb72c426adfdd10b7de725966a19b7e8e00ff", + "oracle": "chromium/svg-pattern-fill-path-cubic.png", + "oracle_sha256": "b585d85f12bbae8c2b18852a8c7b4ebcc919d608c86433e3db5ccdc602dc51ee", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-path-evenodd", + "source": "svg-pattern-fill-path-evenodd.svg", + "source_sha256": "2924b3a4c142b6362baccbaa8f982bd958e9050b2d72edd2beb5df1a0067e053", + "oracle": "chromium/svg-pattern-fill-path-evenodd.png", + "oracle_sha256": "c6c6db9a965b0727ee65089328f132ce45ec517e9451efcfadc38dc83d0e802e", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-rounded-rect", + "source": "svg-pattern-fill-rounded-rect.svg", + "source_sha256": "020638da16a764e0161253fe226c4eb96fda4bfc14779ac70c0c818fed3da79f", + "oracle": "chromium/svg-pattern-fill-rounded-rect.png", + "oracle_sha256": "e43070d6630b878c65517fa743fc69038d181037a5ef4a3ce249a2a05de89d93", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-stroke-opacity", + "source": "svg-pattern-fill-stroke-opacity.svg", + "source_sha256": "899944bab98694a35b4aa9052f4ff87dd8ca147e50337f15266fe57c2604c120", + "oracle": "chromium/svg-pattern-fill-stroke-opacity.png", + "oracle_sha256": "51c45a113a2b0a1cff766a74b67d17aae9e75040201aa81629dfbffc2c1b9194", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-absent-fallback", + "source": "svg-pattern-height-absent-fallback.svg", + "source_sha256": "f4438f90607da96da1f9a794e79f50cb1d92e3d7f7d017079a5dc21bcaec5f2c", + "oracle": "chromium/svg-pattern-height-absent-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-negative-fallback", + "source": "svg-pattern-height-negative-fallback.svg", + "source_sha256": "e82291fb5fa03ca36aa52b16ba5fc8cacf540d19884611779adb56eb74070d05", + "oracle": "chromium/svg-pattern-height-negative-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-zero-fallback", + "source": "svg-pattern-height-zero-fallback.svg", + "source_sha256": "c9fbf5156ed8e124724d72dc27d7352198ce7390222ce9ceaffb0bfdec3e166c", + "oracle": "chromium/svg-pattern-height-zero-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-beats-xlink", + "source": "svg-pattern-href-beats-xlink.svg", + "source_sha256": "5a4c62f8910b8957bed6eff17a3d73d950083069b9e93ed156d4e21bb18b5aa5", + "oracle": "chromium/svg-pattern-href-beats-xlink.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-cycle-content", + "source": "svg-pattern-href-cycle-content.svg", + "source_sha256": "c70404a8d26605b5f10f6592237031a490b195cc1875f5a06b352c2a398fcb7a", + "oracle": "chromium/svg-pattern-href-cycle-content.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-first-present", + "source": "svg-pattern-href-first-present.svg", + "source_sha256": "96c88ba11fbfe4f63a430e2642e4185b361e4397c1f4445e4d7cd7ad8696770a", + "oracle": "chromium/svg-pattern-href-first-present.png", + "oracle_sha256": "206fdbeb5fadea4bdd038e8dda67ed2670b111b6c027342dc36646ac62631c1b", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-template", + "source": "svg-pattern-href-template.svg", + "source_sha256": "be2e56ca3eca3d08dd0f577b2a62dfcf9b484b14eca0f79b16f1030d6c053f5e", + "oracle": "chromium/svg-pattern-href-template.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-invalid-content-units", + "source": "svg-pattern-invalid-content-units.svg", + "source_sha256": "e2367ee392999b0ccb7c50eda3fa2d6cba454341cf73ebd09d041bdb49fcf1b7", + "oracle": "chromium/svg-pattern-invalid-content-units.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-invalid-pattern-units", + "source": "svg-pattern-invalid-pattern-units.svg", + "source_sha256": "3b495f01af5c4513eddaff6680a7f31b5743fc8919b6e5bdf7bb7283c70995f0", + "oracle": "chromium/svg-pattern-invalid-pattern-units.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-mask-source", + "source": "svg-pattern-mask-source.svg", + "source_sha256": "58e00abe238aa1d070d09e364ffff6449c8a3c17ffe2092ca67b78c4d5953e3d", + "oracle": "chromium/svg-pattern-mask-source.png", + "oracle_sha256": "6c9e4854178c8ccb2ca18e75698cde1ac750be130f420a8dafd8ad990bdd8cb3", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-negative-origin", + "source": "svg-pattern-negative-origin.svg", + "source_sha256": "1c324733c8d1dd5e1135452f94737fb04eaf15f42c0a9d4f34158964070f8dcf", + "oracle": "chromium/svg-pattern-negative-origin.png", + "oracle_sha256": "3c0c16503c4d6aab204ddde7b1d85ca5a8a71b68b81488806de820c6faeb7f45", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-nested-repeat", + "source": "svg-pattern-nested-repeat.svg", + "source_sha256": "c96b63d323a260702e4717d675dfa39eba76818a821b24774567e072c03dcf03", + "oracle": "chromium/svg-pattern-nested-repeat.png", + "oracle_sha256": "7b66b1be5f4b33bb46ca838bb4dc49aae26c485afa9917dee9147d8df1cd26be", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-number", + "source": "svg-pattern-object-number.svg", + "source_sha256": "61c4b49dfa557bf78d86d973054a50507e368b954edc1ea4423b42453ebc3309", + "oracle": "chromium/svg-pattern-object-number.png", + "oracle_sha256": "b8c3990f7b7ca01970bc54aac74289a7bab0b9e58999aeb814cef5305177afa4", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-per-client", + "source": "svg-pattern-object-per-client.svg", + "source_sha256": "14efb91e3aab8b4059b91a84288ea2b42a545bd25cb13ca4f7cf5fd29cc763be", + "oracle": "chromium/svg-pattern-object-per-client.png", + "oracle_sha256": "f120b968f3b31751d8e5ac13aa01cdd202c36b1e4502b58a8debacafa4f1bcde", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-percent", + "source": "svg-pattern-object-percent.svg", + "source_sha256": "c67d275cf7f1f8f5f4ba6b5715e75547f233e97fbccfb8f5da3a91e8ff78090e", + "oracle": "chromium/svg-pattern-object-percent.png", + "oracle_sha256": "b8c3990f7b7ca01970bc54aac74289a7bab0b9e58999aeb814cef5305177afa4", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-overflow-default-clip", + "source": "svg-pattern-overflow-default-clip.svg", + "source_sha256": "d12e632515f31f1bc1d68ba2dac067e081d5df77a623288d0feac5bb8f08c6e8", + "oracle": "chromium/svg-pattern-overflow-default-clip.png", + "oracle_sha256": "a698fa0be5cbf4a09e20eecc93a24d27bc42a940690d34becc220120e6ccd4a1", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-plain-transform-inert", + "source": "svg-pattern-plain-transform-inert.svg", + "source_sha256": "dada37c0afc5f7abbcd489af1d43b0df19483e7c43614b581cd8d244ff6cccc4", + "oracle": "chromium/svg-pattern-plain-transform-inert.png", + "oracle_sha256": "cd343be558bf411efff6ce1e9b4510ed925c7c136476d0cf943b2dc9a1c66694", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-px-lengths", + "source": "svg-pattern-px-lengths.svg", + "source_sha256": "d826b1ffdafd1052c67cc70ff8420ca96825bef8a9516d764d2cfbc46bd6515b", + "oracle": "chromium/svg-pattern-px-lengths.png", + "oracle_sha256": "74018b538ef02b856bf6a318fc742db44e311e89017762f8e06995e5bb6c45ff", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-repeat", + "source": "svg-pattern-repeat.svg", + "source_sha256": "d21a1ebb8edb8fae81d956b87086d45cafe962795ba9019aec95a353d9868aaa", + "oracle": "chromium/svg-pattern-repeat.png", + "oracle_sha256": "b17cc2e310b423413dfdcb8bdd039eda614710f172e05eb0e52fc0b93ab4f17a", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-singular-transform-fallback", + "source": "svg-pattern-singular-transform-fallback.svg", + "source_sha256": "d64dfd0e7ccb94883e4a765a69a94be90e172deba1ec25e9f86b5ceeaf300e56", + "oracle": "chromium/svg-pattern-singular-transform-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-gradient", + "source": "svg-pattern-source-gradient.svg", + "source_sha256": "f9d600c847d6e6d7c57c964860c8667f3ee7914149fcf5b02a3a6a8cc8e51368", + "oracle": "chromium/svg-pattern-source-gradient.png", + "oracle_sha256": "0a5a1ebdba5c88af2610e4aa6a34497ecfad063d5a34cb5d32cffd06b73327c1", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-mask", + "source": "svg-pattern-source-mask.svg", + "source_sha256": "9a2b7227b7f7fe8a5607403c16928565475c67cd033d87407947ada4db19bc25", + "oracle": "chromium/svg-pattern-source-mask.png", + "oracle_sha256": "6835363080fa57e2b0cad3b5079f39bed0d1fc71ac2f83b08f60083334ca1123", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-one-draw-opacity", + "source": "svg-pattern-source-one-draw-opacity.svg", + "source_sha256": "deaeac98aa2491313387c34ea73986607586abb679aad16a428b6610d9561bab", + "oracle": "chromium/svg-pattern-source-one-draw-opacity.png", + "oracle_sha256": "0047fa75e060439b46c2bea75f162ff39285f67c74e536f83ee94202d0c2d65f", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-use", + "source": "svg-pattern-source-use.svg", + "source_sha256": "fb07254fa59037aaa95bd30db7e50f5c91f7a2f4c46745cb334f8cf41a20697d", + "oracle": "chromium/svg-pattern-source-use.png", + "oracle_sha256": "39867bd5fbcbf4389e64e790047b66970aab4984e55fe0124978c163ca004dce", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-dash-round", + "source": "svg-pattern-stroke-dash-round.svg", + "source_sha256": "ff39d6b20fd29f5ba1970d9feaa064e3f9320e15bebe644fd85f5da60036be68", + "oracle": "chromium/svg-pattern-stroke-dash-round.png", + "oracle_sha256": "f0300bf007b277b903899f575b2fc66031ea4943691573ec5d246bf10e4f2d33", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-ellipse", + "source": "svg-pattern-stroke-ellipse.svg", + "source_sha256": "4999131313f78de2592b1ed25127f15efab22255530e48586cd137033eab968e", + "oracle": "chromium/svg-pattern-stroke-ellipse.png", + "oracle_sha256": "10ddf184586efdecf838bdbf30094729abd3263b35c4ff64bc67e4c61e175b75", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-rect", + "source": "svg-pattern-stroke-rect.svg", + "source_sha256": "6bdb58c2af073c1d4e4d9ea07bde641a29daa540939ea817043050f5b67353b5", + "oracle": "chromium/svg-pattern-stroke-rect.png", + "oracle_sha256": "fa2e93e404f840728cc3d3dc1ce55fdda468f5dd98a02cb63328008298b146e0", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-axis-transform", + "source": "svg-pattern-target-axis-transform.svg", + "source_sha256": "f215b82c1bab25fd6732259d7bf0faeeee39174e6982e2fdfbe5ff6782dc4b2f", + "oracle": "chromium/svg-pattern-target-axis-transform.png", + "oracle_sha256": "701abdbd1e15af779bd1bdf8c92bf739c07e609a9b7e598b137217d4528d7aef", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-clip", + "source": "svg-pattern-target-clip.svg", + "source_sha256": "8b792159a2acafbf43486b15c3257fb5618c578760bc2998be132d3ac0722cdf", + "oracle": "chromium/svg-pattern-target-clip.png", + "oracle_sha256": "95194b82fd726213a2434fa59001e588e4f20614b07103fcf00816d17b24d0d9", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-element-opacity", + "source": "svg-pattern-target-element-opacity.svg", + "source_sha256": "d06c1ae21402d38e484dbf320671e63fcc4a2ba01eb873f7f31e2bf118cc27ec", + "oracle": "chromium/svg-pattern-target-element-opacity.png", + "oracle_sha256": "a03f869c7451e4f2e458540da6ddf1c1aec1865d358abe0379038134e40306e7", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-fill-opacity", + "source": "svg-pattern-target-fill-opacity.svg", + "source_sha256": "6ca2ec9307d80b2ba8aa32b26e7484e41cd9ed78e4b667ff05e4c21c56802bfe", + "oracle": "chromium/svg-pattern-target-fill-opacity.png", + "oracle_sha256": "58f6f3613e4eb6a2b90aaab1d17757953b50eee41915dfbbef4951fa04d8f14e", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-filter", + "source": "svg-pattern-target-filter.svg", + "source_sha256": "3e223120ba471d0eb393995aacf02b900d6a4a68c7bf091bcf4993fb9b782a15", + "oracle": "chromium/svg-pattern-target-filter.png", + "oracle_sha256": "575880903d533acbebf434034237e33aa36c14589ee36b5daef9f3591afe49f6", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-group-opacity", + "source": "svg-pattern-target-group-opacity.svg", + "source_sha256": "c5b431c4b1993d9d4192f2d5c5c6bc2c75203302b1f164eedadb4c9d0e33d1ea", + "oracle": "chromium/svg-pattern-target-group-opacity.png", + "oracle_sha256": "de6d591106d2eb97c9ddfaafe65171a5e76599e2cd41711d9d800ce9ab65ba99", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-mask", + "source": "svg-pattern-target-mask.svg", + "source_sha256": "7c2180fbaa595d054ce3dfeb94b3a4cf2b8b0605d8a7620c6d5d02eb94e05af6", + "oracle": "chromium/svg-pattern-target-mask.png", + "oracle_sha256": "893fa37ba73b4124234f6ffad4d769272365a1bf8dc4e9f6758b1ae0a0b0577f", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-template-style-owner", + "source": "svg-pattern-template-style-owner.svg", + "source_sha256": "53d980da511f35170d053682a65584c70e6c5f75bf24aacaf42ecb431fd46fa7", + "oracle": "chromium/svg-pattern-template-style-owner.png", + "oracle_sha256": "31a43626eee89c600a77ae3e806d345ec0034f94226a8f0317665838eb17e365", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-axis-scale", + "source": "svg-pattern-transform-axis-scale.svg", + "source_sha256": "28135f11fb6266dc526c99ac6716be30edeef7991c8bde95051858c591652125", + "oracle": "chromium/svg-pattern-transform-axis-scale.png", + "oracle_sha256": "65f601e443f67c182ad4d23ae3e72b6c88d5cfa5bdf4dbf181314256828522ea", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-css", + "source": "svg-pattern-transform-css.svg", + "source_sha256": "e2f4001f433810e4b95e82b6a839cf70254aaf9713801fb5fc18798bc9a6e834", + "oracle": "chromium/svg-pattern-transform-css.png", + "oracle_sha256": "ae6fa8e8432d6f195100eb9b99ca0bb68d7e9fa14fe1848f37a929853f4a485c", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-hint", + "source": "svg-pattern-transform-hint.svg", + "source_sha256": "34974aabd8b446047d9293c0b328d9c45a8409d466b8c3072020965f18998581", + "oracle": "chromium/svg-pattern-transform-hint.png", + "oracle_sha256": "95bf108449c971c942b1c262c23b46c0088b02c9da5019c2d6a306cbad1928b1", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-quarter-turn", + "source": "svg-pattern-transform-quarter-turn.svg", + "source_sha256": "1b322587de8612eed7cf9a0b962a19d6b8d623a283787c897e9756348899e4ac", + "oracle": "chromium/svg-pattern-transform-quarter-turn.png", + "oracle_sha256": "6786beab76c1d9fc22f65948348804e04f04ac23c908658610b637b39992d4ff", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-use-scale", + "source": "svg-pattern-use-scale.svg", + "source_sha256": "b0edec5ee1c1814be42ec2b238e397c71ba52b80da06aba413b5ff41809185aa", + "oracle": "chromium/svg-pattern-use-scale.png", + "oracle_sha256": "40dad6814aa704670cb95bd9889cb64a15528ba63da9d09e61dcf34ea63a1868", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-use-translate", + "source": "svg-pattern-use-translate.svg", + "source_sha256": "d3f95203542d019449613ce8e3dc600423715f2b4b4febfdec067fb50809ebd1", + "oracle": "chromium/svg-pattern-use-translate.png", + "oracle_sha256": "1a092c8c1950dc14a48a1ad702416ddd0654cf135dd611dda2ef971f7eb70e39", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-user-percent", + "source": "svg-pattern-user-percent.svg", + "source_sha256": "75ce309eed4ca7250f15846271c28299a3459b362b84c6178c7740f1ea3ab983", + "oracle": "chromium/svg-pattern-user-percent.png", + "oracle_sha256": "7e0ac10e923c772860b6c241f9fab42372824e8360e6f1b11110956cc0316a3d", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-valid-empty", + "source": "svg-pattern-valid-empty.svg", + "source_sha256": "3137672fcadf72e5764fe4d9bb208b890011169f666c4c606d1fb3e490675429", + "oracle": "chromium/svg-pattern-valid-empty.png", + "oracle_sha256": "1502272898b22a79c129dc16ab37a1280c353a18767753769bc82532dddd72a7", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-content-object", + "source": "svg-pattern-viewbox-content-object.svg", + "source_sha256": "ea07d01cd4b6f3ad66163fc4cdb1d35dc5956bb7f69e41aae0b6a1d92152392c", + "oracle": "chromium/svg-pattern-viewbox-content-object.png", + "oracle_sha256": "3a377f4cf55d6b23dd4a77854f9ca4181e27a0dff43a0ed24d83e03ffce91c6f", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-content-user", + "source": "svg-pattern-viewbox-content-user.svg", + "source_sha256": "b8e77c4e9368684c5846630f02dd8ae8d6f2da558d653323270d99d702a2bdfb", + "oracle": "chromium/svg-pattern-viewbox-content-user.png", + "oracle_sha256": "3a377f4cf55d6b23dd4a77854f9ca4181e27a0dff43a0ed24d83e03ffce91c6f", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-meet", + "source": "svg-pattern-viewbox-meet.svg", + "source_sha256": "c35badddded32906c0600cc35adf60e251a2ef715c78a1e86b7f58b86a95df7e", + "oracle": "chromium/svg-pattern-viewbox-meet.png", + "oracle_sha256": "6e7c8df88e35acd79a224b20fa6367bc550b1174179de8ffc89470f264f2e627", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-none", + "source": "svg-pattern-viewbox-none.svg", + "source_sha256": "d4126dcdebdcbfabd94d69ea8e1460fe83680249d4948e2e5f9d3bba0926e81b", + "oracle": "chromium/svg-pattern-viewbox-none.png", + "oracle_sha256": "3a377f4cf55d6b23dd4a77854f9ca4181e27a0dff43a0ed24d83e03ffce91c6f", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-slice", + "source": "svg-pattern-viewbox-slice.svg", + "source_sha256": "33147963313a06e382ebeb22fdec597921294df16f90d1d986eadd1c854f1489", + "oracle": "chromium/svg-pattern-viewbox-slice.png", + "oracle_sha256": "4fa59ab89599fd77666163f251eba6b58d4ad9bbfe77ae08785099aaf1333064", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-absent-fallback", + "source": "svg-pattern-width-absent-fallback.svg", + "source_sha256": "377adefde75ff9ccc3f86f6b9af42234de3b5a228d4a536080f03732558a55ff", + "oracle": "chromium/svg-pattern-width-absent-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-malformed-fallback", + "source": "svg-pattern-width-malformed-fallback.svg", + "source_sha256": "bfe34c8d3528c57068d71fe6bfc0b65e86373b2f8b2a72b05533ee981de082c6", + "oracle": "chromium/svg-pattern-width-malformed-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-negative-fallback", + "source": "svg-pattern-width-negative-fallback.svg", + "source_sha256": "d5105aec517fcb0b8f37e2f81680ad744824e3b5909f8d9a1690ce6e8372073d", + "oracle": "chromium/svg-pattern-width-negative-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-zero-fallback", + "source": "svg-pattern-width-zero-fallback.svg", + "source_sha256": "b3fa1beb15c93a305d0321770a982c5c05ecda35ef99a19e61eb14489b35f8fb", + "oracle": "chromium/svg-pattern-width-zero-fallback.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-xlink-template", + "source": "svg-pattern-xlink-template.svg", + "source_sha256": "e46ea926d8528577e3712372ec69b83b7fe006fa078139ce494fbd5f97c340b5", + "oracle": "chromium/svg-pattern-xlink-template.png", + "oracle_sha256": "e3e7d7bd8868770106d447601e0288678be1b89fc87573df7ed87e5884b54d65", + "width": 64, + "height": 64 + }, { "id": "svg-percent-circle-diagonal", "source": "svg-percent-circle-diagonal.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index c68ec570..89e431e7 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -17,6 +17,14 @@ "width": 64, "height": 64 }, + { + "id": "html-inline-svg-pattern", + "source": "html-inline-svg-pattern.html", + "entry": "html-inline-svg", + "oracle": "chromium/html-inline-svg-pattern.png", + "width": 64, + "height": 64 + }, { "id": "html-webpage-mockup", "source": "html-webpage-mockup.html", @@ -5179,6 +5187,494 @@ "width": 64, "height": 64 }, + { + "id": "svg-pattern-degenerate-viewbox-fallback", + "source": "svg-pattern-degenerate-viewbox-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-degenerate-viewbox-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-descriptive-content-drop", + "source": "svg-pattern-descriptive-content-drop.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-descriptive-content-drop.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-duplicate-id-first", + "source": "svg-pattern-duplicate-id-first.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-duplicate-id-first.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-circle", + "source": "svg-pattern-fill-circle.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-fill-circle.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-path-cubic", + "source": "svg-pattern-fill-path-cubic.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-fill-path-cubic.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-path-evenodd", + "source": "svg-pattern-fill-path-evenodd.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-fill-path-evenodd.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-rounded-rect", + "source": "svg-pattern-fill-rounded-rect.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-fill-rounded-rect.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-fill-stroke-opacity", + "source": "svg-pattern-fill-stroke-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-fill-stroke-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-absent-fallback", + "source": "svg-pattern-height-absent-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-height-absent-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-negative-fallback", + "source": "svg-pattern-height-negative-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-height-negative-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-height-zero-fallback", + "source": "svg-pattern-height-zero-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-height-zero-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-beats-xlink", + "source": "svg-pattern-href-beats-xlink.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-href-beats-xlink.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-cycle-content", + "source": "svg-pattern-href-cycle-content.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-href-cycle-content.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-first-present", + "source": "svg-pattern-href-first-present.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-href-first-present.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-href-template", + "source": "svg-pattern-href-template.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-href-template.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-invalid-content-units", + "source": "svg-pattern-invalid-content-units.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-invalid-content-units.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-invalid-pattern-units", + "source": "svg-pattern-invalid-pattern-units.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-invalid-pattern-units.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-mask-source", + "source": "svg-pattern-mask-source.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-mask-source.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-negative-origin", + "source": "svg-pattern-negative-origin.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-negative-origin.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-nested-repeat", + "source": "svg-pattern-nested-repeat.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-nested-repeat.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-number", + "source": "svg-pattern-object-number.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-object-number.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-per-client", + "source": "svg-pattern-object-per-client.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-object-per-client.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-object-percent", + "source": "svg-pattern-object-percent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-object-percent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-overflow-default-clip", + "source": "svg-pattern-overflow-default-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-overflow-default-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-plain-transform-inert", + "source": "svg-pattern-plain-transform-inert.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-plain-transform-inert.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-px-lengths", + "source": "svg-pattern-px-lengths.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-px-lengths.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-repeat", + "source": "svg-pattern-repeat.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-repeat.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-singular-transform-fallback", + "source": "svg-pattern-singular-transform-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-singular-transform-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-gradient", + "source": "svg-pattern-source-gradient.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-source-gradient.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-mask", + "source": "svg-pattern-source-mask.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-source-mask.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-one-draw-opacity", + "source": "svg-pattern-source-one-draw-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-source-one-draw-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-source-use", + "source": "svg-pattern-source-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-source-use.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-dash-round", + "source": "svg-pattern-stroke-dash-round.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-stroke-dash-round.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-ellipse", + "source": "svg-pattern-stroke-ellipse.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-stroke-ellipse.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-stroke-rect", + "source": "svg-pattern-stroke-rect.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-stroke-rect.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-axis-transform", + "source": "svg-pattern-target-axis-transform.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-axis-transform.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-clip", + "source": "svg-pattern-target-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-element-opacity", + "source": "svg-pattern-target-element-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-element-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-fill-opacity", + "source": "svg-pattern-target-fill-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-fill-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-filter", + "source": "svg-pattern-target-filter.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-filter.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-group-opacity", + "source": "svg-pattern-target-group-opacity.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-group-opacity.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-target-mask", + "source": "svg-pattern-target-mask.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-target-mask.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-template-style-owner", + "source": "svg-pattern-template-style-owner.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-template-style-owner.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-axis-scale", + "source": "svg-pattern-transform-axis-scale.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-transform-axis-scale.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-css", + "source": "svg-pattern-transform-css.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-transform-css.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-hint", + "source": "svg-pattern-transform-hint.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-transform-hint.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-transform-quarter-turn", + "source": "svg-pattern-transform-quarter-turn.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-transform-quarter-turn.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-use-scale", + "source": "svg-pattern-use-scale.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-use-scale.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-use-translate", + "source": "svg-pattern-use-translate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-use-translate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-user-percent", + "source": "svg-pattern-user-percent.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-user-percent.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-valid-empty", + "source": "svg-pattern-valid-empty.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-valid-empty.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-content-object", + "source": "svg-pattern-viewbox-content-object.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-viewbox-content-object.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-content-user", + "source": "svg-pattern-viewbox-content-user.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-viewbox-content-user.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-meet", + "source": "svg-pattern-viewbox-meet.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-viewbox-meet.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-none", + "source": "svg-pattern-viewbox-none.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-viewbox-none.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-viewbox-slice", + "source": "svg-pattern-viewbox-slice.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-viewbox-slice.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-absent-fallback", + "source": "svg-pattern-width-absent-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-width-absent-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-malformed-fallback", + "source": "svg-pattern-width-malformed-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-width-malformed-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-negative-fallback", + "source": "svg-pattern-width-negative-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-width-negative-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-width-zero-fallback", + "source": "svg-pattern-width-zero-fallback.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-width-zero-fallback.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-pattern-xlink-template", + "source": "svg-pattern-xlink-template.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-pattern-xlink-template.png", + "width": 64, + "height": 64 + }, { "id": "svg-percent-circle-diagonal", "source": "svg-percent-circle-diagonal.svg", diff --git a/fixtures/web-first/svg-pattern-degenerate-viewbox-fallback.svg b/fixtures/web-first/svg-pattern-degenerate-viewbox-fallback.svg new file mode 100644 index 00000000..4db86f97 --- /dev/null +++ b/fixtures/web-first/svg-pattern-degenerate-viewbox-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-descriptive-content-drop.svg b/fixtures/web-first/svg-pattern-descriptive-content-drop.svg new file mode 100644 index 00000000..cc51aedc --- /dev/null +++ b/fixtures/web-first/svg-pattern-descriptive-content-drop.svg @@ -0,0 +1 @@ +local diff --git a/fixtures/web-first/svg-pattern-duplicate-id-first.svg b/fixtures/web-first/svg-pattern-duplicate-id-first.svg new file mode 100644 index 00000000..d3aa924f --- /dev/null +++ b/fixtures/web-first/svg-pattern-duplicate-id-first.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-fill-circle.svg b/fixtures/web-first/svg-pattern-fill-circle.svg new file mode 100644 index 00000000..51fd9095 --- /dev/null +++ b/fixtures/web-first/svg-pattern-fill-circle.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-fill-path-cubic.svg b/fixtures/web-first/svg-pattern-fill-path-cubic.svg new file mode 100644 index 00000000..2cbf7d5e --- /dev/null +++ b/fixtures/web-first/svg-pattern-fill-path-cubic.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-fill-path-evenodd.svg b/fixtures/web-first/svg-pattern-fill-path-evenodd.svg new file mode 100644 index 00000000..fccb1914 --- /dev/null +++ b/fixtures/web-first/svg-pattern-fill-path-evenodd.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-fill-rounded-rect.svg b/fixtures/web-first/svg-pattern-fill-rounded-rect.svg new file mode 100644 index 00000000..b22d748f --- /dev/null +++ b/fixtures/web-first/svg-pattern-fill-rounded-rect.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-fill-stroke-opacity.svg b/fixtures/web-first/svg-pattern-fill-stroke-opacity.svg new file mode 100644 index 00000000..c05c51af --- /dev/null +++ b/fixtures/web-first/svg-pattern-fill-stroke-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-height-absent-fallback.svg b/fixtures/web-first/svg-pattern-height-absent-fallback.svg new file mode 100644 index 00000000..7f37653c --- /dev/null +++ b/fixtures/web-first/svg-pattern-height-absent-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-height-negative-fallback.svg b/fixtures/web-first/svg-pattern-height-negative-fallback.svg new file mode 100644 index 00000000..1c13fba1 --- /dev/null +++ b/fixtures/web-first/svg-pattern-height-negative-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-height-zero-fallback.svg b/fixtures/web-first/svg-pattern-height-zero-fallback.svg new file mode 100644 index 00000000..4966c80c --- /dev/null +++ b/fixtures/web-first/svg-pattern-height-zero-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-href-beats-xlink.svg b/fixtures/web-first/svg-pattern-href-beats-xlink.svg new file mode 100644 index 00000000..fbcf59aa --- /dev/null +++ b/fixtures/web-first/svg-pattern-href-beats-xlink.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-href-cycle-content.svg b/fixtures/web-first/svg-pattern-href-cycle-content.svg new file mode 100644 index 00000000..47b5edd9 --- /dev/null +++ b/fixtures/web-first/svg-pattern-href-cycle-content.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-href-first-present.svg b/fixtures/web-first/svg-pattern-href-first-present.svg new file mode 100644 index 00000000..f8911055 --- /dev/null +++ b/fixtures/web-first/svg-pattern-href-first-present.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-href-template.svg b/fixtures/web-first/svg-pattern-href-template.svg new file mode 100644 index 00000000..e467d865 --- /dev/null +++ b/fixtures/web-first/svg-pattern-href-template.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-invalid-content-units.svg b/fixtures/web-first/svg-pattern-invalid-content-units.svg new file mode 100644 index 00000000..f6954f54 --- /dev/null +++ b/fixtures/web-first/svg-pattern-invalid-content-units.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-invalid-pattern-units.svg b/fixtures/web-first/svg-pattern-invalid-pattern-units.svg new file mode 100644 index 00000000..ce246359 --- /dev/null +++ b/fixtures/web-first/svg-pattern-invalid-pattern-units.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-mask-source-pattern.svg b/fixtures/web-first/svg-pattern-mask-source.svg similarity index 100% rename from fixtures/web-first/unsupported/svg-mask-source-pattern.svg rename to fixtures/web-first/svg-pattern-mask-source.svg diff --git a/fixtures/web-first/svg-pattern-negative-origin.svg b/fixtures/web-first/svg-pattern-negative-origin.svg new file mode 100644 index 00000000..0ae40c69 --- /dev/null +++ b/fixtures/web-first/svg-pattern-negative-origin.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-nested-repeat.svg b/fixtures/web-first/svg-pattern-nested-repeat.svg new file mode 100644 index 00000000..cb6670ae --- /dev/null +++ b/fixtures/web-first/svg-pattern-nested-repeat.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-object-number.svg b/fixtures/web-first/svg-pattern-object-number.svg new file mode 100644 index 00000000..89acf61c --- /dev/null +++ b/fixtures/web-first/svg-pattern-object-number.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-object-per-client.svg b/fixtures/web-first/svg-pattern-object-per-client.svg new file mode 100644 index 00000000..e750d156 --- /dev/null +++ b/fixtures/web-first/svg-pattern-object-per-client.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-object-percent.svg b/fixtures/web-first/svg-pattern-object-percent.svg new file mode 100644 index 00000000..b420ee6f --- /dev/null +++ b/fixtures/web-first/svg-pattern-object-percent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-overflow-default-clip.svg b/fixtures/web-first/svg-pattern-overflow-default-clip.svg new file mode 100644 index 00000000..8a83b3d6 --- /dev/null +++ b/fixtures/web-first/svg-pattern-overflow-default-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-plain-transform-inert.svg b/fixtures/web-first/svg-pattern-plain-transform-inert.svg new file mode 100644 index 00000000..7f105abe --- /dev/null +++ b/fixtures/web-first/svg-pattern-plain-transform-inert.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-px-lengths.svg b/fixtures/web-first/svg-pattern-px-lengths.svg new file mode 100644 index 00000000..2047d7c3 --- /dev/null +++ b/fixtures/web-first/svg-pattern-px-lengths.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-repeat.svg b/fixtures/web-first/svg-pattern-repeat.svg new file mode 100644 index 00000000..9b55abd4 --- /dev/null +++ b/fixtures/web-first/svg-pattern-repeat.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-singular-transform-fallback.svg b/fixtures/web-first/svg-pattern-singular-transform-fallback.svg new file mode 100644 index 00000000..c651f570 --- /dev/null +++ b/fixtures/web-first/svg-pattern-singular-transform-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-source-gradient.svg b/fixtures/web-first/svg-pattern-source-gradient.svg new file mode 100644 index 00000000..f9550244 --- /dev/null +++ b/fixtures/web-first/svg-pattern-source-gradient.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-source-mask.svg b/fixtures/web-first/svg-pattern-source-mask.svg new file mode 100644 index 00000000..a952b1be --- /dev/null +++ b/fixtures/web-first/svg-pattern-source-mask.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-source-one-draw-opacity.svg b/fixtures/web-first/svg-pattern-source-one-draw-opacity.svg new file mode 100644 index 00000000..3992f694 --- /dev/null +++ b/fixtures/web-first/svg-pattern-source-one-draw-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-source-use.svg b/fixtures/web-first/svg-pattern-source-use.svg new file mode 100644 index 00000000..dc44b97e --- /dev/null +++ b/fixtures/web-first/svg-pattern-source-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-stroke-dash-round.svg b/fixtures/web-first/svg-pattern-stroke-dash-round.svg new file mode 100644 index 00000000..af9cb07c --- /dev/null +++ b/fixtures/web-first/svg-pattern-stroke-dash-round.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-stroke-ellipse.svg b/fixtures/web-first/svg-pattern-stroke-ellipse.svg new file mode 100644 index 00000000..aa0d6bca --- /dev/null +++ b/fixtures/web-first/svg-pattern-stroke-ellipse.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-stroke-rect.svg b/fixtures/web-first/svg-pattern-stroke-rect.svg new file mode 100644 index 00000000..bfd6a0d0 --- /dev/null +++ b/fixtures/web-first/svg-pattern-stroke-rect.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-axis-transform.svg b/fixtures/web-first/svg-pattern-target-axis-transform.svg new file mode 100644 index 00000000..2d00564a --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-axis-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-clip.svg b/fixtures/web-first/svg-pattern-target-clip.svg new file mode 100644 index 00000000..56f830f7 --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-element-opacity.svg b/fixtures/web-first/svg-pattern-target-element-opacity.svg new file mode 100644 index 00000000..ca1c3582 --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-element-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-fill-opacity.svg b/fixtures/web-first/svg-pattern-target-fill-opacity.svg new file mode 100644 index 00000000..ebe1c371 --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-fill-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-filter.svg b/fixtures/web-first/svg-pattern-target-filter.svg new file mode 100644 index 00000000..56ebb3cf --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-filter.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-group-opacity.svg b/fixtures/web-first/svg-pattern-target-group-opacity.svg new file mode 100644 index 00000000..b3cf8880 --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-group-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-target-mask.svg b/fixtures/web-first/svg-pattern-target-mask.svg new file mode 100644 index 00000000..7d4d0887 --- /dev/null +++ b/fixtures/web-first/svg-pattern-target-mask.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-template-style-owner.svg b/fixtures/web-first/svg-pattern-template-style-owner.svg new file mode 100644 index 00000000..cd1a6778 --- /dev/null +++ b/fixtures/web-first/svg-pattern-template-style-owner.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-transform-axis-scale.svg b/fixtures/web-first/svg-pattern-transform-axis-scale.svg new file mode 100644 index 00000000..5c8508c0 --- /dev/null +++ b/fixtures/web-first/svg-pattern-transform-axis-scale.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-transform-css.svg b/fixtures/web-first/svg-pattern-transform-css.svg new file mode 100644 index 00000000..d3c20d0e --- /dev/null +++ b/fixtures/web-first/svg-pattern-transform-css.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-transform-hint.svg b/fixtures/web-first/svg-pattern-transform-hint.svg new file mode 100644 index 00000000..c563fd89 --- /dev/null +++ b/fixtures/web-first/svg-pattern-transform-hint.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-transform-quarter-turn.svg b/fixtures/web-first/svg-pattern-transform-quarter-turn.svg new file mode 100644 index 00000000..6c6c8749 --- /dev/null +++ b/fixtures/web-first/svg-pattern-transform-quarter-turn.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-use-scale.svg b/fixtures/web-first/svg-pattern-use-scale.svg new file mode 100644 index 00000000..48f0fa77 --- /dev/null +++ b/fixtures/web-first/svg-pattern-use-scale.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-use-translate.svg b/fixtures/web-first/svg-pattern-use-translate.svg new file mode 100644 index 00000000..dc4c01f9 --- /dev/null +++ b/fixtures/web-first/svg-pattern-use-translate.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-user-percent.svg b/fixtures/web-first/svg-pattern-user-percent.svg new file mode 100644 index 00000000..aac6c00c --- /dev/null +++ b/fixtures/web-first/svg-pattern-user-percent.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-valid-empty.svg b/fixtures/web-first/svg-pattern-valid-empty.svg new file mode 100644 index 00000000..5fb0d826 --- /dev/null +++ b/fixtures/web-first/svg-pattern-valid-empty.svg @@ -0,0 +1 @@ +local diff --git a/fixtures/web-first/svg-pattern-viewbox-content-object.svg b/fixtures/web-first/svg-pattern-viewbox-content-object.svg new file mode 100644 index 00000000..36b42a0d --- /dev/null +++ b/fixtures/web-first/svg-pattern-viewbox-content-object.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-viewbox-content-user.svg b/fixtures/web-first/svg-pattern-viewbox-content-user.svg new file mode 100644 index 00000000..a14b4954 --- /dev/null +++ b/fixtures/web-first/svg-pattern-viewbox-content-user.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-viewbox-meet.svg b/fixtures/web-first/svg-pattern-viewbox-meet.svg new file mode 100644 index 00000000..7aff6ba3 --- /dev/null +++ b/fixtures/web-first/svg-pattern-viewbox-meet.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-viewbox-none.svg b/fixtures/web-first/svg-pattern-viewbox-none.svg new file mode 100644 index 00000000..ac7aa2cf --- /dev/null +++ b/fixtures/web-first/svg-pattern-viewbox-none.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-viewbox-slice.svg b/fixtures/web-first/svg-pattern-viewbox-slice.svg new file mode 100644 index 00000000..62fc6353 --- /dev/null +++ b/fixtures/web-first/svg-pattern-viewbox-slice.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-width-absent-fallback.svg b/fixtures/web-first/svg-pattern-width-absent-fallback.svg new file mode 100644 index 00000000..f478e15e --- /dev/null +++ b/fixtures/web-first/svg-pattern-width-absent-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-width-malformed-fallback.svg b/fixtures/web-first/svg-pattern-width-malformed-fallback.svg new file mode 100644 index 00000000..affd702d --- /dev/null +++ b/fixtures/web-first/svg-pattern-width-malformed-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-width-negative-fallback.svg b/fixtures/web-first/svg-pattern-width-negative-fallback.svg new file mode 100644 index 00000000..185d09d3 --- /dev/null +++ b/fixtures/web-first/svg-pattern-width-negative-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-width-zero-fallback.svg b/fixtures/web-first/svg-pattern-width-zero-fallback.svg new file mode 100644 index 00000000..11c9270b --- /dev/null +++ b/fixtures/web-first/svg-pattern-width-zero-fallback.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-pattern-xlink-template.svg b/fixtures/web-first/svg-pattern-xlink-template.svg new file mode 100644 index 00000000..9e75a701 --- /dev/null +++ b/fixtures/web-first/svg-pattern-xlink-template.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index d7097470..5c140b84 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -57,7 +57,21 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-use-symbol.svg` | A `` target declares at the clone's own path as an unsupported element: instantiated, a symbol renders like a nested `` viewport — a scope the flat frame cannot hold, same as `svg-nested-svg`. | | `svg-image.svg` | `` refuses as an element: the glyphless product is declared resource-free, and websem is forbidden I/O by its architecture lock. The data:-URI sub-slice enters with the resource environment, not before. | | `svg-nested-svg.svg` | A nested `` establishes a new viewport and clip — a scope the flat frame cannot hold. Declared until the group-scope rung. | -| `svg-pattern-paint-server.svg` | `` declares twice: the element by name, and the referencing `fill="url(#…)"` as an unsupported fill value. Never approximated to a solid. (`svg-gradient-paint-server` graduated with the gradient rung — gradients are baked cells now.) | +| `svg-pattern-external.svg` | Refuse a pattern whose local facts may inherit through an external template edge. `websem` owns no resource I/O, so it cannot prove that dropping the edge leaves the same tile. | +| `svg-pattern-context-selection.svg` | Refuse a pattern paint selected through `context-fill`/`context-stroke`. Chromium propagates the server through the context relation and the direct probe differs from no paint by 1,152 pixels at maximum delta 255, but per-context pattern composition is reserved for the next pattern rung and has no committed matrix yet (measured, not celled). | +| `svg-pattern-css-transform-percentage.svg` | Refuse a CSS percentage transform on the pattern resource until its reference box is carried without invention. Chromium resolves inline `transform:translate(50%, 0px)` against the 64-unit viewport: it is exact to `32px`, while the former tile-width basis selected `7px` and changed 1,008 target pixels at maximum delta 205. The invalid percentage spelling in the `patternTransform` presentation attribute itself drops to identity, so the patrol is scoped to the computed CSS route (measured, not celled). | +| `svg-pattern-length-unit.svg` · `svg-pattern-length-calc.svg` · `svg-pattern-length-var.svg` · `svg-pattern-length-css-wide.svg` · `svg-pattern-length-css-comments.svg` | Refuse a non-`px` unit, CSS math, custom-property substitution, a CSS-wide value, and CSS comments around an otherwise valid pattern tile length by the exact field. Chromium computes each route. The comment witness was formerly silent in both admissions and changed all 2,304 target pixels at maximum delta 202; their independent syntax/unit/value-type rows carry those missing contexts (measured, not celled). | +| `svg-pattern-length-used-range.svg` | Refuse finite tile coordinates outside Chromium's unimplemented Web used-length clamp. Chromium makes `x="1000000000"`, `x="33554430"`, `x="-1000000000"`, and finite source `x="1e100"` equal their signed clamp controls. Before the patrol, both admissions differed by 1,152, 1,152, 768, and 2,112 pixels respectively, all at maximum delta 205 (measured, not celled). | +| `svg-pattern-number-precision-alias.svg` | Conservatively refuse the established raw-number normalization alias before pattern tile geometry. At 64×64 the amplified percentage alias and direct midpoint pairs were pixel-identical in Chromium (measured, not celled), so this is a one-way provenance patrol rather than a claimed raster divergence: the direct f32 decoder still cannot prove which Blink CSS-parser used value entered the tile. The shared `x`/`y`/`width`/`height` rows remain open for their wider pattern applicability. | +| `svg-pattern-source-coverage-precision.svg` | Refuse curved source geometry at the pinned Chromium/Skia picture-shader coverage boundary. A circle inside the tile changed 189 pixels at maximum delta 8; touching and overflowing circles changed 315/225 at delta 32/15. Integer-mapped rectangle programs remain exact (measured, not celled). | +| `svg-pattern-source-effect-precision.svg` · `svg-pattern-source-clip-precision.svg` | Refuse a multi-draw isolated source opacity or geometric clip. Group opacity changed 1,152–1,728 pixels at maximum delta 2, and a circular clip changed 216 at delta 9. One-draw folded opacity and the sampled un-clipped rectangle programs are exact; a rectangle clip was exact but shares the conservative source-effect patrol (measured, not celled). | +| `svg-pattern-source-filter.svg` | Refuse filter composition inside the tile program until the pattern×filter family has its own matrix. Chromium honors the sampled safe blur—the filtered and plain controls differ by all 2,304 target pixels at maximum delta 182—and the current route happened to match that one Chromium raster exactly, but one sample cannot admit the complete filter graph inside a second picture-shader composition (measured, not celled). | +| `svg-pattern-nested-composition-precision.svg` | A nested pattern alone is admitted and exact. Compositing another source draw over it changed 108 pixels at maximum delta 1, so that mixed picture program refuses by name (measured, not celled). | +| `svg-pattern-nesting-too-deep.svg` | Refuse a ninth distinct nested pattern before its source walk begins. The resolved contract admits at most eight immutable programs; the producer applies the same bound before recursively measuring or compiling another source, while the separate active-id patrol still catches cycles. | +| `svg-pattern-affine-precision.svg` | Refuse a pattern whose final tile map carries a general rotation or shear. General rotation is content-dependent: sampled half-tile subdivisions were exact while a six-unit grid changed two pixels at delta 1 and a related non-square grid changed one pixel at delta 3. Shear/skew witnesses changed 147–222 pixels at maximum delta 2. Translation, axis scales, reflection, and exact quarter turns remain admitted (measured, not celled). | +| `svg-pattern-tile-sampling-precision.svg` | Refuse a tile whose final device-axis extent is fractional. A root 0.8 scale changed 230 pixels at maximum delta 4; non-uniform and fractional root maps changed 194/164 at delta 4, and a fractional authored tile changed 407 at delta 28. Integer final extents remain admitted (measured, not celled). | +| `svg-pattern-source-unsupported.svg` | Any unsupported child invalidates pattern-source compilation transactionally. The witness places a valid rectangle before an unsupported image; best effort skips the whole affected client, so a plausible partial tile can never escape. | +| `svg-pattern-transform-none-provenance.svg` | Refuse the narrow derived-template case where an author stylesheet may contribute `transform:none`. The computed empty transform loses whether the sheet supplied `none` or no declaration; inheriting the template's `patternTransform` would resurrect a transform Chromium suppresses. Inline declarations remain attributable and admitted. | | `svg-gradient-focal.svg` | A focal radial (`fx`/`fy` off the center, `fr > 0` alike) refuses by name: the shared radial paint leaf is concentric, and Chromium's focal cone — unclamped, leaving pixels unpainted (measured) — is inexpressible in it until its owner amendment. | | `svg-gradient-linearrgb.svg` | `color-interpolation="linearRGB"` is honored by Chromium (measured: the linear-light midpoint, not the sRGB one) and refuses by name — one backend ramp cannot interpolate in a second space. | | `svg-gradient-stop-css.svg` | A stylesheet declaring `stop-color` is a document-level declaration: the pinned cascade has no such longhand (Gecko-only at the Stylo pin), so the sheet is named and the gradient renders with its attribute colors — a declared divergence, since Chromium honors the sheet. | @@ -111,7 +125,7 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-mask-css-properties.svg` | Refuse authored CSS mask-family ingress. Chromium honors these declarations, but the pinned Servo-mode cascade furnishes no computed route the compiler can consume. The contract test sweeps `mask`, every listed mask longhand and border longhand (including the newly enumerated `mask-border-mode`), plus the shipped `-webkit-mask-image` alias. No matcher grows around Stylo. | | `svg-mask-external.svg` · `svg-mask-root.svg` | Absolute and relative external resources and the root host-layer route stay outside this command's self-contained SVG-local frame. Each external URL skips its attributable target; an active mask on the root `` refuses in both admissions because no local target skip can preserve the document. | | `svg-mask-full-shorthand.svg` · `svg-mask-var.svg` | The direct presentation reader carries `none` and one same-document URL. A full mask layer, multiple layers, or `var()` needs the unavailable property/substitution route and refuses rather than being mistaken for the admitted URL branch. The independently listed CSS masking and custom-property rows carry those gaps. | -| `svg-mask-cycle.svg` · `svg-mask-source-pattern.svg` | A nested cycle refuses by stable chain name. Any unsupported child in the source image is transactional: the whole referencing target is skipped and declared, so a partially painted source can never escape as a wrong mask. The pattern witness guards this route; text and nested `` source probes reach the same boundary and retain their own element rows. | +| `svg-mask-cycle.svg` | A nested cycle refuses by stable chain name. Unsupported mask-source children remain transactional: the whole referencing target is skipped and declared, so a partially painted source can never escape. The former pattern witness graduated as the Chromium-exact `svg-pattern-mask-source` cell; text and nested `` source probes retain their own element rows. | | `svg-mask-resource-style-inheritance.svg` | Refuse an unrepresented inline declaration on the `` resource before it can change a source descendant silently. Chromium inherits resource-own `shape-rendering: crispEdges` exactly like the same child declaration, 96 pixels at Δ63 from the default; the former n0 route emitted that default byte-identically. Resource-own `color-interpolation: linearRGB` also changes 30 pixels at Δ1. The separately measured inert `filter`, `mask`, and `clip-path` effects remain admitted on the resource, while `mask-type` keeps its dedicated row. | | `svg-mask-region-calc.svg` · `svg-mask-region-unit.svg` · `svg-mask-region-var.svg` | CSS math, a non-`px` unit, and custom-property substitution in a mask-region field refuse by that exact field. Chromium computes all three; their independent value-type rows carry the missing computation/basis/substitution contexts. | | `svg-mask-region-used-range.svg` | Refuse finite mask-region coordinates beyond the unimplemented Web used-length clamp, including a valid exponent beyond f32. Chromium makes `x="1000000000"`, `x="100000000000000000000"`, `x="1e100"`, and the adjacent 33,554,430/33,554,432 controls identical to 33,554,428. Before the patrol each huge source lost 1,728 pixels, while the adjacent controls lost 96/192, all at Δ255. The x witness is exact; sibling fields conservatively share the named range patrol. | diff --git a/fixtures/web-first/unsupported/svg-pattern-affine-precision.svg b/fixtures/web-first/unsupported/svg-pattern-affine-precision.svg new file mode 100644 index 00000000..3982de29 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-affine-precision.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-context-selection.svg b/fixtures/web-first/unsupported/svg-pattern-context-selection.svg new file mode 100644 index 00000000..4e921928 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-context-selection.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-css-transform-percentage.svg b/fixtures/web-first/unsupported/svg-pattern-css-transform-percentage.svg new file mode 100644 index 00000000..ea517d32 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-css-transform-percentage.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-external.svg b/fixtures/web-first/unsupported/svg-pattern-external.svg new file mode 100644 index 00000000..9c364aaa --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-external.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-calc.svg b/fixtures/web-first/unsupported/svg-pattern-length-calc.svg new file mode 100644 index 00000000..0be7cae3 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-calc.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-css-comments.svg b/fixtures/web-first/unsupported/svg-pattern-length-css-comments.svg new file mode 100644 index 00000000..1e0e0da2 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-css-comments.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-css-wide.svg b/fixtures/web-first/unsupported/svg-pattern-length-css-wide.svg new file mode 100644 index 00000000..b823f65d --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-css-wide.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-unit.svg b/fixtures/web-first/unsupported/svg-pattern-length-unit.svg new file mode 100644 index 00000000..508fe47d --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-unit.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-used-range.svg b/fixtures/web-first/unsupported/svg-pattern-length-used-range.svg new file mode 100644 index 00000000..86054b93 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-used-range.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-length-var.svg b/fixtures/web-first/unsupported/svg-pattern-length-var.svg new file mode 100644 index 00000000..03274034 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-length-var.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-nested-composition-precision.svg b/fixtures/web-first/unsupported/svg-pattern-nested-composition-precision.svg new file mode 100644 index 00000000..3bc414e8 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-nested-composition-precision.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-nesting-too-deep.svg b/fixtures/web-first/unsupported/svg-pattern-nesting-too-deep.svg new file mode 100644 index 00000000..5644f3e4 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-nesting-too-deep.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-number-precision-alias.svg b/fixtures/web-first/unsupported/svg-pattern-number-precision-alias.svg new file mode 100644 index 00000000..2cbf8795 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-number-precision-alias.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-paint-server.svg b/fixtures/web-first/unsupported/svg-pattern-paint-server.svg deleted file mode 100644 index e75198c3..00000000 --- a/fixtures/web-first/unsupported/svg-pattern-paint-server.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fixtures/web-first/unsupported/svg-pattern-source-clip-precision.svg b/fixtures/web-first/unsupported/svg-pattern-source-clip-precision.svg new file mode 100644 index 00000000..a7571ee7 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-source-clip-precision.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-source-coverage-precision.svg b/fixtures/web-first/unsupported/svg-pattern-source-coverage-precision.svg new file mode 100644 index 00000000..e2abb62b --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-source-coverage-precision.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-source-effect-precision.svg b/fixtures/web-first/unsupported/svg-pattern-source-effect-precision.svg new file mode 100644 index 00000000..76ceda11 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-source-effect-precision.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-source-filter.svg b/fixtures/web-first/unsupported/svg-pattern-source-filter.svg new file mode 100644 index 00000000..11b85001 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-source-filter.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-source-unsupported.svg b/fixtures/web-first/unsupported/svg-pattern-source-unsupported.svg new file mode 100644 index 00000000..0121c2fe --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-source-unsupported.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-tile-sampling-precision.svg b/fixtures/web-first/unsupported/svg-pattern-tile-sampling-precision.svg new file mode 100644 index 00000000..f4e3fd4a --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-tile-sampling-precision.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-pattern-transform-none-provenance.svg b/fixtures/web-first/unsupported/svg-pattern-transform-none-provenance.svg new file mode 100644 index 00000000..31fc2181 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-pattern-transform-none-provenance.svg @@ -0,0 +1,12 @@ + + + + + + + + + + +