From 8240c39af08c44d6037d38e609749f0fb70908ef Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 28 Aug 2026 19:08:51 +0900 Subject: [PATCH 1/2] websem: admit same-document static SVG markers --- crates/n0/src/drawlist.rs | 2 + crates/n0/src/glyphless.rs | 58 +- crates/n0/src/paint.rs | 2 +- crates/n0_cli/README.md | 49 +- crates/rframe/README.md | 14 +- crates/rframe/src/clip.rs | 33 +- crates/rframe/src/lib.rs | 4 +- crates/rframe/tests/clip_contract.rs | 16 +- crates/websem/src/svg.rs | 1335 +++++++++++++++-- crates/websem/src/svg_path.rs | 580 ++++++- crates/websem/tests/marker_contract.rs | 329 ++++ crates/websem/tests/paths_contract.rs | 34 +- crates/websem/tests/points_contract.rs | 11 +- crates/websem/tests/unsupported_corpus.rs | 81 +- docs/wg/consolidation/svg-engine-of-record.md | 147 +- docs/wg/consolidation/web-checklist.md | 61 + .../chromium/svg/resources-and-effects.md | 22 +- fixtures/web-first/README.md | 5 + fixtures/web-first/STATUS.md | 113 +- .../svg-marker-bare-attribute-drop.png | Bin 0 -> 212 bytes .../chromium/svg-marker-circle-drop.png | Bin 0 -> 1652 bytes .../chromium/svg-marker-client-clip.png | Bin 0 -> 1305 bytes .../chromium/svg-marker-client-filter.png | Bin 0 -> 1506 bytes .../chromium/svg-marker-client-mask.png | Bin 0 -> 1104 bytes ...svg-marker-client-opacity-empty-source.png | Bin 0 -> 755 bytes .../svg-marker-client-opacity-missing.png | Bin 0 -> 771 bytes ...g-marker-client-opacity-unselected-mid.png | Bin 0 -> 558 bytes ...svg-marker-client-opacity-wrong-target.png | Bin 0 -> 771 bytes ...vg-marker-client-opacity-zero-viewport.png | Bin 0 -> 755 bytes .../chromium/svg-marker-client-opacity.png | Bin 0 -> 1562 bytes .../svg-marker-client-root-viewbox.png | Bin 0 -> 1805 bytes .../svg-marker-client-stroke-none-width.png | Bin 0 -> 211 bytes .../chromium/svg-marker-client-transform.png | Bin 0 -> 1730 bytes .../svg-marker-client-use-inherited.png | Bin 0 -> 288 bytes .../chromium/svg-marker-client-use-target.png | Bin 0 -> 1586 bytes .../chromium/svg-marker-context-fill.png | Bin 0 -> 269 bytes .../chromium/svg-marker-context-stroke.png | Bin 0 -> 265 bytes .../svg-marker-default-viewport-clip.png | Bin 0 -> 277 bytes .../svg-marker-display-none-resource.png | Bin 0 -> 275 bytes .../svg-marker-duplicate-id-first.png | Bin 0 -> 253 bytes .../chromium/svg-marker-ellipse-drop.png | Bin 0 -> 1387 bytes .../svg-marker-height-direct-alias.png | Bin 0 -> 212 bytes .../chromium/svg-marker-height-percentage.png | Bin 0 -> 210 bytes .../chromium/svg-marker-inherited-end.png | Bin 0 -> 275 bytes .../chromium/svg-marker-line-zero-length.png | Bin 0 -> 264 bytes .../web-first/chromium/svg-marker-line.png | Bin 0 -> 298 bytes .../chromium/svg-marker-missing-reference.png | Bin 0 -> 212 bytes .../chromium/svg-marker-none-reset.png | Bin 0 -> 212 bytes .../svg-marker-orient-auto-start-reverse.png | Bin 0 -> 334 bytes .../chromium/svg-marker-orient-degrees.png | Bin 0 -> 332 bytes .../svg-marker-orient-direct-alias.png | Bin 0 -> 419 bytes .../chromium/svg-marker-orient-gradians.png | Bin 0 -> 332 bytes .../chromium/svg-marker-orient-invalid.png | Bin 0 -> 266 bytes .../chromium/svg-marker-orient-radians.png | Bin 0 -> 332 bytes .../chromium/svg-marker-orient-turn.png | Bin 0 -> 332 bytes .../svg-marker-orient-whitespace-drop.png | Bin 0 -> 1300 bytes .../svg-marker-path-arc-authored-vertex.png | Bin 0 -> 1266 bytes .../svg-marker-path-cubic-authored-vertex.png | Bin 0 -> 1390 bytes .../chromium/svg-marker-path-move-only.png | Bin 0 -> 256 bytes .../svg-marker-path-multiple-subpaths.png | Bin 0 -> 395 bytes ...-marker-path-quadratic-authored-vertex.png | Bin 0 -> 1390 bytes .../svg-marker-path-repeated-close.png | Bin 0 -> 703 bytes .../svg-marker-path-repeated-moves.png | Bin 0 -> 340 bytes .../chromium/svg-marker-path-valid-prefix.png | Bin 0 -> 700 bytes .../web-first/chromium/svg-marker-path.png | Bin 0 -> 857 bytes .../chromium/svg-marker-polygon-one-point.png | Bin 0 -> 264 bytes .../web-first/chromium/svg-marker-polygon.png | Bin 0 -> 1292 bytes .../svg-marker-polyline-one-point.png | Bin 0 -> 256 bytes .../chromium/svg-marker-polyline.png | Bin 0 -> 857 bytes .../chromium/svg-marker-rect-drop.png | Bin 0 -> 231 bytes .../svg-marker-reference-negative.png | Bin 0 -> 263 bytes .../chromium/svg-marker-refx-direct-alias.png | Bin 0 -> 212 bytes .../chromium/svg-marker-refx-keyword-drop.png | Bin 0 -> 274 bytes .../svg-marker-refx-percentage-root.png | Bin 0 -> 300 bytes .../svg-marker-refx-percentage-viewbox.png | Bin 0 -> 244 bytes .../chromium/svg-marker-refy-keyword-drop.png | Bin 0 -> 283 bytes .../svg-marker-refy-percentage-alias.png | Bin 0 -> 218 bytes .../chromium/svg-marker-source-empty.png | Bin 0 -> 179 bytes .../svg-marker-source-group-transform.png | Bin 0 -> 209 bytes .../chromium/svg-marker-source-shapes.png | Bin 0 -> 388 bytes .../svg-marker-tangent-angle-wrap.png | Bin 0 -> 265 bytes .../svg-marker-tangent-degenerate-cubic.png | Bin 0 -> 251 bytes ...vg-marker-tangent-degenerate-quadratic.png | Bin 0 -> 251 bytes .../chromium/svg-marker-tangent-opposite.png | Bin 0 -> 292 bytes .../chromium/svg-marker-units-default.png | Bin 0 -> 251 bytes .../chromium/svg-marker-units-invalid.png | Bin 0 -> 211 bytes .../svg-marker-units-stroke-width.png | Bin 0 -> 251 bytes ...svg-marker-units-user-space-whitespace.png | Bin 0 -> 362 bytes .../chromium/svg-marker-units-user-space.png | Bin 0 -> 240 bytes .../chromium/svg-marker-url-commented.png | Bin 0 -> 266 bytes .../chromium/svg-marker-url-escaped.png | Bin 0 -> 266 bytes .../svg-marker-url-malformed-drop.png | Bin 0 -> 179 bytes .../chromium/svg-marker-url-quoted.png | Bin 0 -> 266 bytes .../chromium/svg-marker-viewbox-empty.png | Bin 0 -> 179 bytes .../chromium/svg-marker-viewbox-invalid.png | Bin 0 -> 210 bytes .../chromium/svg-marker-viewbox-meet.png | Bin 0 -> 210 bytes .../chromium/svg-marker-viewbox-negative.png | Bin 0 -> 210 bytes .../chromium/svg-marker-viewbox-none.png | Bin 0 -> 208 bytes .../svg-marker-viewbox-nonzero-origin.png | Bin 0 -> 211 bytes .../chromium/svg-marker-viewbox-slice.png | Bin 0 -> 208 bytes .../svg-marker-width-direct-alias.png | Bin 0 -> 213 bytes .../chromium/svg-marker-width-invalid.png | Bin 0 -> 298 bytes .../chromium/svg-marker-width-negative.png | Bin 0 -> 212 bytes .../svg-marker-width-percentage-alias.png | Bin 0 -> 234 bytes .../chromium/svg-marker-width-percentage.png | Bin 0 -> 245 bytes .../chromium/svg-marker-width-px.png | Bin 0 -> 275 bytes .../chromium/svg-marker-width-zero.png | Bin 0 -> 212 bytes .../chromium/svg-marker-wrong-target.png | Bin 0 -> 212 bytes .../chromium/svg-path-marker-end.png | Bin 0 -> 519 bytes fixtures/web-first/oracle-bake.json | 812 +++++++++- fixtures/web-first/primitives.json | 720 +++++++++ .../svg-marker-bare-attribute-drop.svg | 1 + fixtures/web-first/svg-marker-circle-drop.svg | 1 + fixtures/web-first/svg-marker-client-clip.svg | 1 + .../web-first/svg-marker-client-filter.svg | 1 + fixtures/web-first/svg-marker-client-mask.svg | 1 + ...svg-marker-client-opacity-empty-source.svg | 5 + .../svg-marker-client-opacity-missing.svg | 4 + ...g-marker-client-opacity-unselected-mid.svg | 5 + ...svg-marker-client-opacity-wrong-target.svg | 5 + ...vg-marker-client-opacity-zero-viewport.svg | 5 + .../web-first/svg-marker-client-opacity.svg | 1 + .../svg-marker-client-root-viewbox.svg | 1 + .../svg-marker-client-stroke-none-width.svg | 1 + .../web-first/svg-marker-client-transform.svg | 1 + .../svg-marker-client-use-inherited.svg | 1 + .../svg-marker-client-use-target.svg | 1 + .../web-first/svg-marker-context-fill.svg | 1 + .../web-first/svg-marker-context-stroke.svg | 1 + .../svg-marker-default-viewport-clip.svg | 1 + .../svg-marker-display-none-resource.svg | 1 + .../svg-marker-duplicate-id-first.svg | 1 + .../web-first/svg-marker-ellipse-drop.svg | 1 + .../svg-marker-height-direct-alias.svg | 1 + .../svg-marker-height-percentage.svg | 1 + .../web-first/svg-marker-inherited-end.svg | 1 + .../web-first/svg-marker-line-zero-length.svg | 1 + fixtures/web-first/svg-marker-line.svg | 1 + .../svg-marker-missing-reference.svg | 1 + fixtures/web-first/svg-marker-none-reset.svg | 1 + .../svg-marker-orient-auto-start-reverse.svg | 1 + .../web-first/svg-marker-orient-degrees.svg | 1 + .../svg-marker-orient-direct-alias.svg | 1 + .../web-first/svg-marker-orient-gradians.svg | 1 + .../web-first/svg-marker-orient-invalid.svg | 1 + .../web-first/svg-marker-orient-radians.svg | 1 + fixtures/web-first/svg-marker-orient-turn.svg | 1 + .../svg-marker-orient-whitespace-drop.svg | 1 + .../svg-marker-path-arc-authored-vertex.svg | 1 + .../svg-marker-path-cubic-authored-vertex.svg | 1 + .../web-first/svg-marker-path-move-only.svg | 1 + .../svg-marker-path-multiple-subpaths.svg | 1 + ...-marker-path-quadratic-authored-vertex.svg | 1 + .../svg-marker-path-repeated-close.svg | 1 + .../svg-marker-path-repeated-moves.svg | 1 + .../svg-marker-path-valid-prefix.svg | 1 + fixtures/web-first/svg-marker-path.svg | 1 + .../svg-marker-polygon-one-point.svg | 1 + fixtures/web-first/svg-marker-polygon.svg | 1 + .../svg-marker-polyline-one-point.svg | 1 + fixtures/web-first/svg-marker-polyline.svg | 1 + fixtures/web-first/svg-marker-rect-drop.svg | 1 + .../svg-marker-reference-negative.svg | 1 + .../svg-marker-refx-direct-alias.svg | 1 + .../svg-marker-refx-keyword-drop.svg | 1 + .../svg-marker-refx-percentage-root.svg | 1 + .../svg-marker-refx-percentage-viewbox.svg | 1 + .../svg-marker-refy-keyword-drop.svg | 1 + .../svg-marker-refy-percentage-alias.svg | 1 + .../web-first/svg-marker-source-empty.svg | 1 + .../svg-marker-source-group-transform.svg | 1 + .../web-first/svg-marker-source-shapes.svg | 1 + .../svg-marker-tangent-angle-wrap.svg | 1 + .../svg-marker-tangent-degenerate-cubic.svg | 1 + ...vg-marker-tangent-degenerate-quadratic.svg | 1 + .../web-first/svg-marker-tangent-opposite.svg | 1 + .../web-first/svg-marker-units-default.svg | 1 + .../web-first/svg-marker-units-invalid.svg | 1 + .../svg-marker-units-stroke-width.svg | 1 + ...svg-marker-units-user-space-whitespace.svg | 1 + .../web-first/svg-marker-units-user-space.svg | 1 + .../web-first/svg-marker-url-commented.svg | 1 + fixtures/web-first/svg-marker-url-escaped.svg | 1 + .../svg-marker-url-malformed-drop.svg | 1 + fixtures/web-first/svg-marker-url-quoted.svg | 1 + .../web-first/svg-marker-viewbox-empty.svg | 1 + .../web-first/svg-marker-viewbox-invalid.svg | 1 + .../web-first/svg-marker-viewbox-meet.svg | 1 + .../web-first/svg-marker-viewbox-negative.svg | 1 + .../web-first/svg-marker-viewbox-none.svg | 1 + .../svg-marker-viewbox-nonzero-origin.svg | 1 + .../web-first/svg-marker-viewbox-slice.svg | 1 + .../svg-marker-width-direct-alias.svg | 1 + .../web-first/svg-marker-width-invalid.svg | 1 + .../web-first/svg-marker-width-negative.svg | 1 + .../svg-marker-width-percentage-alias.svg | 1 + .../web-first/svg-marker-width-percentage.svg | 1 + fixtures/web-first/svg-marker-width-px.svg | 1 + fixtures/web-first/svg-marker-width-zero.svg | 1 + .../web-first/svg-marker-wrong-target.svg | 1 + .../{unsupported => }/svg-path-marker-end.svg | 0 fixtures/web-first/unsupported/README.md | 11 +- .../svg-marker-author-stylesheet.svg | 5 + .../unsupported/svg-marker-external.svg | 3 + .../unsupported/svg-marker-height-var.svg | 4 + .../unsupported/svg-marker-orient-var.svg | 4 + .../unsupported/svg-marker-overflow.svg | 4 + .../unsupported/svg-marker-refx-unit.svg | 4 + .../unsupported/svg-marker-refy-css-wide.svg | 4 + .../unsupported/svg-marker-root-effect.svg | 7 + .../unsupported/svg-marker-root-opacity.svg | 4 + .../svg-marker-root-rendering-style.svg | 8 + .../unsupported/svg-marker-root-transform.svg | 4 + .../svg-marker-source-animation.svg | 8 + .../svg-marker-source-gradient.svg | 9 + .../unsupported/svg-marker-source-nested.svg | 9 + .../unsupported/svg-marker-source-opacity.svg | 8 + .../unsupported/svg-marker-source-text.svg | 6 + .../unsupported/svg-marker-source-use.svg | 7 + .../unsupported/svg-marker-width-calc.svg | 4 + 220 files changed, 4482 insertions(+), 167 deletions(-) create mode 100644 crates/websem/tests/marker_contract.rs create mode 100644 fixtures/web-first/chromium/svg-marker-bare-attribute-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-circle-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-clip.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-filter.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-mask.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity-empty-source.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity-missing.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity-unselected-mid.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity-wrong-target.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity-zero-viewport.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-opacity.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-root-viewbox.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-stroke-none-width.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-transform.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-use-inherited.png create mode 100644 fixtures/web-first/chromium/svg-marker-client-use-target.png create mode 100644 fixtures/web-first/chromium/svg-marker-context-fill.png create mode 100644 fixtures/web-first/chromium/svg-marker-context-stroke.png create mode 100644 fixtures/web-first/chromium/svg-marker-default-viewport-clip.png create mode 100644 fixtures/web-first/chromium/svg-marker-display-none-resource.png create mode 100644 fixtures/web-first/chromium/svg-marker-duplicate-id-first.png create mode 100644 fixtures/web-first/chromium/svg-marker-ellipse-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-height-direct-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-height-percentage.png create mode 100644 fixtures/web-first/chromium/svg-marker-inherited-end.png create mode 100644 fixtures/web-first/chromium/svg-marker-line-zero-length.png create mode 100644 fixtures/web-first/chromium/svg-marker-line.png create mode 100644 fixtures/web-first/chromium/svg-marker-missing-reference.png create mode 100644 fixtures/web-first/chromium/svg-marker-none-reset.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-auto-start-reverse.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-degrees.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-direct-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-gradians.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-invalid.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-radians.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-turn.png create mode 100644 fixtures/web-first/chromium/svg-marker-orient-whitespace-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-arc-authored-vertex.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-cubic-authored-vertex.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-move-only.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-multiple-subpaths.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-quadratic-authored-vertex.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-repeated-close.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-repeated-moves.png create mode 100644 fixtures/web-first/chromium/svg-marker-path-valid-prefix.png create mode 100644 fixtures/web-first/chromium/svg-marker-path.png create mode 100644 fixtures/web-first/chromium/svg-marker-polygon-one-point.png create mode 100644 fixtures/web-first/chromium/svg-marker-polygon.png create mode 100644 fixtures/web-first/chromium/svg-marker-polyline-one-point.png create mode 100644 fixtures/web-first/chromium/svg-marker-polyline.png create mode 100644 fixtures/web-first/chromium/svg-marker-rect-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-reference-negative.png create mode 100644 fixtures/web-first/chromium/svg-marker-refx-direct-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-refx-keyword-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-refx-percentage-root.png create mode 100644 fixtures/web-first/chromium/svg-marker-refx-percentage-viewbox.png create mode 100644 fixtures/web-first/chromium/svg-marker-refy-keyword-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-refy-percentage-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-source-empty.png create mode 100644 fixtures/web-first/chromium/svg-marker-source-group-transform.png create mode 100644 fixtures/web-first/chromium/svg-marker-source-shapes.png create mode 100644 fixtures/web-first/chromium/svg-marker-tangent-angle-wrap.png create mode 100644 fixtures/web-first/chromium/svg-marker-tangent-degenerate-cubic.png create mode 100644 fixtures/web-first/chromium/svg-marker-tangent-degenerate-quadratic.png create mode 100644 fixtures/web-first/chromium/svg-marker-tangent-opposite.png create mode 100644 fixtures/web-first/chromium/svg-marker-units-default.png create mode 100644 fixtures/web-first/chromium/svg-marker-units-invalid.png create mode 100644 fixtures/web-first/chromium/svg-marker-units-stroke-width.png create mode 100644 fixtures/web-first/chromium/svg-marker-units-user-space-whitespace.png create mode 100644 fixtures/web-first/chromium/svg-marker-units-user-space.png create mode 100644 fixtures/web-first/chromium/svg-marker-url-commented.png create mode 100644 fixtures/web-first/chromium/svg-marker-url-escaped.png create mode 100644 fixtures/web-first/chromium/svg-marker-url-malformed-drop.png create mode 100644 fixtures/web-first/chromium/svg-marker-url-quoted.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-empty.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-invalid.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-meet.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-negative.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-none.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-nonzero-origin.png create mode 100644 fixtures/web-first/chromium/svg-marker-viewbox-slice.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-direct-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-invalid.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-negative.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-percentage-alias.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-percentage.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-px.png create mode 100644 fixtures/web-first/chromium/svg-marker-width-zero.png create mode 100644 fixtures/web-first/chromium/svg-marker-wrong-target.png create mode 100644 fixtures/web-first/chromium/svg-path-marker-end.png create mode 100644 fixtures/web-first/svg-marker-bare-attribute-drop.svg create mode 100644 fixtures/web-first/svg-marker-circle-drop.svg create mode 100644 fixtures/web-first/svg-marker-client-clip.svg create mode 100644 fixtures/web-first/svg-marker-client-filter.svg create mode 100644 fixtures/web-first/svg-marker-client-mask.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity-empty-source.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity-missing.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity-unselected-mid.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity-wrong-target.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity-zero-viewport.svg create mode 100644 fixtures/web-first/svg-marker-client-opacity.svg create mode 100644 fixtures/web-first/svg-marker-client-root-viewbox.svg create mode 100644 fixtures/web-first/svg-marker-client-stroke-none-width.svg create mode 100644 fixtures/web-first/svg-marker-client-transform.svg create mode 100644 fixtures/web-first/svg-marker-client-use-inherited.svg create mode 100644 fixtures/web-first/svg-marker-client-use-target.svg create mode 100644 fixtures/web-first/svg-marker-context-fill.svg create mode 100644 fixtures/web-first/svg-marker-context-stroke.svg create mode 100644 fixtures/web-first/svg-marker-default-viewport-clip.svg create mode 100644 fixtures/web-first/svg-marker-display-none-resource.svg create mode 100644 fixtures/web-first/svg-marker-duplicate-id-first.svg create mode 100644 fixtures/web-first/svg-marker-ellipse-drop.svg create mode 100644 fixtures/web-first/svg-marker-height-direct-alias.svg create mode 100644 fixtures/web-first/svg-marker-height-percentage.svg create mode 100644 fixtures/web-first/svg-marker-inherited-end.svg create mode 100644 fixtures/web-first/svg-marker-line-zero-length.svg create mode 100644 fixtures/web-first/svg-marker-line.svg create mode 100644 fixtures/web-first/svg-marker-missing-reference.svg create mode 100644 fixtures/web-first/svg-marker-none-reset.svg create mode 100644 fixtures/web-first/svg-marker-orient-auto-start-reverse.svg create mode 100644 fixtures/web-first/svg-marker-orient-degrees.svg create mode 100644 fixtures/web-first/svg-marker-orient-direct-alias.svg create mode 100644 fixtures/web-first/svg-marker-orient-gradians.svg create mode 100644 fixtures/web-first/svg-marker-orient-invalid.svg create mode 100644 fixtures/web-first/svg-marker-orient-radians.svg create mode 100644 fixtures/web-first/svg-marker-orient-turn.svg create mode 100644 fixtures/web-first/svg-marker-orient-whitespace-drop.svg create mode 100644 fixtures/web-first/svg-marker-path-arc-authored-vertex.svg create mode 100644 fixtures/web-first/svg-marker-path-cubic-authored-vertex.svg create mode 100644 fixtures/web-first/svg-marker-path-move-only.svg create mode 100644 fixtures/web-first/svg-marker-path-multiple-subpaths.svg create mode 100644 fixtures/web-first/svg-marker-path-quadratic-authored-vertex.svg create mode 100644 fixtures/web-first/svg-marker-path-repeated-close.svg create mode 100644 fixtures/web-first/svg-marker-path-repeated-moves.svg create mode 100644 fixtures/web-first/svg-marker-path-valid-prefix.svg create mode 100644 fixtures/web-first/svg-marker-path.svg create mode 100644 fixtures/web-first/svg-marker-polygon-one-point.svg create mode 100644 fixtures/web-first/svg-marker-polygon.svg create mode 100644 fixtures/web-first/svg-marker-polyline-one-point.svg create mode 100644 fixtures/web-first/svg-marker-polyline.svg create mode 100644 fixtures/web-first/svg-marker-rect-drop.svg create mode 100644 fixtures/web-first/svg-marker-reference-negative.svg create mode 100644 fixtures/web-first/svg-marker-refx-direct-alias.svg create mode 100644 fixtures/web-first/svg-marker-refx-keyword-drop.svg create mode 100644 fixtures/web-first/svg-marker-refx-percentage-root.svg create mode 100644 fixtures/web-first/svg-marker-refx-percentage-viewbox.svg create mode 100644 fixtures/web-first/svg-marker-refy-keyword-drop.svg create mode 100644 fixtures/web-first/svg-marker-refy-percentage-alias.svg create mode 100644 fixtures/web-first/svg-marker-source-empty.svg create mode 100644 fixtures/web-first/svg-marker-source-group-transform.svg create mode 100644 fixtures/web-first/svg-marker-source-shapes.svg create mode 100644 fixtures/web-first/svg-marker-tangent-angle-wrap.svg create mode 100644 fixtures/web-first/svg-marker-tangent-degenerate-cubic.svg create mode 100644 fixtures/web-first/svg-marker-tangent-degenerate-quadratic.svg create mode 100644 fixtures/web-first/svg-marker-tangent-opposite.svg create mode 100644 fixtures/web-first/svg-marker-units-default.svg create mode 100644 fixtures/web-first/svg-marker-units-invalid.svg create mode 100644 fixtures/web-first/svg-marker-units-stroke-width.svg create mode 100644 fixtures/web-first/svg-marker-units-user-space-whitespace.svg create mode 100644 fixtures/web-first/svg-marker-units-user-space.svg create mode 100644 fixtures/web-first/svg-marker-url-commented.svg create mode 100644 fixtures/web-first/svg-marker-url-escaped.svg create mode 100644 fixtures/web-first/svg-marker-url-malformed-drop.svg create mode 100644 fixtures/web-first/svg-marker-url-quoted.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-empty.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-invalid.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-meet.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-negative.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-none.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-nonzero-origin.svg create mode 100644 fixtures/web-first/svg-marker-viewbox-slice.svg create mode 100644 fixtures/web-first/svg-marker-width-direct-alias.svg create mode 100644 fixtures/web-first/svg-marker-width-invalid.svg create mode 100644 fixtures/web-first/svg-marker-width-negative.svg create mode 100644 fixtures/web-first/svg-marker-width-percentage-alias.svg create mode 100644 fixtures/web-first/svg-marker-width-percentage.svg create mode 100644 fixtures/web-first/svg-marker-width-px.svg create mode 100644 fixtures/web-first/svg-marker-width-zero.svg create mode 100644 fixtures/web-first/svg-marker-wrong-target.svg rename fixtures/web-first/{unsupported => }/svg-path-marker-end.svg (100%) create mode 100644 fixtures/web-first/unsupported/svg-marker-author-stylesheet.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-external.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-height-var.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-orient-var.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-overflow.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-refx-unit.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-refy-css-wide.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-root-effect.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-root-opacity.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-root-rendering-style.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-root-transform.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-animation.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-gradient.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-nested.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-opacity.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-text.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-source-use.svg create mode 100644 fixtures/web-first/unsupported/svg-marker-width-calc.svg diff --git a/crates/n0/src/drawlist.rs b/crates/n0/src/drawlist.rs index eec8025e..887e8765 100644 --- a/crates/n0/src/drawlist.rs +++ b/crates/n0/src/drawlist.rs @@ -43,6 +43,8 @@ pub(crate) struct ResolvedClipLayer { #[derive(Debug, Clone, PartialEq)] pub struct ResolvedClipPath { pub(crate) layers: Arc<[ResolvedClipLayer]>, + /// Whether the backend should compute fractional edge coverage. + pub(crate) anti_alias: bool, } /// Drawlist projection of a resolved mask-source interpretation. diff --git a/crates/n0/src/glyphless.rs b/crates/n0/src/glyphless.rs index 1b3cd224..d08ad73c 100644 --- a/crates/n0/src/glyphless.rs +++ b/crates/n0/src/glyphless.rs @@ -912,7 +912,10 @@ fn compile_clip_path(clip: &ClipPath) -> ResolvedClipPath { }) .collect::>() .into(); - ResolvedClipPath { layers } + ResolvedClipPath { + layers, + anti_alias: clip.edge_mode() == rframe::ClipEdgeMode::AntiAliased, + } } /// Project one checked, source-neutral filter program into private painter @@ -2038,6 +2041,14 @@ mod tests { } fn clip_begin(owner: VisualRef, layers: Vec>) -> FrameItem { + clip_begin_with_edge(owner, layers, rframe::ClipEdgeMode::AntiAliased) + } + + fn clip_begin_with_edge( + owner: VisualRef, + layers: Vec>, + edge_mode: rframe::ClipEdgeMode, + ) -> FrameItem { let layers = layers .into_iter() .map(|geometries| { @@ -2054,7 +2065,8 @@ mod tests { FrameItem::ScopeBegin(Scope { owner, effect: ScopeEffect::Clip( - rframe::ClipPath::new(layers).expect("test clip has at least one layer"), + rframe::ClipPath::new_with_edge_mode(layers, edge_mode) + .expect("test clip has at least one layer"), ), }) } @@ -3363,6 +3375,48 @@ mod tests { assert_eq!(rgba_at(&pixels, 64, 12, 24), [0xff, 0xff, 0xff, 0xff]); } + #[test] + fn hard_clip_edge_policy_reaches_the_painter() { + let scene = |edge_mode| { + let items = FrameItems::try_new(vec![ + clip_begin_with_edge( + SCOPE_OWNER, + vec![vec![( + Rectangle::from_xywh(8.25, 6.25, 20.0, 16.0), + AffineTransform::identity(), + )]], + edge_mode, + ), + FrameItem::Node(rect_node( + RECT_OWNER, + Rectangle::from_xywh(0.0, 0.0, 64.0, 48.0), + 0xFF16_A34A, + )), + FrameItem::ScopeEnd, + ]) + .expect("balanced clip scope"); + compile(frame_of(items)).expect("admitted clip scene") + }; + + let ordinary = scene(rframe::ClipEdgeMode::AntiAliased); + let hard = scene(rframe::ClipEdgeMode::Hard); + let ItemKind::BeginClipPath { clip } = &hard.drawlist.items[1].kind else { + panic!("resolved clip item") + }; + assert!(!clip.anti_alias, "hard policy survives contract lowering"); + + let raster = |product: &FrameProduct| { + product + .raster_to_bytes(&AffineTransform::identity(), 64, 48, &PaintCtx::new(None)) + .expect("resource-free clip raster") + }; + assert_ne!( + raster(&ordinary), + raster(&hard), + "the backend edge flag changes fractional clip coverage" + ); + } + #[test] fn an_empty_clip_layer_is_the_checked_clip_all_fact() { let items = FrameItems::try_new(vec![ diff --git a/crates/n0/src/paint.rs b/crates/n0/src/paint.rs index 6e0bfbf9..466bc4fc 100644 --- a/crates/n0/src/paint.rs +++ b/crates/n0/src/paint.rs @@ -3898,7 +3898,7 @@ pub fn execute_unchecked(canvas: &Canvas, list: &DrawList, view: &Affine, .expect("geometric clip path operations were preflighted at product build"); canvas.save(); canvas.set_matrix(&skia_matrix(&total).into()); - canvas.clip_path(&path, ClipOp::Intersect, true); + canvas.clip_path(&path, ClipOp::Intersect, clip.anti_alias); scopes.push(Scope::Clip); } ItemKind::EndClip => { diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index f1064671..1f14d9af 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -539,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 936 Chromium-baked cells plus 16 sampled - frames, with 172 named + cells. The complete corpus contains 1,026 Chromium-baked cells plus 16 sampled + frames, with 189 named refusal rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, @@ -851,6 +851,51 @@ cargo run -p n0_cli --bin n0 -- \ formerly admitted only by Rust, and one valid midpoint-adjacent decimal formerly selected the wrong binary32 neighbour. Three focused refusal rows guard those classes. The `` and `` rows remain open. + `` is consumed in one bounded, same-document static profile. + Direct inherited `marker-start`, `marker-mid`, and `marker-end` references + apply to ``, ``, ``, and ``; `none`, malformed + hints, missing or wrong-kind targets, first-id lookup, quoted/escaped URL + forms, and `` clients follow Chromium's measured selection behavior. + CSS marker properties remain authored-ingress refusals because the pinned + Stylo build has no marker longhands. Chromium's bare `marker` attribute and + marker attributes on rect/circle/ellipse are inert and celled as such. + Placement retains authored vertex topology independently of raster path + decomposition: start and end belong to the whole path, later subpath moves + are mids, close back-patches the start tangent and contributes its duplicate + vertex, and one authored cubic, quadratic, or arc contributes one marker + edge. Move-only, one-point, valid-prefix, zero-length, degenerate-tangent, + angle-wrap, and exact-opposite cases are baked. + The marker viewport admits numeric and percentage `markerWidth`, + `markerHeight`, `refX`, and `refY`, plus `px`; both `markerUnits` branches; + explicit `orient` angles in every listed angle unit, `auto`, and + `auto-start-reverse`; and `viewBox` through the complete admitted + `preserveAspectRatio` mapper. Length percentages use the outer SVG viewport + axes even when the marker has a `viewBox`, as Chromium measures. Invalid or + non-positive viewport geometry produces the measured nothing. The marker + viewport is a hard clip, not ordinary anti-aliased clip-path coverage. + A source may contain the admitted solid/context-solid shapes and groups with + transforms. Client transforms, computed stroke-width scaling even when the + stroke paint is none, opacity, clip, mask, filter, root mapping, and context + paint retain their measured order. Every instance lowers to ordinary + source-neutral frame nodes and hard geometric clip scopes; no marker, + resource id, URL, or authored topology crosses `rframe`. + Marker opacity composition begins only when an authored vertex kind selects + an actual marker resource. Missing ids, wrong-kind targets, and a valid + marker property for a vertex kind the client does not have are + Chromium-equivalent to `none`; a selected real marker still chooses the + combined span when its viewport is zero or its valid source is empty. Five + exact cells guard this distinction. + What refuses by stable marker name: external URLs; author CSS around a used + marker; CSS math, custom properties, wider units, and CSS-wide resource + values; resource-root overflow, effects, opacity, transform, or an inherited + rendering declaration; source + dynamics, paint servers, effects, nested markers, text, and ``; and the + checked position/source/client fan-out limits. Source compilation is one + transaction: strict refuses, while best effort removes the complete client + span rather than leaking its ordinary fill or stroke. Ninety exact + Chromium cells carry M1. The element, three presentation attributes, four + CSS property rows, and six marker-resource attribute rows remain open for + the wider source, cascade, grammar, dynamics, and external-I/O surface. `` 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 d4b2b5b9..26506433 100644 --- a/crates/rframe/README.md +++ b/crates/rframe/README.md @@ -25,7 +25,7 @@ producer (e.g. websem, from SVG) | `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 | +| `clip` | `ClipPath` — bounded path unions intersected in layers, with resolved transforms, conservative bounds, and an explicit anti-aliased or hard edge policy | 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 @@ -89,11 +89,13 @@ 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 -path; text/raster fallback and image-backed masks remain inexpressible. Groups -as authored first-class nodes are likewise absent. A producer that meets one of -those boundaries must refuse or declare it rather than lower it into something -this contract cannot hold. +layers intersect. Its edge policy is one resolved raster fact: ordinary +coverage is anti-aliased by default, while a producer may explicitly require a +hard boundary. It carries no URL, element, paint, alpha mask, backend path, or +source reason for that policy; text/raster fallback and image-backed masks +remain inexpressible. Groups as authored first-class nodes are likewise absent. +A producer that meets one of those boundaries must refuse or declare it rather +than lower it into something this contract cannot hold. Why this shape is chosen, and where the renderer joins it, is recorded in [docs/wg/consolidation/n0-join-point.md](../../docs/wg/consolidation/n0-join-point.md). diff --git a/crates/rframe/src/clip.rs b/crates/rframe/src/clip.rs index 6ca84fec..a75e9e5b 100644 --- a/crates/rframe/src/clip.rs +++ b/crates/rframe/src/clip.rs @@ -29,6 +29,18 @@ pub const MAX_CLIP_GEOMETRIES_PER_LAYER: usize = 42; /// The maximum number of chained clip layers in one resolved effect. pub const MAX_CLIP_LAYERS: usize = 64; +/// Raster edge policy for a resolved geometric clip. +/// +/// Ordinary SVG `clip-path` coverage is anti-aliased. Viewport clips are hard +/// pixel masks in Chromium, including after an affine mapping; carrying that +/// distinction here avoids smuggling source vocabulary into the consumer. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ClipEdgeMode { + #[default] + AntiAliased, + Hard, +} + /// Why resolved clip geometry cannot cross the contract. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ClipGeometryError { @@ -192,10 +204,18 @@ impl std::error::Error for ClipPathError {} pub struct ClipPath { layers: Arc<[ClipLayer]>, bounds: Option, + edge_mode: ClipEdgeMode, } impl ClipPath { pub fn new(layers: impl Into>) -> Result { + Self::new_with_edge_mode(layers, ClipEdgeMode::AntiAliased) + } + + pub fn new_with_edge_mode( + layers: impl Into>, + edge_mode: ClipEdgeMode, + ) -> Result { let layers = layers.into(); if layers.is_empty() { return Err(ClipPathError::NoLayers); @@ -212,7 +232,11 @@ impl ClipPath { _ => None, }; } - Ok(Self { layers, bounds }) + Ok(Self { + layers, + bounds, + edge_mode, + }) } #[must_use] @@ -226,6 +250,13 @@ impl ClipPath { pub const fn bounds(&self) -> Option { self.bounds } + + /// Whether the clip edge carries ordinary coverage AA or a hard viewport + /// boundary. + #[must_use] + pub const fn edge_mode(&self) -> ClipEdgeMode { + self.edge_mode + } } fn valid_rectangle(rectangle: Rectangle) -> bool { diff --git a/crates/rframe/src/lib.rs b/crates/rframe/src/lib.rs index d4b201ee..e6a7b654 100644 --- a/crates/rframe/src/lib.rs +++ b/crates/rframe/src/lib.rs @@ -21,8 +21,8 @@ mod scope; mod stroke; pub use clip::{ - ClipGeometry, ClipGeometryError, ClipLayer, ClipLayerError, ClipPath, ClipPathError, - MAX_CLIP_GEOMETRIES_PER_LAYER, MAX_CLIP_LAYERS, + ClipEdgeMode, ClipGeometry, ClipGeometryError, ClipLayer, ClipLayerError, ClipPath, + ClipPathError, MAX_CLIP_GEOMETRIES_PER_LAYER, MAX_CLIP_LAYERS, }; pub use filter::{ Filter, FilterBlend, FilterChannelTables, FilterColorSpace, FilterComposite, diff --git a/crates/rframe/tests/clip_contract.rs b/crates/rframe/tests/clip_contract.rs index 85784c70..736a460b 100644 --- a/crates/rframe/tests/clip_contract.rs +++ b/crates/rframe/tests/clip_contract.rs @@ -1,8 +1,8 @@ use math2::Rectangle; use math2::transform::AffineTransform; use rframe::{ - ClipGeometry, ClipGeometryError, ClipLayer, ClipLayerError, ClipPath, ClipPathError, Geometry, - MAX_CLIP_GEOMETRIES_PER_LAYER, MAX_CLIP_LAYERS, + ClipEdgeMode, ClipGeometry, ClipGeometryError, ClipLayer, ClipLayerError, ClipPath, + ClipPathError, Geometry, MAX_CLIP_GEOMETRIES_PER_LAYER, MAX_CLIP_LAYERS, }; fn rect(x: f32, y: f32, width: f32, height: f32) -> ClipGeometry { @@ -41,6 +41,18 @@ fn an_empty_layer_is_a_valid_clip_all_fact() { assert_eq!(clip.bounds(), None); } +#[test] +fn edge_policy_is_explicit_and_ordinary_clips_stay_antialiased() { + let layer = || ClipLayer::new(vec![rect(0.0, 0.0, 10.0, 10.0)]).unwrap(); + let ordinary = ClipPath::new(vec![layer()]).unwrap(); + assert_eq!(ordinary.edge_mode(), ClipEdgeMode::AntiAliased); + + let hard = ClipPath::new_with_edge_mode(vec![layer()], ClipEdgeMode::Hard).unwrap(); + assert_eq!(hard.edge_mode(), ClipEdgeMode::Hard); + assert_eq!(hard.layers(), ordinary.layers()); + assert_eq!(hard.bounds(), ordinary.bounds()); +} + #[test] fn geometry_checks_local_and_transformed_finiteness() { assert_eq!( diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index e04cc099..e18c46df 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -345,6 +345,9 @@ pub enum CompileError { /// A filter value, graph, or operation that cannot be resolved into the /// admitted source-neutral image-filter program. UnsupportedFilter(String), + /// A marker reference, placement value, authored vertex projection, or + /// source subtree outside the admitted bounded marker program. + UnsupportedMarker(String), /// A numeric attribute failed to parse. BadNumber { attr: String, value: String }, /// Viewport sizing needs a default/CSS sizing path this slice lacks. @@ -885,6 +888,9 @@ impl std::fmt::Display for CompileError { CompileError::UnsupportedFilter(reason) => { write!(f, "unsupported SVG filter: {reason}") } + CompileError::UnsupportedMarker(reason) => { + write!(f, "unsupported SVG marker: {reason}") + } CompileError::BadNumber { attr, value } => { write!(f, "attribute {attr}={value:?} is not a number") } @@ -1081,13 +1087,11 @@ const RENDERING_ATTRIBUTES_NOT_CONSUMED: &[&str] = &[ // presentation hint (csscascade), the currentColor basis the paint // resolvers already read from the cascade. "paint-order", - // Markers apply to ``, ``, `` and ``, and - // this slice admits the first two. Nothing else "reads" a marker - // property — the property *is* the paint trigger — so this patrol is what - // keeps Chromium's arrowhead from becoming a silent hole. - "marker-start", - "marker-mid", - "marker-end", + // The direct marker resolver owns marker-start/mid/end on Chromium's four + // applicable shapes, including inheritance. Current Chromium drops those + // attributes on the other admitted shapes, so a global patrol would + // over-refuse the measured correct nothing. CSS spellings remain in the + // pinned-cascade patrol below; no matcher is added around Stylo. "shape-rendering", "image-rendering", "color-rendering", @@ -1914,6 +1918,7 @@ fn measure_use_boxes_in_subtree( || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" + || tag == "marker" { child = el.next_element_sibling(); continue; @@ -2022,6 +2027,7 @@ fn measure_effect_boxes_in_subtree( || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" + || tag == "marker" { child = el.next_element_sibling(); continue; @@ -2103,6 +2109,7 @@ fn measure_subtree_geometry( || tag == "linearGradient" || tag == "radialGradient" || tag == "pattern" + || tag == "marker" { child = el.next_element_sibling(); continue; @@ -2554,6 +2561,7 @@ 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 markers = marker_resource::Resources::collect(document_root(svg), svg); let has_author_css = document_has_author_css(svg); let patterns = PatternCompiler { values, @@ -2564,6 +2572,7 @@ fn compile_svg_element( clips: &clips, masks: &masks, filters: &filters, + markers: &markers, fonts, }; let GeometryMeasurements { @@ -2581,6 +2590,7 @@ fn compile_svg_element( clips: &clips, masks: &masks, filters: &filters, + markers: &markers, patterns: &patterns, use_boxes, effect_boxes, @@ -2591,6 +2601,7 @@ fn compile_svg_element( top_level_shapes: Vec::new(), active_masks: Vec::new(), active_patterns: Vec::new(), + active_markers: Vec::new(), next_id: 0, }; if root_disposition != RenderDisposition::PrunedSubtree || initial_viewport.is_some() { @@ -2717,6 +2728,7 @@ struct PatternCompiler<'a> { clips: &'a clip_path::Resources<'a>, masks: &'a mask_resource::Resources<'a>, filters: &'a filter_resource::Resources<'a>, + markers: &'a marker_resource::Resources<'a>, fonts: &'a textlayout::Environment, } @@ -3039,6 +3051,7 @@ impl<'a> PatternCompiler<'a> { clips: self.clips, masks: self.masks, filters: self.filters, + markers: self.markers, patterns: self, use_boxes, effect_boxes, @@ -3049,6 +3062,7 @@ impl<'a> PatternCompiler<'a> { top_level_shapes: Vec::new(), active_masks: Vec::new(), active_patterns: source_active_patterns, + active_markers: Vec::new(), next_id: 0, }; walk.compile_children( @@ -3365,6 +3379,678 @@ fn pattern_view_box(chain: &[HtmlElement<'_>]) -> PatternViewBox { } } +/// Same-document SVG marker resources and their direct attribute grammar. +/// +/// The pinned Servo cascade has no marker longhands. Client references are +/// therefore decoded from inherited presentation attributes only, while the +/// document-wide authored-CSS patrol continues to quarantine every CSS +/// spelling. Resource values use cssparser's f64-accumulating token route — +/// the same normalization order the Chromium precision probes selected — and +/// are fully resolved before ordinary frame items are emitted. +mod marker_resource { + use std::collections::HashMap; + + use cssparser::{Parser, ParserInput, Token}; + + use super::*; + + pub(super) const MAX_POSITIONS_PER_CLIENT: usize = 4096; + pub(super) const MAX_SOURCE_ITEMS: usize = 64; + pub(super) const MAX_ITEMS_PER_CLIENT: usize = 16_384; + + enum Resource<'d> { + Marker { + element: HtmlElement<'d>, + inside_compiled_svg: bool, + }, + Other, + } + + pub(super) struct Resources<'d> { + by_fragment: HashMap>, + } + + #[derive(Clone, Copy)] + pub(super) enum Lookup<'d> { + Invalid, + Outside, + Marker(HtmlElement<'d>), + } + + impl<'d> Resources<'d> { + pub(super) fn collect( + document_root: HtmlElement<'d>, + compiled_svg: HtmlElement<'d>, + ) -> Self { + let mut by_fragment = HashMap::new(); + let mut stack = vec![document_root]; + while let Some(element) = stack.pop() { + let mut children = Vec::new(); + let mut child = element.first_element_child(); + while let Some(next) = child { + children.push(next); + child = next.next_element_sibling(); + } + stack.extend(children.into_iter().rev()); + + if has_use_ancestor(element) { + continue; + } + let Some(id) = element_id(element) else { + continue; + }; + by_fragment.entry(id).or_insert_with(|| { + if element.local_name_string() == "marker" { + Resource::Marker { + element, + inside_compiled_svg: is_inside(element, compiled_svg), + } + } else { + Resource::Other + } + }); + } + Self { by_fragment } + } + + pub(super) fn lookup(&self, fragment: &str) -> Lookup<'d> { + match self.by_fragment.get(fragment) { + None | Some(Resource::Other) => Lookup::Invalid, + Some(Resource::Marker { + inside_compiled_svg: false, + .. + }) => Lookup::Outside, + Some(Resource::Marker { element, .. }) => Lookup::Marker(*element), + } + } + + /// Whether any marker property selected by an authored vertex resolves + /// to an actual marker resource. + /// + /// Missing ids, wrong-type targets, and an unselected marker kind are + /// Chromium-equivalent to `none`: they must not force the client's + /// combined marker/effect compositing route. A selected marker remains + /// active even when its viewport or source later paints nothing. + pub(super) fn has_selected_resource( + &self, + references: &References, + positions: &[crate::svg_path::MarkerPosition], + ) -> Result { + let mut selected = false; + for position in positions { + let Some(fragment) = references.selected(position.kind) else { + continue; + }; + match self.lookup(fragment) { + Lookup::Invalid => {} + Lookup::Outside => { + return Err(CompileError::UnsupportedMarker(format!( + "url(#{fragment}) resolves outside the compiled SVG subtree" + ))); + } + Lookup::Marker(_) => selected = true, + } + } + Ok(selected) + } + } + + fn has_use_ancestor(element: HtmlElement<'_>) -> bool { + let mut current = element.traversal_parent(); + while let Some(parent) = current { + if parent.local_name_string() == "use" { + return true; + } + current = parent.traversal_parent(); + } + false + } + + fn is_inside(element: HtmlElement<'_>, ancestor: HtmlElement<'_>) -> bool { + let mut current = Some(element); + while let Some(node) = current { + if node.node_id() == ancestor.node_id() { + return true; + } + current = node.traversal_parent(); + } + false + } + + fn element_id(element: HtmlElement<'_>) -> Option { + if let DemoNodeData::Element(data) = &element.dom_node().data { + data.id_attr.as_ref().map(ToString::to_string) + } else { + None + } + } + + #[derive(Debug, Clone, Default, PartialEq, Eq)] + pub(super) struct References { + pub(super) start: Option, + pub(super) mid: Option, + pub(super) end: Option, + } + + impl References { + pub(super) fn is_empty(&self) -> bool { + self.start.is_none() && self.mid.is_none() && self.end.is_none() + } + + pub(super) fn selected(&self, kind: crate::svg_path::MarkerType) -> Option<&str> { + match kind { + crate::svg_path::MarkerType::Start => self.start.as_deref(), + crate::svg_path::MarkerType::Mid => self.mid.as_deref(), + crate::svg_path::MarkerType::End => self.end.as_deref(), + } + } + } + + pub(super) fn inherited_references( + element: HtmlElement<'_>, + ) -> Result { + Ok(References { + start: inherited_reference(element, "marker-start")?, + mid: inherited_reference(element, "marker-mid")?, + end: inherited_reference(element, "marker-end")?, + }) + } + + fn inherited_reference( + element: HtmlElement<'_>, + property: &str, + ) -> Result, CompileError> { + let mut current = Some(element); + while let Some(candidate) = current { + if let Some(raw) = get_attr(candidate, property) { + let mut input = ParserInput::new(&raw); + let mut parser = Parser::new(&mut input); + let parsed_url: Result<_, cssparser::ParseError<'_, ()>> = + parser.parse_entirely(|input| input.expect_url().map_err(Into::into)); + if let Ok(url) = parsed_url { + let url = url.to_string(); + if url.contains("/*") { + // A comment inside url() makes a bad URL token in + // Chromium; the invalid hint disappears and inherited + // marker state remains visible. + } else { + let base = ::url::Url::parse("about:blank").expect("fixed URL base"); + let resolved = base.join(&url).map_err(|_| { + CompileError::UnsupportedMarker(format!( + "{property} url({url}) is a relative external reference; this compiler owns no resource base or I/O" + )) + })?; + let Some(fragment) = + crate::svg_paint_server::same_document_fragment(&resolved) + else { + return Err(CompileError::UnsupportedMarker(format!( + "{property} url({resolved}) is external; this compiler owns no resource I/O" + ))); + }; + return Ok(Some(fragment.to_string())); + } + } else if let Some(ident) = entire_ident(&raw) { + match ident.to_ascii_lowercase().as_str() { + "none" | "initial" => return Ok(None), + // The property is inherited. These CSS-wide values + // expose the parent value at this direct hint seat. + "inherit" | "unset" | "revert" | "revert-layer" => {} + _ => {} + } + } else if first_function(&raw, "var") { + return Err(CompileError::UnsupportedMarker(format!( + "{property} presentation attribute uses var(), whose substitution is not represented at this Stylo pin" + ))); + } + // Every other malformed presentation hint is absent; keep + // walking because marker properties inherit. + } + current = candidate.traversal_parent(); + } + Ok(None) + } + + fn entire_ident(raw: &str) -> Option { + let mut input = ParserInput::new(raw); + let mut parser = Parser::new(&mut input); + let parsed: Result<_, cssparser::ParseError<'_, ()>> = + parser.parse_entirely(|input| input.expect_ident_cloned().map_err(Into::into)); + parsed.ok().map(|ident| ident.to_string()) + } + + fn first_function(raw: &str, expected: &str) -> bool { + let mut input = ParserInput::new(raw); + let mut parser = Parser::new(&mut input); + matches!( + parser.next().ok(), + Some(Token::Function(name)) if name.eq_ignore_ascii_case(expected) + ) + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(super) enum Units { + StrokeWidth, + UserSpaceOnUse, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub(super) enum Orient { + Angle(f32), + Auto, + AutoStartReverse, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub(super) enum ViewBox { + None, + Degenerate, + Mapped((f32, f32, f32, f32)), + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub(super) struct Viewport { + pub(super) width: f32, + pub(super) height: f32, + pub(super) ref_x: f32, + pub(super) ref_y: f32, + pub(super) units: Units, + pub(super) orient: Orient, + pub(super) view_box: ViewBox, + pub(super) preserve_aspect_ratio: PreserveAspectRatio, + } + + pub(super) fn viewport( + marker: HtmlElement<'_>, + root_bases: PercentBases, + has_author_css: bool, + ) -> Result { + patrol_marker_root(marker, has_author_css)?; + let units = match get_attr(marker, "markerUnits").as_deref() { + Some("userSpaceOnUse") => Units::UserSpaceOnUse, + Some("strokeWidth") | None => Units::StrokeWidth, + // Case-sensitive enum; an invalid value selects the initial. + _ => Units::StrokeWidth, + }; + let width = length(marker, "markerWidth", root_bases.width, 3.0)?; + let height = length(marker, "markerHeight", root_bases.height, 3.0)?; + let ref_x = length(marker, "refX", root_bases.width, 0.0)?; + let ref_y = length(marker, "refY", root_bases.height, 0.0)?; + let orient = orient(marker)?; + let view_box = view_box(marker); + let preserve_aspect_ratio = match get_attr(marker, "preserveAspectRatio") { + Some(raw) => parse_preserve_aspect_ratio(&raw).map_err(|error| { + CompileError::UnsupportedMarker(format!( + "marker preserveAspectRatio is not admitted: {error}" + )) + })?, + None => PreserveAspectRatio::default(), + }; + Ok(Viewport { + width, + height, + ref_x, + ref_y, + units, + orient, + view_box, + preserve_aspect_ratio, + }) + } + + fn patrol_marker_root( + marker: HtmlElement<'_>, + has_author_css: bool, + ) -> Result<(), CompileError> { + if has_author_css { + return Err(CompileError::UnsupportedMarker( + "a used marker in a document with author CSS needs resource-side selector attribution at the pinned marker cascade boundary" + .to_string(), + )); + } + if get_attr(marker, "overflow").is_some() + || get_attr(marker, "style") + .is_some_and(|style| css_declares_property(&style, "overflow")) + { + return Err(CompileError::UnsupportedMarker( + "authored overflow on is outside the default hidden viewport-clip profile" + .to_string(), + )); + } + if ["filter", "mask", "clip-path"].iter().any(|name| { + get_attr(marker, name).is_some() + || get_attr(marker, "style") + .is_some_and(|style| css_declares_property(&style, name)) + }) { + return Err(CompileError::UnsupportedMarker( + "an effect on the resource root needs marker-source effect composition" + .to_string(), + )); + } + if element_has_computed_transform(marker)? { + return Err(CompileError::UnsupportedMarker( + "transform on the resource root is outside the admitted viewport mapping" + .to_string(), + )); + } + let patrol = patrol_computed_style(marker, false)?; + if patrol.opacity != 1.0 { + return Err(CompileError::UnsupportedMarker( + "opacity on the resource root needs one source compositing scope" + .to_string(), + )); + } + if let DemoNodeData::Element(data) = &marker.dom_node().data + && let Some(attribute) = data.attrs.iter().find_map(|attribute| { + let local = attribute.name.local.as_ref(); + RENDERING_ATTRIBUTES_NOT_CONSUMED + .contains(&local) + .then(|| local.to_string()) + }) + { + return Err(CompileError::UnsupportedMarker(format!( + "resource-root rendering declaration {attribute} on can inherit into its source subtree, outside the M1 source profile" + ))); + } + if let Some(style) = get_attr(marker, "style") + && let Some(property) = unrepresented_property(&style) + { + return Err(CompileError::UnsupportedMarker(format!( + "resource-root rendering declaration {property} on can inherit into its source subtree, outside the M1 source profile" + ))); + } + Ok(()) + } + + fn length( + element: HtmlElement<'_>, + name: &str, + percentage_basis: f32, + initial: f32, + ) -> Result { + let Some(raw) = get_attr(element, name) else { + return Ok(initial); + }; + let mut input = ParserInput::new(&raw); + let mut parser = Parser::new(&mut input); + let first = parser.next().ok().cloned(); + if matches!( + first, + Some(Token::Function(ref function)) + if function.eq_ignore_ascii_case("var") || function.eq_ignore_ascii_case("calc") + ) { + let function = match first { + Some(Token::Function(function)) => function, + _ => unreachable!(), + }; + return Err(CompileError::UnsupportedMarker(format!( + "marker {name} uses {function}(), whose computed length is not represented by the direct resource decoder" + ))); + } + let value = if parser.is_exhausted() { + match first { + Some(Token::Number { value, .. }) => Some(value), + Some(Token::Percentage { unit_value, .. }) => Some(percentage_basis * unit_value), + Some(Token::Dimension { value, unit, .. }) if unit.eq_ignore_ascii_case("px") => { + Some(value) + } + Some(Token::Dimension { unit, .. }) => { + return Err(CompileError::UnsupportedMarker(format!( + "marker {name} uses {unit} units, whose basis is not admitted" + ))); + } + Some(Token::Ident(ident)) + if matches!( + ident.to_ascii_lowercase().as_str(), + "initial" | "inherit" | "unset" | "revert" | "revert-layer" + ) => + { + return Err(CompileError::UnsupportedMarker(format!( + "marker {name} uses the CSS-wide value {ident}, whose resource-side cascade is not represented at this Stylo pin" + ))); + } + // SVG 2 ref keywords are valid standard-track values, but + // Chromium 149 drops all of them to initial zero. Keep that + // measured browser behavior explicit rather than inventing + // marker-viewBox alignment. + Some(Token::Ident(ident)) + if matches!( + (name, ident.to_ascii_lowercase().as_str()), + ("refX", "left" | "center" | "right") + | ("refY", "top" | "center" | "bottom") + ) => + { + Some(initial) + } + _ => None, + } + } else { + None + }; + let Some(value) = value.filter(|value| value.is_finite()) else { + // Invalid marker resource hints select their initial values. + return Ok(initial); + }; + if !(WEB_USED_LENGTH_MIN..=WEB_USED_LENGTH_MAX).contains(&value) { + return Err(CompileError::UnsupportedMarker(format!( + "marker {name} exceeds the admitted Web used-value range" + ))); + } + Ok(value) + } + + fn orient(element: HtmlElement<'_>) -> Result { + let Some(raw) = get_attr(element, "orient") else { + return Ok(Orient::Angle(0.0)); + }; + // Blink's SVG-angle keyword route is case-sensitive and does not trim + // the authored token. Numeric angles take the CSS token route below, + // where surrounding whitespace is accepted. + if raw == "auto" { + return Ok(Orient::Auto); + } + if raw == "auto-start-reverse" { + return Ok(Orient::AutoStartReverse); + } + if let Some(ident) = entire_ident(&raw) { + return Ok(match ident.to_ascii_lowercase().as_str() { + "initial" | "inherit" | "unset" | "revert" | "revert-layer" => { + return Err(CompileError::UnsupportedMarker(format!( + "marker orient uses the CSS-wide value {ident}, whose resource-side cascade is not represented at this Stylo pin" + ))); + } + _ => Orient::Angle(0.0), + }); + } + let mut input = ParserInput::new(&raw); + let mut parser = Parser::new(&mut input); + let first = parser.next().ok().cloned(); + if matches!( + first, + Some(Token::Function(ref function)) + if function.eq_ignore_ascii_case("var") || function.eq_ignore_ascii_case("calc") + ) { + return Err(CompileError::UnsupportedMarker( + "marker orient uses CSS math or var(), whose resource-side computed angle is not represented" + .to_string(), + )); + } + let degrees = if parser.is_exhausted() { + match first { + Some(Token::Number { value, .. }) => Some(value), + Some(Token::Dimension { value, unit, .. }) if unit.eq_ignore_ascii_case("deg") => { + Some(value) + } + Some(Token::Dimension { value, unit, .. }) if unit.eq_ignore_ascii_case("rad") => { + Some(value.to_degrees()) + } + Some(Token::Dimension { value, unit, .. }) if unit.eq_ignore_ascii_case("grad") => { + Some(value * 0.9) + } + Some(Token::Dimension { value, unit, .. }) if unit.eq_ignore_ascii_case("turn") => { + Some(value * 360.0) + } + _ => None, + } + } else { + None + }; + Ok(Orient::Angle( + degrees.filter(|value| value.is_finite()).unwrap_or(0.0), + )) + } + + fn view_box(element: HtmlElement<'_>) -> ViewBox { + let Some(raw) = get_attr(element, "viewBox") else { + return ViewBox::None; + }; + let Some(values) = crate::svg_number_list::parse(&raw) else { + return ViewBox::None; + }; + let [x, y, width, height] = values.as_slice() else { + return ViewBox::None; + }; + if ![*x, *y, *width, *height].into_iter().all(f32::is_finite) { + return ViewBox::None; + } + if *width == 0.0 || *height == 0.0 { + ViewBox::Degenerate + } else if *width < 0.0 || *height < 0.0 { + ViewBox::None + } else { + ViewBox::Mapped((*x, *y, *width, *height)) + } + } + + pub(super) fn instance_angle(orient: Orient, position: crate::svg_path::MarkerPosition) -> f32 { + match orient { + Orient::Angle(angle) => angle, + Orient::Auto => position.angle, + Orient::AutoStartReverse if position.kind == crate::svg_path::MarkerType::Start => { + position.angle + 180.0 + } + Orient::AutoStartReverse => position.angle, + } + } + + /// Blink's marker transform rotates through its double affine helper even + /// though the stored marker angle is float. The matrix that reaches Skia + /// is therefore double-trig rounded to f32, not f32 radians/trig as on the + /// ordinary computed-transform route. + fn rotation_components(degrees: f32) -> (f32, f32) { + const EXACT_QUARTER_TURN_LIMIT: f32 = 360.0 * 1024.0; + if degrees.abs() <= EXACT_QUARTER_TURN_LIMIT && degrees % 90.0 == 0.0 { + return match (degrees / 90.0).rem_euclid(4.0) as i32 { + 0 => (0.0, 1.0), + 1 => (1.0, 0.0), + 2 => (0.0, -1.0), + _ => (-1.0, 0.0), + }; + } + let (sin, cos) = f64::from(degrees).to_radians().sin_cos(); + (sin as f32, cos as f32) + } + + /// Build Blink's translate/rotate/scale/reference sequence in its one + /// double affine before rounding the six matrix components for Skia. + pub(super) fn placement( + position: (f32, f32), + degrees: f32, + scale: f32, + mapped_reference: (f32, f32), + ) -> AffineTransform { + let (sin_f32, cos_f32) = rotation_components(degrees); + let sin = f64::from(sin_f32); + let cos = f64::from(cos_f32); + let scale = f64::from(scale); + let a = cos * scale; + let c = -sin * scale; + let b = sin * scale; + let d = cos * scale; + let ref_x = f64::from(mapped_reference.0); + let ref_y = f64::from(mapped_reference.1); + let e = f64::from(position.0) - a * ref_x - c * ref_y; + let f = f64::from(position.1) - b * ref_x - d * ref_y; + AffineTransform::from_acebdf(a as f32, c as f32, e as f32, b as f32, d as f32, f as f32) + } + + pub(super) fn patrol_source_tree(marker: HtmlElement<'_>) -> Result<(), CompileError> { + let mut stack = Vec::new(); + let mut child = marker.first_element_child(); + while let Some(next) = child { + stack.push(next); + child = next.next_element_sibling(); + } + while let Some(element) = stack.pop() { + let tag = element.local_name_string(); + if is_animation_element(&tag) { + return Err(CompileError::UnsupportedMarker( + "animation inside a used marker source needs the marker sampling inventory" + .to_string(), + )); + } + if matches!( + tag.as_str(), + "use" + | "text" + | "image" + | "foreignObject" + | "filter" + | "mask" + | "clipPath" + | "linearGradient" + | "radialGradient" + | "pattern" + ) { + return Err(CompileError::UnsupportedMarker(format!( + "a <{tag}> inside a used marker source is outside the admitted static solid subtree" + ))); + } + let mut child = element.first_element_child(); + while let Some(next) = child { + stack.push(next); + child = next.next_element_sibling(); + } + } + Ok(()) + } + + pub(super) fn patrol_source_items(items: &[FrameItem]) -> Result<(), CompileError> { + if items.len() > MAX_SOURCE_ITEMS { + return Err(CompileError::UnsupportedMarker(format!( + "one marker source emits {} frame items; the admitted limit is {MAX_SOURCE_ITEMS}", + items.len() + ))); + } + for item in items { + let FrameItem::Node(node) = item else { + return Err(CompileError::UnsupportedMarker( + "a marker source needs an opacity, clip, mask, or filter scope outside the admitted simple subtree" + .to_string(), + )); + }; + if node.paints.pattern().is_some() + || node + .paints + .iter() + .any(|paint| !matches!(paint, cg::Paint::Solid(_))) + || node.stroke.as_ref().is_some_and(|stroke| { + stroke.paints().pattern().is_some() + || stroke + .paints() + .iter() + .any(|paint| !matches!(paint, cg::Paint::Solid(_))) + }) + { + return Err(CompileError::UnsupportedMarker( + "a marker source resolves a paint server outside the admitted solid/context-solid profile" + .to_string(), + )); + } + } + Ok(()) + } +} + /// The recursive descent that materializes shapes in painter order. /// /// Containers are **flattened** wherever flattening is exact: a `` @@ -3408,6 +4094,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>, + /// Same-document SVG marker resources. Marker instances flatten to + /// ordinary items; this table and every authored vertex stay producer-side. + markers: &'a marker_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>, @@ -3445,6 +4134,9 @@ struct ChildWalk<'a> { /// Pattern resources whose source programs are currently compiling. /// Nested pattern paints may recurse, but a source cycle never can. active_patterns: Vec, + /// Used marker resources currently compiling. M1 admits no nested marker + /// source; the stack names that boundary and prevents cycles. + active_markers: Vec, next_id: u64, } @@ -3624,10 +4316,9 @@ impl<'a> ChildWalk<'a> { Ok(facts) } - /// Apply an already-resolved element opacity outside a completed masked - /// span. A mask is itself a compositing scope, so Chromium never takes the - /// unmasked one-draw fold for this branch; its opaque-luminance ordering - /// probe differs by one code value when opacity is placed inside instead. + /// Apply an already-resolved element opacity outside a completed effect or + /// marker span. Those spans contain multiple draws/scopes, so Chromium + /// never takes the ordinary one-draw alpha fold for this branch. fn wrap_masked_span_with_opacity( &mut self, checkpoint: usize, @@ -3773,6 +4464,13 @@ impl<'a> ChildWalk<'a> { child = c.next_element_sibling(); continue; } + // `` paints only through an applicable client's resolved + // marker reference. Its subtree is compiled transactionally for + // each selected vertex, never in authored resource position. + if tag == "marker" { + 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 @@ -4053,30 +4751,299 @@ impl<'a> ChildWalk<'a> { if let Some(filter) = filter { facts = self.wrap_span_with_filter(checkpoint, facts, filter); } - facts = self.wrap_span_with_mask(checkpoint, facts, mask, path, depth)?; - Ok(self.wrap_masked_span_with_opacity(checkpoint, facts, patrol.opacity)) - }) - } else { - self.compile_span_with_opacity( - el, - transform, + facts = self.wrap_span_with_mask(checkpoint, facts, mask, path, depth)?; + Ok(self.wrap_masked_span_with_opacity(checkpoint, facts, patrol.opacity)) + }) + } else { + self.compile_span_with_opacity( + el, + transform, + path, + depth, + patrol.opacity, + replay_opacity, + ) + }; + self.context_paint_transform = previous_context_paint_transform; + self.paint_contexts.pop(); + let facts = self.wrap_span_with_clip(checkpoint, facts?, clip); + // The `x`/`y` translate is part of the use's own transform (SVG2 + // §5.6.2), so like the transform property it stays *on* this + // element — an enclosing one-draw route is broken only by a transform + // strictly below it, and this one is not below. + Ok(SpanFacts { + transformed: facts.transformed || own_transformed || x != 0.0 || y != 0.0, + ..facts + }) + } + + #[allow(clippy::too_many_arguments)] + fn compile_marker_instances( + &mut self, + client: HtmlElement<'a>, + client_to_frame: AffineTransform, + client_context_to_frame: AffineTransform, + client_box: Option, + references: &marker_resource::References, + positions: &[crate::svg_path::MarkerPosition], + path: &str, + depth: usize, + ) -> Result { + let checkpoint = self.items.len(); + let next_id = self.next_id; + let result = self.compile_marker_instances_inner( + client, + client_to_frame, + client_context_to_frame, + client_box, + references, + positions, + path, + depth, + ); + if result.is_err() { + self.items.truncate(checkpoint); + self.next_id = next_id; + } + result + } + + #[allow(clippy::too_many_arguments)] + fn compile_marker_instances_inner( + &mut self, + client: HtmlElement<'a>, + client_to_frame: AffineTransform, + client_context_to_frame: AffineTransform, + client_box: Option, + references: &marker_resource::References, + positions: &[crate::svg_path::MarkerPosition], + path: &str, + depth: usize, + ) -> Result { + if positions.len() > marker_resource::MAX_POSITIONS_PER_CLIENT { + return Err(CompileError::UnsupportedMarker(format!( + "one marker client has {} authored positions; the admitted limit is {}", + positions.len(), + marker_resource::MAX_POSITIONS_PER_CLIENT + ))); + } + let mut facts = SpanFacts::default(); + let mut emitted_items = 0usize; + for (index, position) in positions.iter().copied().enumerate() { + let Some(fragment) = references.selected(position.kind) else { + continue; + }; + let marker = match self.markers.lookup(fragment) { + marker_resource::Lookup::Invalid => continue, + marker_resource::Lookup::Outside => { + return Err(CompileError::UnsupportedMarker(format!( + "url(#{fragment}) resolves outside the compiled SVG subtree" + ))); + } + marker_resource::Lookup::Marker(marker) => marker, + }; + if !self.active_markers.is_empty() { + return Err(CompileError::UnsupportedMarker(format!( + "nested marker source reaches url(#{fragment}); bounded nested marker composition is a later rung" + ))); + } + let viewport = marker_resource::viewport(marker, self.bases, self.has_author_css)?; + if !(viewport.width > 0.0 && viewport.height > 0.0) + || viewport.view_box == marker_resource::ViewBox::Degenerate + { + continue; + } + let marker_scale = match viewport.units { + marker_resource::Units::UserSpaceOnUse => 1.0, + marker_resource::Units::StrokeWidth => { + resolve_stroke_width(client, &client.local_name_string(), self.bases)? + } + }; + if marker_scale == 0.0 { + continue; + } + let (content_mapping, source_bases) = match viewport.view_box { + marker_resource::ViewBox::Mapped(view_box) => ( + viewbox_to_viewport_transform( + (viewport.width, viewport.height), + view_box, + viewport.preserve_aspect_ratio, + ), + PercentBases { + width: view_box.2, + height: view_box.3, + }, + ), + marker_resource::ViewBox::None => ( + AffineTransform::identity(), + PercentBases { + width: viewport.width, + height: viewport.height, + }, + ), + marker_resource::ViewBox::Degenerate => unreachable!("suppressed above"), + }; + let mapped_reference = map_point(content_mapping, (viewport.ref_x, viewport.ref_y)); + let placement = marker_resource::placement( + position.origin, + marker_resource::instance_angle(viewport.orient, position), + marker_scale, + mapped_reference, + ); + let marker_to_frame = client_to_frame.compose(&placement); + let content_to_frame = marker_to_frame.compose(&content_mapping); + if !content_to_frame + .matrix + .iter() + .flatten() + .all(|value| value.is_finite()) + { + return Err(CompileError::UnsupportedMarker(format!( + "marker instance {} for url(#{fragment}) has a non-finite placement transform", + index + 1 + ))); + } + let mut source_items = self.compile_marker_source( + client, + client_context_to_frame, + client_box, + marker, + fragment, + content_to_frame, + source_bases, path, depth, - patrol.opacity, - replay_opacity, + )?; + if source_items.is_empty() { + continue; + } + let clip_geometry = ClipGeometry::new( + marker_to_frame, + Geometry::Rect(Rectangle::from_xywh( + 0.0, + 0.0, + viewport.width, + viewport.height, + )), ) + .map_err(|error| { + CompileError::UnsupportedMarker(format!( + "marker viewport clip cannot enter the resolved frame: {error}" + )) + })?; + let clip_layer = ClipLayer::new(Arc::<[ClipGeometry]>::from([clip_geometry])) + .map_err(|error| CompileError::UnsupportedMarker(error.to_string()))?; + let clip = ClipPath::new_with_edge_mode( + Arc::<[ClipLayer]>::from([clip_layer]), + rframe::ClipEdgeMode::Hard, + ) + .map_err(|error| CompileError::UnsupportedMarker(error.to_string()))?; + source_items.insert(0, clip_scope_item(&mut self.next_id, clip)); + source_items.push(FrameItem::ScopeEnd); + emitted_items += source_items.len(); + if emitted_items > marker_resource::MAX_ITEMS_PER_CLIENT { + return Err(CompileError::UnsupportedMarker(format!( + "one marker client emits more than {} resolved items", + marker_resource::MAX_ITEMS_PER_CLIENT + ))); + } + self.items.extend(source_items); + facts.has_scope = true; + facts.transformed = true; + } + Ok(facts) + } + + #[allow(clippy::too_many_arguments)] + fn compile_marker_source( + &mut self, + client: HtmlElement<'a>, + client_context_to_frame: AffineTransform, + client_box: Option, + marker: HtmlElement<'a>, + fragment: &str, + content_to_frame: AffineTransform, + source_bases: PercentBases, + target_path: &str, + depth: usize, + ) -> Result, CompileError> { + marker_resource::patrol_source_tree(marker)?; + let GeometryMeasurements { + use_boxes, + effect_boxes, + } = measure_geometry( + marker, + self.values, + source_bases, + self.fonts, + self.override_skips, + ) + .map_err(|error| { + CompileError::UnsupportedMarker(format!( + "marker #{fragment} source geometry cannot be measured: {error}" + )) + })?; + let mut degradations = Vec::new(); + let mut active_markers = self.active_markers.clone(); + active_markers.push(marker.node_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, + markers: self.markers, + patterns: self.patterns, + use_boxes, + effect_boxes, + paint_contexts: vec![PaintContext { + element: client, + reference_box: client_box, + to_frame: client_context_to_frame, + }], + context_paint_transform: content_to_frame, + fonts: self.fonts, + items: Vec::new(), + top_level_shapes: Vec::new(), + active_masks: self.active_masks.clone(), + active_patterns: self.active_patterns.clone(), + active_markers, + next_id: self.next_id, }; - self.context_paint_transform = previous_context_paint_transform; - self.paint_contexts.pop(); - let facts = self.wrap_span_with_clip(checkpoint, facts?, clip); - // The `x`/`y` translate is part of the use's own transform (SVG2 - // §5.6.2), so like the transform property it stays *on* this - // element — an enclosing one-draw route is broken only by a transform - // strictly below it, and this one is not below. - Ok(SpanFacts { - transformed: facts.transformed || own_transformed || x != 0.0 || y != 0.0, - ..facts - }) + walk.compile_children( + marker, + content_to_frame, + &format!("{target_path}/marker-source(#{fragment})"), + depth + 1, + 1.0, + ) + .map_err(|error| { + CompileError::UnsupportedMarker(format!( + "marker #{fragment} source cannot compile completely: {error}" + )) + })?; + let items = std::mem::take(&mut walk.items); + let next_id = walk.next_id; + drop(walk); + if let Some(degradation) = degradations.first() { + return Err(CompileError::UnsupportedMarker(format!( + "marker #{fragment} source cannot compile completely: {}", + degradation.reason() + ))); + } + marker_resource::patrol_source_items(&items)?; + FrameItems::try_new(items.clone()).map_err(|error| { + CompileError::UnsupportedMarker(format!( + "marker #{fragment} source item stream is invalid: {error}" + )) + })?; + self.next_id = next_id; + Ok(items) } fn compile_leaf( @@ -4092,9 +5059,19 @@ impl<'a> ChildWalk<'a> { // whose `d` draws nothing. That is not a hole: the element is // admitted, it is simply not a node. let checkpoint = self.items.len(); + let next_id_checkpoint = self.next_id; let mut facts = SpanFacts::default(); let tag = el.local_name_string(); + let marker_references = if matches!(tag.as_str(), "line" | "path" | "polyline" | "polygon") + { + marker_resource::inherited_references(el)? + } else { + marker_resource::References::default() + }; + let marker_syntax_present = !marker_references.is_empty(); let target_to_frame = compose_element_transform(el, transform, &tag, self.bases)?; + let client_context_to_frame = + compose_element_transform(el, self.context_paint_transform, &tag, self.bases)?; let target_box = self .effect_boxes .get(&el.node_id()) @@ -4111,12 +5088,22 @@ impl<'a> ChildWalk<'a> { filter_resource::Resolution::Apply(filter) => Some(filter), }; let mask = self.resolve_mask(el, target_to_frame, target_box)?; - let deferred_opacity = if mask.is_some() || filter.is_some() { + let marker_projection = + prepare_marker_projection(el, &tag, self.values, self.bases, marker_syntax_present)?; + let marker_selected = self + .markers + .has_selected_resource(&marker_references, &marker_projection.positions)?; + let marker_client_clip = if marker_selected { + self.resolve_clip(el, target_to_frame, target_box)? + } else { + None + }; + let deferred_opacity = if mask.is_some() || filter.is_some() || marker_selected { patrol_computed_style(el, tag == "rect")?.opacity } else { 1.0 }; - if let Some(outcome) = compile_shape( + let compilation = compile_shape( el, transform, self.context_paint_transform, @@ -4127,15 +5114,23 @@ impl<'a> ChildWalk<'a> { &self.active_patterns, &self.paint_contexts, self.bases, - mask.is_some() || filter.is_some(), + mask.is_some() || filter.is_some() || marker_selected, replay_opacity, self.fonts, - )? { - let clip = self.resolve_clip( + marker_projection, + )?; + let clip = if marker_selected { + marker_client_clip + } else if let Some(outcome) = compilation.outcome.as_ref() { + self.resolve_clip( el, outcome.node.transform, Some(outcome.node.geometry.local_box()), - )?; + )? + } else { + None + }; + if let Some(outcome) = compilation.outcome { facts.one_draw_opacity = outcome.one_draw_opacity; facts.transformed = outcome.transformed; match outcome.scope_opacity { @@ -4154,13 +5149,34 @@ impl<'a> ChildWalk<'a> { self.items.push(FrameItem::Node(outcome.node)); } } - if let Some(filter) = filter { - facts = self.wrap_span_with_filter(checkpoint, facts, filter); + } + let marker_facts = match self.compile_marker_instances( + el, + target_to_frame, + client_context_to_frame, + target_box, + &marker_references, + &compilation.marker_positions, + path, + depth, + ) { + Ok(facts) => facts, + Err(error) => { + // Marker source compilation is one client transaction. In + // best effort the parent will declare and skip this element; + // no already-emitted fill or stroke may survive that skip. + self.items.truncate(checkpoint); + self.next_id = next_id_checkpoint; + return Err(error); } - facts = self.wrap_span_with_mask(checkpoint, facts, mask, path, depth)?; - facts = self.wrap_masked_span_with_opacity(checkpoint, facts, deferred_opacity); - facts = self.wrap_span_with_clip(checkpoint, facts, clip); + }; + facts.absorb(marker_facts); + if let Some(filter) = filter { + facts = self.wrap_span_with_filter(checkpoint, facts, filter); } + facts = self.wrap_span_with_mask(checkpoint, facts, mask, path, depth)?; + facts = self.wrap_masked_span_with_opacity(checkpoint, facts, deferred_opacity); + facts = self.wrap_span_with_clip(checkpoint, facts, clip); if top_level { self.top_level_shapes.push(el.node_id()); } @@ -4190,6 +5206,17 @@ fn clip_scope_item(next_id: &mut u64, clip: ClipPath) -> FrameItem { }) } +fn map_point(transform: AffineTransform, point: (f32, f32)) -> (f32, f32) { + ( + transform.matrix[0][0] * point.0 + + transform.matrix[0][1] * point.1 + + transform.matrix[0][2], + transform.matrix[1][0] * point.0 + + transform.matrix[1][1] * point.1 + + transform.matrix[1][2], + ) +} + fn filter_scope_item(next_id: &mut u64, filter: Filter) -> FrameItem { let scope_id = *next_id + 1; *next_id += 1; @@ -8372,6 +9399,81 @@ fn compose_element_transform( Ok(composed) } +/// Marker facts prepared before shape compilation so the client can choose +/// the same opacity/effect route Chromium does without rescanning path data. +#[derive(Debug, Default)] +struct MarkerProjection { + parsed_path: Option, + positions: Vec, +} + +fn prepare_marker_projection( + el: HtmlElement<'_>, + tag: &str, + values: &EffectiveValues, + bases: PercentBases, + requested: bool, +) -> Result { + if !requested { + return Ok(MarkerProjection::default()); + } + let patrol = patrol_computed_style(el, false)?; + let projects = patrol.disposition == RenderDisposition::Renders && patrol.opacity != 0.0; + match tag { + "path" => { + let parsed_path = match get_attr(el, "d") { + None => crate::svg_path::ParsedPath::default(), + Some(value) => crate::svg_path::parse_path(&value), + }; + let positions = if projects { + parsed_path + .marker_positions() + .map_err(|reason| CompileError::UnsupportedMarker(reason.to_string()))? + } else { + Vec::new() + }; + Ok(MarkerProjection { + parsed_path: Some(parsed_path), + positions, + }) + } + "line" if projects => { + let start = ( + geometry_attr_f32(el, "x1", values, bases)?.unwrap_or(0.0), + geometry_attr_f32(el, "y1", values, bases)?.unwrap_or(0.0), + ); + let end = ( + geometry_attr_f32(el, "x2", values, bases)?.unwrap_or(0.0), + geometry_attr_f32(el, "y2", values, bases)?.unwrap_or(0.0), + ); + Ok(MarkerProjection { + parsed_path: None, + positions: crate::svg_path::line_marker_positions(start, end), + }) + } + "polygon" | "polyline" if projects => { + let points = match get_attr(el, "points") { + None => Vec::new(), + Some(value) => crate::svg_path::parse_points(&value).map_err( + |crate::svg_path::SourceSyntaxError::Syntax { offset }| { + CompileError::BadPoints { + element: tag.to_string(), + offset, + excerpt: excerpt_at(&value, offset), + } + }, + )?, + }; + Ok(MarkerProjection { + parsed_path: None, + positions: crate::svg_path::points_marker_positions(&points, tag == "polygon"), + }) + } + "line" | "polygon" | "polyline" => Ok(MarkerProjection::default()), + _ => unreachable!("marker projection is prepared only for applicable shapes"), + } +} + /// Compile a single shape element into a resolved node. /// /// Local names match exactly: SVG element names are case-sensitive, and each @@ -8392,8 +9494,13 @@ fn compile_shape( defer_own_opacity: bool, replay_opacity: f32, fonts: &textlayout::Environment, -) -> Result, CompileError> { + marker_projection: MarkerProjection, +) -> Result { let tag = el.local_name_string(); + let MarkerProjection { + mut parsed_path, + positions: marker_positions, + } = marker_projection; let transformed = element_has_computed_transform(el)?; // A shape's own `transform` composes inside the mapping it inherits // from the viewport and its ancestor containers, exactly as a @@ -8444,19 +9551,28 @@ fn compile_shape( defer_own_opacity, replay_opacity, ), - "path" => compile_path( - el, - transform, - context_paint_transform, - next_id, - servers, - patterns, - active_patterns, - paint_contexts, - bases, - defer_own_opacity, - replay_opacity, - ), + "path" => { + let parsed = parsed_path + .take() + .unwrap_or_else(|| match get_attr(el, "d") { + None => crate::svg_path::ParsedPath::default(), + Some(value) => crate::svg_path::parse_path(&value), + }); + compile_path( + el, + transform, + context_paint_transform, + next_id, + parsed.commands, + servers, + patterns, + active_patterns, + paint_contexts, + bases, + defer_own_opacity, + replay_opacity, + ) + } "text" => compile_text( el, transform, @@ -8516,10 +9632,14 @@ fn compile_shape( ), other => Err(CompileError::UnsupportedElement(other.to_string())), }?; - Ok(outcome.map(|outcome| ShapeOutcome { + let outcome = outcome.map(|outcome| ShapeOutcome { transformed, ..outcome - })) + }); + Ok(ShapeCompilation { + outcome, + marker_positions, + }) } /// Compile one `` element: the document's run, resolved once by the @@ -8900,6 +10020,7 @@ fn compile_path( viewport: AffineTransform, context_paint_transform: AffineTransform, next_id: &mut u64, + commands: Vec, servers: &PaintServers<'_>, patterns: &PatternCompiler<'_>, active_patterns: &[NodeId], @@ -8922,10 +10043,6 @@ fn compile_path( if patrol.opacity == 0.0 { return Ok(None); } - let commands = match get_attr(el, "d") { - None => Vec::new(), - Some(value) => crate::svg_path::parse_path_data(&value), - }; if commands.is_empty() { return Ok(None); } @@ -9239,6 +10356,11 @@ enum Strokable { /// One compiled shape plus the facts the group-scope rung's walk decides /// with: how many paint passes it draws, whether its own opacity needs an /// isolated layer, and whether one-draw opacity or a transform sits on it. +struct ShapeCompilation { + outcome: Option, + marker_positions: Vec, +} + struct ShapeOutcome { node: FrameNode, /// Paint passes the node draws (fill + stroke). @@ -10556,6 +11678,42 @@ mod percentage_resolution_tests { /// A negative `stroke-width` never arrives: it fails the property's /// non-negative grammar, so the cascade drops the declaration and this read /// sees the inherited or initial value — exactly what Chromium paints. +fn resolve_stroke_width( + el: HtmlElement<'_>, + element_name: &str, + bases: PercentBases, +) -> Result { + let destination_data = el.borrow_data().ok_or(CompileError::MissingComputedStyle)?; + let destination_style: &ComputedValues = destination_data.styles.primary(); + match destination_style.clone_stroke_width() { + SVGLength::ContextValue => Err(CompileError::UnsupportedStroke( + "stroke-width: context-value".to_string(), + )), + SVGLength::LengthPercentage(width) => match width.0.to_length() { + Some(length) => { + patrol_stroke_width_units(el, element_name)?; + Ok(clamp_web_used_length(length)) + } + None => match width.0.to_percentage() { + Some(percentage) => { + patrol_stroke_width_percentage_provenance(el)?; + resolve_web_percentage_length(percentage.0, bases.diagonal()).map_err( + |PercentageResolutionError::PrecisionAlias| { + CompileError::UnsupportedStroke( + STROKE_WIDTH_PERCENTAGE_PRECISION_ALIAS.to_string(), + ) + }, + ) + } + None => Err(CompileError::UnsupportedStroke( + "a calc() stroke-width mixing lengths and percentages is not consumed" + .to_string(), + )), + }, + }, + } +} + fn resolve_stroke( el: HtmlElement<'_>, element_name: &str, @@ -10642,57 +11800,16 @@ fn resolve_stroke( return Ok(None); } - // A percentage `stroke-width` resolves against the viewport's normalized - // diagonal (measured: `10%` on a 64x64 viewport paints 6.4 units wide) — - // the same basis chain the shape geometry percentages refuse on. - let destination_data = el.borrow_data().ok_or(CompileError::MissingComputedStyle)?; - let destination_style: &ComputedValues = destination_data.styles.primary(); - let width = match destination_style.clone_stroke_width() { - SVGLength::ContextValue => { - return Err(CompileError::UnsupportedStroke( - "stroke-width: context-value".to_string(), - )); - } - SVGLength::LengthPercentage(width) => match width.0.to_length() { - Some(length) => { - // The unit is gone from a computed length, so the authored text - // is what says whether its basis was one this build has. - patrol_stroke_width_units(el, element_name)?; - clamp_web_used_length(length) - } - // A pure percentage resolves against the viewport's normalized - // diagonal (SVG2 §7.10; measured — `10%` of 64x64 paints 6.4 - // units). A calc() mixing lengths and percentages has neither a - // computed length nor a pure percentage and stays refused. - None => match width.0.to_percentage() { - Some(percentage) => { - // Stylo's fraction no longer says whether the source was - // one recoverable literal or arithmetic/raw precision - // whose Chromium result differs. Refuse that provenance - // loss before the typed preimage check below. - patrol_stroke_width_percentage_provenance(el)?; - match resolve_web_percentage_length(percentage.0, bases.diagonal()) { - Ok(width) => width, - Err(PercentageResolutionError::PrecisionAlias) => { - return Err(CompileError::UnsupportedStroke( - STROKE_WIDTH_PERCENTAGE_PRECISION_ALIAS.to_string(), - )); - } - } - } - None => { - return Err(CompileError::UnsupportedStroke( - "a calc() stroke-width mixing lengths and percentages is not consumed" - .to_string(), - )); - } - }, - }, - }; + // Marker placement also consumes this computed width even when no stroke + // paint exists, so the numeric/cascade route lives in one helper. + let width = resolve_stroke_width(el, element_name, bases)?; if width == 0.0 { return Ok(None); } + let destination_data = el.borrow_data().ok_or(CompileError::MissingComputedStyle)?; + let destination_style: &ComputedValues = destination_data.styles.primary(); + // Computed values have already lost the authored unit and substitution // provenance. Patrol every ingress before looking at the typed list: a // basis-less unit or var() can compute to the empty initial value in this diff --git a/crates/websem/src/svg_path.rs b/crates/websem/src/svg_path.rs index d2afed41..ff6f804c 100644 --- a/crates/websem/src/svg_path.rs +++ b/crates/websem/src/svg_path.rs @@ -64,6 +64,61 @@ use crate::svg_number::{self, ExponentParts, NumberParts}; use rframe::PathCommand; +/// The producer-side result of one authored `d` scan. +/// +/// `commands` is the canonical raster path that may cross into `rframe`. +/// `marker_elements` retains the authored vertex topology Blink uses for +/// marker placement: move-only contours survive, quadratics are normalized to +/// one cubic, and one authored arc contributes at most one synthetic cubic. +/// Neither this type nor the marker projection crosses the resolved contract. +#[derive(Debug, Default)] +pub(crate) struct ParsedPath { + pub(crate) commands: Vec, + marker_elements: Vec, + /// Derived non-finite ordinary geometry made the raster path invalid. + /// Marker painting for that extreme class is not admitted by inference. + marker_projection_poisoned: bool, +} + +impl ParsedPath { + pub(crate) fn marker_positions(&self) -> Result, &'static str> { + if self.marker_projection_poisoned { + return Err( + "marker geometry crosses a derived non-finite path boundary whose vertex projection is not admitted", + ); + } + Ok(build_marker_positions(&self.marker_elements)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum MarkerType { + Start, + Mid, + End, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct MarkerPosition { + pub(crate) kind: MarkerType, + pub(crate) origin: (f32, f32), + pub(crate) angle: f32, +} + +/// Authored marker topology after Blink's path normalizer, but before marker +/// positions and tangent angles are built. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum MarkerPathElement { + MoveTo((f32, f32)), + LineTo((f32, f32)), + CubicTo { + one: (f32, f32), + two: (f32, f32), + end: (f32, f32), + }, + Close, +} + /// Why one SVG numeric grammar stopped parsing. /// /// Path data itself consumes its valid prefix when this occurs. `points` @@ -82,6 +137,11 @@ pub(crate) enum SourceSyntaxError { /// SVG's `d` initial value is `none`, which also has an empty valid prefix. /// Any syntax error ends parsing after the last fully defined segment. pub(crate) fn parse_path_data(d: &str) -> Vec { + parse_path(d).commands +} + +/// Parse both producer projections from one token scan. +pub(crate) fn parse_path(d: &str) -> ParsedPath { Parser::new(d).parse_path_prefix() } @@ -119,6 +179,7 @@ struct Parser<'a> { bytes: &'a [u8], at: usize, commands: Vec, + marker_elements: Vec, current: (f32, f32), subpath_start: (f32, f32), /// The open contour's `MoveTo` index and whether it has drawn yet. @@ -141,6 +202,7 @@ impl<'a> Parser<'a> { bytes: d.as_bytes(), at: 0, commands: Vec::new(), + marker_elements: Vec::new(), current: (0.0, 0.0), subpath_start: (0.0, 0.0), open: None, @@ -154,10 +216,10 @@ impl<'a> Parser<'a> { Err(SourceSyntaxError::Syntax { offset: self.at }) } - fn parse_path_prefix(mut self) -> Vec { + fn parse_path_prefix(mut self) -> ParsedPath { self.skip_wsp(); if self.at >= self.bytes.len() { - return Vec::new(); + return ParsedPath::default(); } let mut first = true; while self.at < self.bytes.len() { @@ -180,16 +242,24 @@ impl<'a> Parser<'a> { self.finish() } - fn finish(mut self) -> Vec { + fn finish(mut self) -> ParsedPath { if self.poisoned { - return Vec::new(); + return ParsedPath { + commands: Vec::new(), + marker_elements: Vec::new(), + marker_projection_poisoned: true, + }; } // A trailing contour that only moved contributes nothing. if let Some((index, false)) = self.open { debug_assert_eq!(index + 1, self.commands.len()); self.commands.truncate(index); } - self.commands + ParsedPath { + commands: self.commands, + marker_elements: self.marker_elements, + marker_projection_poisoned: false, + } } /// One command letter and its complete argument list, including every @@ -326,11 +396,13 @@ impl<'a> Parser<'a> { if self.poisoned || !finite_point(point) { self.poisoned |= !finite_point(point); self.commands.clear(); + self.marker_elements.clear(); self.open = None; self.current = point; self.subpath_start = point; return; } + self.marker_elements.push(MarkerPathElement::MoveTo(point)); // The contour this move replaces drew nothing, so it is not a visual // fact; its move is the last command emitted. if let Some((index, false)) = self.open { @@ -372,10 +444,16 @@ impl<'a> Parser<'a> { if self.poisoned || !finite_point(point) { self.poisoned |= !finite_point(point); self.commands.clear(); + self.marker_elements.clear(); self.open = None; self.current = point; return; } + self.marker_elements.push(MarkerPathElement::LineTo(point)); + self.emit_raster_line(point); + } + + fn emit_raster_line(&mut self, point: (f32, f32)) { self.open_contour(); self.commands.push(PathCommand::LineTo { x: point.0, @@ -389,12 +467,34 @@ impl<'a> Parser<'a> { if self.poisoned || !finite_point(one) || !finite_point(end) { self.poisoned |= !finite_point(one) || !finite_point(end); self.commands.clear(); + self.marker_elements.clear(); self.open = None; self.current = end; self.last_quad = Some(one); self.last_cubic = None; return; } + // Blink normalizes one quadratic to one cubic with these f32 blend + // operations before its marker tangent builder sees it. + let start = if self.open.is_none() { + self.subpath_start + } else { + self.current + }; + let one_third = 1.0 / 3.0_f32; + let cubic_one = ( + (start.0 + 2.0 * one.0) * one_third, + (start.1 + 2.0 * one.1) * one_third, + ); + let cubic_two = ( + (end.0 + 2.0 * one.0) * one_third, + (end.1 + 2.0 * one.1) * one_third, + ); + self.marker_elements.push(MarkerPathElement::CubicTo { + one: cubic_one, + two: cubic_two, + end, + }); self.open_contour(); self.commands.push(PathCommand::QuadTo { x1: one.0, @@ -412,12 +512,15 @@ impl<'a> Parser<'a> { if self.poisoned || !finite_point(one) || !finite_point(two) || !finite_point(end) { self.poisoned |= !finite_point(one) || !finite_point(two) || !finite_point(end); self.commands.clear(); + self.marker_elements.clear(); self.open = None; self.current = end; self.last_cubic = Some(two); self.last_quad = None; return; } + self.marker_elements + .push(MarkerPathElement::CubicTo { one, two, end }); self.open_contour(); self.commands.push(PathCommand::CubicTo { x1: one.0, @@ -454,6 +557,12 @@ impl<'a> Parser<'a> { self.current = self.subpath_start; return; } + // Blink's authored path stream preserves close commands even when a + // second close is raster-neutral. The marker builder still observes + // that authored vertex. + if !self.marker_elements.is_empty() { + self.marker_elements.push(MarkerPathElement::Close); + } match self.open { Some((_, true)) => { self.commands.push(PathCommand::Close); @@ -482,6 +591,7 @@ impl<'a> Parser<'a> { self.poisoned |= !finite_point(one) || !finite_point(end) || !weight.is_finite() || weight <= 0.0; self.commands.clear(); + self.marker_elements.clear(); self.open = None; self.current = end; return; @@ -528,11 +638,16 @@ impl<'a> Parser<'a> { } else { self.current }; + // Marker topology is independent from Skia's raster path. Blink's + // normalizer turns a coincident-endpoint or zero-radius arc into one + // line for marker placement even when the raster builder elides it. + let marker_element = marker_arc_element(start, radii, angle, large, sweep, end); + self.marker_elements.push(marker_element); if start == end { return; } match resolve_arc_like_skia(start, radii, angle, large, sweep, end) { - ArcResolution::Line => self.emit_line(end), + ArcResolution::Line => self.emit_raster_line(end), ArcResolution::NoOp => self.current = end, ArcResolution::Conics(conics) => { for (control, endpoint, weight) in conics { @@ -675,6 +790,347 @@ impl<'a> Parser<'a> { } } +#[derive(Clone, Copy, PartialEq, Eq)] +enum MarkerElementType { + Move, + Line, + Cubic, + Close, +} + +#[derive(Clone, Copy, Default)] +struct MarkerSegmentData { + start_tangent: (f32, f32), + end_tangent: (f32, f32), + position: (f32, f32), +} + +/// Blink's marker-position builder, kept producer-side because its authored +/// path topology has no place in the resolved frame contract. +fn build_marker_positions(elements: &[MarkerPathElement]) -> Vec { + struct Builder { + positions: Vec, + last_moveto_index: usize, + last_element_type: MarkerElementType, + origin: (f32, f32), + subpath_start: (f32, f32), + in_slope: (f32, f32), + out_slope: (f32, f32), + last_moveto_out_slope: (f32, f32), + } + + impl Builder { + fn compute_quad_tangents( + data: &mut MarkerSegmentData, + start: (f32, f32), + control: (f32, f32), + end: (f32, f32), + ) { + data.start_tangent = subtract(control, start); + data.end_tangent = subtract(end, control); + if is_zero(data.start_tangent) { + data.start_tangent = data.end_tangent; + } else if is_zero(data.end_tangent) { + data.end_tangent = data.start_tangent; + } + } + + fn features(&self, element: MarkerPathElement) -> (MarkerElementType, MarkerSegmentData) { + let mut data = MarkerSegmentData::default(); + let kind = match element { + MarkerPathElement::MoveTo(position) => { + data.position = position; + data.start_tangent = subtract(position, self.origin); + data.end_tangent = data.start_tangent; + MarkerElementType::Move + } + MarkerPathElement::LineTo(position) => { + data.position = position; + data.start_tangent = subtract(position, self.origin); + data.end_tangent = data.start_tangent; + MarkerElementType::Line + } + MarkerPathElement::CubicTo { one, two, end } => { + data.position = end; + data.start_tangent = subtract(one, self.origin); + data.end_tangent = subtract(end, two); + if is_zero(data.start_tangent) { + Self::compute_quad_tangents(&mut data, one, two, end); + } else if is_zero(data.end_tangent) { + Self::compute_quad_tangents(&mut data, self.origin, one, two); + } + MarkerElementType::Cubic + } + MarkerPathElement::Close => { + let mut tangent = subtract(self.subpath_start, self.origin); + if self.last_element_type != MarkerElementType::Move && is_zero(tangent) { + tangent = self.last_moveto_out_slope; + } + data.position = self.subpath_start; + data.start_tangent = tangent; + data.end_tangent = tangent; + MarkerElementType::Close + } + }; + (kind, data) + } + + fn current_angle(&self, ends_subpath: bool) -> f32 { + if self.last_element_type == MarkerElementType::Close { + return bisecting_angle(self.in_slope, self.out_slope); + } + if ends_subpath { + return slope_degrees(self.in_slope); + } + if self.last_element_type == MarkerElementType::Move { + return slope_degrees(self.out_slope); + } + bisecting_angle(self.in_slope, self.out_slope) + } + + fn update_angle(&mut self, ends_subpath: bool) { + if self.last_element_type == MarkerElementType::Close { + self.out_slope = self.last_moveto_out_slope; + } + let angle = self.current_angle(ends_subpath); + if self.last_element_type == MarkerElementType::Close { + self.positions[self.last_moveto_index].angle = angle; + } + self.positions.last_mut().expect("a prior marker").angle = angle; + } + + fn update(&mut self, element: MarkerPathElement) { + let (kind, segment) = self.features(element); + self.out_slope = segment.start_tangent; + if self.last_element_type == MarkerElementType::Move { + self.last_moveto_out_slope = self.out_slope; + } + let starts_new_subpath = kind == MarkerElementType::Move; + if !self.positions.is_empty() { + self.update_angle(starts_new_subpath); + } + self.in_slope = segment.end_tangent; + self.origin = segment.position; + if starts_new_subpath { + self.subpath_start = segment.position; + self.last_moveto_index = self.positions.len(); + } + self.last_element_type = kind; + self.positions.push(MarkerPosition { + kind: if self.positions.is_empty() { + MarkerType::Start + } else { + MarkerType::Mid + }, + origin: self.origin, + angle: 0.0, + }); + } + + fn finish(mut self) -> Vec { + if self.positions.is_empty() { + return self.positions; + } + self.update_angle(true); + self.positions.last_mut().expect("one marker").kind = MarkerType::End; + self.positions + } + } + + let mut builder = Builder { + positions: Vec::with_capacity(elements.len()), + last_moveto_index: 0, + last_element_type: MarkerElementType::Move, + origin: (0.0, 0.0), + subpath_start: (0.0, 0.0), + in_slope: (0.0, 0.0), + out_slope: (0.0, 0.0), + last_moveto_out_slope: (0.0, 0.0), + }; + for element in elements { + builder.update(*element); + } + builder.finish() +} + +pub(crate) fn line_marker_positions(start: (f32, f32), end: (f32, f32)) -> Vec { + build_marker_positions(&[ + MarkerPathElement::MoveTo(start), + MarkerPathElement::LineTo(end), + ]) +} + +pub(crate) fn points_marker_positions(points: &[(f32, f32)], closed: bool) -> Vec { + let Some((first, rest)) = points.split_first() else { + return Vec::new(); + }; + let mut elements = Vec::with_capacity(points.len() + usize::from(closed)); + elements.push(MarkerPathElement::MoveTo(*first)); + for point in rest { + elements.push(MarkerPathElement::LineTo(*point)); + } + if closed { + elements.push(MarkerPathElement::Close); + } + build_marker_positions(&elements) +} + +fn subtract(left: (f32, f32), right: (f32, f32)) -> (f32, f32) { + (left.0 - right.0, left.1 - right.1) +} + +fn is_zero(vector: (f32, f32)) -> bool { + vector == (0.0, 0.0) +} + +fn slope_degrees(vector: (f32, f32)) -> f32 { + vector.1.atan2(vector.0).to_degrees() +} + +fn bisecting_angle(in_slope: (f32, f32), out_slope: (f32, f32)) -> f32 { + let mut in_angle = f64::from(slope_degrees(in_slope)); + let out_angle = f64::from(slope_degrees(out_slope)); + let diff = in_angle - out_angle; + if diff > 180.0 || diff <= -180.0 { + in_angle += 360.0; + } + ((in_angle + out_angle) / 2.0) as f32 +} + +/// Normalize one authored arc to the single synthetic cubic Blink feeds to +/// its marker builder: first control from the first decomposed cubic, and +/// second control/end from the last. Failure becomes one line segment. +fn marker_arc_element( + start: (f32, f32), + radii: (f32, f32), + angle: f32, + large: bool, + sweep: bool, + end: (f32, f32), +) -> MarkerPathElement { + let curves = decompose_marker_arc_to_cubics(start, radii, angle, large, sweep, end); + let (Some(first), Some(last)) = (curves.first(), curves.last()) else { + return MarkerPathElement::LineTo(end); + }; + MarkerPathElement::CubicTo { + one: first.0, + two: last.1, + end: last.2, + } +} + +type MarkerCubic = ((f32, f32), (f32, f32), (f32, f32)); + +/// Port of Blink's `SVGPathNormalizer::DecomposeArcToCubic` arithmetic. +/// The raster path intentionally keeps its independent pinned-Skia conic +/// route; this helper exists only for authored marker tangents. +fn decompose_marker_arc_to_cubics( + start: (f32, f32), + radii: (f32, f32), + angle: f32, + large: bool, + sweep: bool, + end: (f32, f32), +) -> Vec { + let mut rx = radii.0.abs(); + let mut ry = radii.1.abs(); + if rx == 0.0 || ry == 0.0 || start == end { + return Vec::new(); + } + + let midpoint_delta = ((start.0 - end.0) * 0.5, (start.1 - end.1) * 0.5); + let transformed_midpoint = rotate_point_f32(midpoint_delta, -angle); + let square_rx = rx * rx; + let square_ry = ry * ry; + let square_x = transformed_midpoint.0 * transformed_midpoint.0; + let square_y = transformed_midpoint.1 * transformed_midpoint.1; + let radii_scale = square_x / square_rx + square_y / square_ry; + if radii_scale > 1.0 { + let scale = radii_scale.sqrt(); + rx *= scale; + ry *= scale; + } + if ![rx, ry].into_iter().all(f32::is_finite) { + return Vec::new(); + } + + let point1 = ellipse_to_unit(start, rx, ry, angle); + let point2 = ellipse_to_unit(end, rx, ry, angle); + let mut delta = subtract(point2, point1); + let length_squared = delta.0 * delta.0 + delta.1 * delta.1; + let scale_factor_squared = f64::from((1.0 / length_squared) - 0.25).max(0.0); + let mut scale_factor = scale_factor_squared.sqrt() as f32; + if sweep == large { + scale_factor = -scale_factor; + } + delta.0 *= scale_factor; + delta.1 *= scale_factor; + let mut center = ((point1.0 + point2.0) * 0.5, (point1.1 + point2.1) * 0.5); + center.0 -= delta.1; + center.1 += delta.0; + + let theta1 = (point1.1 - center.1).atan2(point1.0 - center.0); + let theta2 = (point2.1 - center.1).atan2(point2.0 - center.0); + let mut theta_arc = theta2 - theta1; + if theta_arc < 0.0 && sweep { + theta_arc += std::f32::consts::TAU; + } else if theta_arc > 0.0 && !sweep { + theta_arc -= std::f32::consts::TAU; + } + if !theta_arc.is_finite() { + return Vec::new(); + } + + let segments = (theta_arc.abs() / (std::f32::consts::FRAC_PI_2 + 0.001)).ceil() as usize; + if segments == 0 { + return Vec::new(); + } + let mut curves = Vec::with_capacity(segments); + for index in 0..segments { + let start_theta = theta1 + index as f32 * theta_arc / segments as f32; + let end_theta = theta1 + (index + 1) as f32 * theta_arc / segments as f32; + let tangent = (8.0 / 6.0_f32) * (0.25 * (end_theta - start_theta)).tan(); + if !tangent.is_finite() { + return Vec::new(); + } + let (sin_start, cos_start) = start_theta.sin_cos(); + let (sin_end, cos_end) = end_theta.sin_cos(); + let control_one_unit = ( + cos_start - tangent * sin_start + center.0, + sin_start + tangent * cos_start + center.1, + ); + let target_unit = (cos_end + center.0, sin_end + center.1); + let control_two_unit = ( + target_unit.0 + tangent * sin_end, + target_unit.1 - tangent * cos_end, + ); + curves.push(( + unit_to_ellipse(control_one_unit, rx, ry, angle), + unit_to_ellipse(control_two_unit, rx, ry, angle), + unit_to_ellipse(target_unit, rx, ry, angle), + )); + } + curves +} + +fn rotate_point_f32(point: (f32, f32), degrees: f32) -> (f32, f32) { + let radians = f64::from(degrees).to_radians(); + let (sin, cos) = radians.sin_cos(); + ( + (f64::from(point.0) * cos - f64::from(point.1) * sin) as f32, + (f64::from(point.0) * sin + f64::from(point.1) * cos) as f32, + ) +} + +fn ellipse_to_unit(point: (f32, f32), rx: f32, ry: f32, angle: f32) -> (f32, f32) { + let rotated = rotate_point_f32(point, -angle); + (rotated.0 / rx, rotated.1 / ry) +} + +fn unit_to_ellipse(point: (f32, f32), rx: f32, ry: f32, angle: f32) -> (f32, f32) { + rotate_point_f32((point.0 * rx, point.1 * ry), angle) +} + type ResolvedConic = ((f32, f32), (f32, f32), f32); enum ArcResolution { @@ -876,3 +1332,115 @@ fn skia_round(value: f32) -> f32 { fn finite_point(point: (f32, f32)) -> bool { point.0.is_finite() && point.1.is_finite() } + +#[cfg(test)] +mod marker_tests { + use super::*; + + fn assert_position(actual: MarkerPosition, kind: MarkerType, origin: (f32, f32), angle: f32) { + assert_eq!(actual.kind, kind); + assert_eq!(actual.origin, origin); + assert!( + (actual.angle - angle).abs() <= 0.000_01, + "expected angle {angle}, got {}", + actual.angle + ); + } + + #[test] + fn one_scan_projects_raster_commands_and_authored_vertices() { + let parsed = parse_path("M0 0L10 0L10 10"); + assert_eq!( + parsed.commands, + [ + PathCommand::MoveTo { x: 0.0, y: 0.0 }, + PathCommand::LineTo { x: 10.0, y: 0.0 }, + PathCommand::LineTo { x: 10.0, y: 10.0 }, + ] + ); + let positions = parsed.marker_positions().expect("finite marker projection"); + assert_eq!(positions.len(), 3); + assert_position(positions[0], MarkerType::Start, (0.0, 0.0), 0.0); + assert_position(positions[1], MarkerType::Mid, (10.0, 0.0), 45.0); + assert_position(positions[2], MarkerType::End, (10.0, 10.0), 90.0); + } + + #[test] + fn start_and_end_belong_to_the_whole_path_not_each_subpath() { + let positions = parse_path("M0 0L10 0M20 0L20 10") + .marker_positions() + .expect("finite marker projection"); + assert_eq!(positions.len(), 4); + assert_position(positions[0], MarkerType::Start, (0.0, 0.0), 0.0); + assert_position(positions[1], MarkerType::Mid, (10.0, 0.0), 0.0); + assert_position(positions[2], MarkerType::Mid, (20.0, 0.0), 90.0); + assert_position(positions[3], MarkerType::End, (20.0, 10.0), 90.0); + } + + #[test] + fn close_backpatches_the_start_and_keeps_the_duplicate_vertex() { + let positions = parse_path("M0 0L10 0L10 10Z") + .marker_positions() + .expect("finite marker projection"); + assert_eq!(positions.len(), 4); + assert_position(positions[0], MarkerType::Start, (0.0, 0.0), -67.5); + assert_position(positions[1], MarkerType::Mid, (10.0, 0.0), 45.0); + assert_position(positions[2], MarkerType::Mid, (10.0, 10.0), 157.5); + assert_position(positions[3], MarkerType::End, (0.0, 0.0), -67.5); + } + + #[test] + fn marker_only_shapes_survive_raster_geometry_elision() { + let move_only = parse_path("M12 20"); + assert!(move_only.commands.is_empty()); + let positions = move_only + .marker_positions() + .expect("move-only marker projection"); + assert_eq!(positions.len(), 1); + assert_position( + positions[0], + MarkerType::End, + (12.0, 20.0), + slope_degrees((12.0, 20.0)), + ); + + let polyline = points_marker_positions(&[(12.0, 20.0)], false); + assert_eq!(polyline.len(), 1); + assert_eq!(polyline[0].kind, MarkerType::End); + + let polygon = points_marker_positions(&[(12.0, 20.0)], true); + assert_eq!( + polygon + .iter() + .map(|position| position.kind) + .collect::>(), + [MarkerType::Start, MarkerType::End] + ); + } + + #[test] + fn one_authored_arc_is_one_marker_segment_even_when_raster_decomposes() { + let parsed = parse_path("M8 32A24 12 0 1 1 56 32"); + assert!( + parsed.commands.len() > parsed.marker_elements.len(), + "the raster conics may not become authored marker vertices" + ); + assert_eq!(parsed.marker_elements.len(), 2); + let MarkerPathElement::CubicTo { end, .. } = parsed.marker_elements[1] else { + panic!("an authored arc must have one synthetic cubic marker segment") + }; + assert!((end.0 - 56.0).abs() < 0.000_01 && (end.1 - 32.0).abs() < 0.000_01); + let positions = parsed.marker_positions().expect("finite arc projection"); + assert_eq!(positions.len(), 2); + assert_eq!(positions[0].kind, MarkerType::Start); + assert_eq!(positions[1].kind, MarkerType::End); + } + + #[test] + fn exact_opposite_tangents_take_blinks_lower_bisector_branch() { + let positions = parse_path("M8 32L32 32L8 32") + .marker_positions() + .expect("finite marker projection"); + assert_position(positions[1], MarkerType::Mid, (32.0, 32.0), 270.0); + } +} diff --git a/crates/websem/tests/marker_contract.rs b/crates/websem/tests/marker_contract.rs new file mode 100644 index 00000000..e6b56dc8 --- /dev/null +++ b/crates/websem/tests/marker_contract.rs @@ -0,0 +1,329 @@ +//! Same-document static SVG marker placement contracts. +//! +//! Chromium-baked cells own the pixel oracle. These laws pin the producer +//! structure and the refusal boundary: authored marker topology and resource +//! references disappear in `websem`, each admitted instance becomes ordinary +//! source-neutral items, and a rejected source removes the whole client in +//! both admissions. + +use rframe::{ClipEdgeMode, FrameItem, ScopeEffect}; +use websem::{CompileError, DegradationAction, InitialViewport, SvgFrameSource}; + +fn viewport() -> InitialViewport { + InitialViewport::new(64.0, 64.0) +} + +fn document(body: &str) -> String { + format!( + r##" +{body} +"## + ) +} + +fn arrow(body: &str) -> String { + format!( + r##"{body}"## + ) +} + +fn admit_both(source: &str) -> rframe::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"); + assert!( + best.degradations().is_empty(), + "admitted source declares nothing" + ); + assert_eq!(strict.base_frame(), best.base_frame()); + strict.base_frame() +} + +fn assert_skipped_by_marker(source: &str, needle: &str) { + let strict = SvgFrameSource::from_standalone_svg(source, viewport()) + .expect_err("strict must refuse the marker client"); + assert!( + matches!(strict, CompileError::UnsupportedMarker(_)), + "stable marker error class; got {strict}" + ); + assert!(strict.to_string().contains(needle), "got {strict}"); + + let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport()) + .expect("best effort declares and skips"); + assert!( + best.base_frame().nodes().is_empty(), + "a rejected marker source removes the client's base paint too" + ); + assert_eq!( + best.degradations().len(), + 1, + "unexpected declarations: {:?}", + best.degradations() + ); + assert_eq!(best.degradations()[0].action(), DegradationAction::Skipped); + assert!(best.degradations()[0].reason().contains(needle)); +} + +fn assert_marker_skip_among_declarations(source: &str, needle: &str) { + let strict = SvgFrameSource::from_standalone_svg(source, viewport()) + .expect_err("strict must refuse the marker client"); + assert!(matches!(strict, CompileError::UnsupportedMarker(_))); + assert!(strict.to_string().contains(needle), "got {strict}"); + + let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport()) + .expect("best effort declares and skips"); + assert!(best.base_frame().nodes().is_empty()); + assert!(best.degradations().iter().any(|degradation| { + degradation.action() == DegradationAction::Skipped && degradation.reason().contains(needle) + })); +} + +#[test] +fn instances_flatten_to_ordinary_nodes_inside_hard_viewport_clips() { + let source = document(&format!( + r##"{} +"##, + arrow(r##""##) + )); + let frame = admit_both(&source); + assert_eq!(frame.nodes().len(), 4, "one client and three instances"); + + let clips = frame + .items + .iter() + .filter_map(|item| match item { + FrameItem::ScopeBegin(scope) => match &scope.effect { + ScopeEffect::Clip(clip) => Some(clip), + ScopeEffect::Opacity(_) | ScopeEffect::Filter(_) => None, + }, + FrameItem::Node(_) + | FrameItem::ScopeEnd + | FrameItem::MaskBegin(_) + | FrameItem::MaskSource + | FrameItem::MaskEnd => None, + }) + .collect::>(); + assert_eq!(clips.len(), 3); + assert!( + clips + .iter() + .all(|clip| clip.edge_mode() == ClipEdgeMode::Hard), + "Chromium marker viewports are hard clips" + ); +} + +#[test] +fn marker_only_geometry_is_not_discarded_with_the_base_shape() { + let marker = arrow(r##""##); + let move_only = admit_both(&document(&format!( + r##"{marker}"## + ))); + assert_eq!( + move_only.nodes().len(), + 1, + "a lone path move selects only end" + ); + + let one_polyline = admit_both(&document(&format!( + r##"{marker}"## + ))); + assert_eq!( + one_polyline.nodes().len(), + 1, + "one open point selects only end" + ); + + let one_polygon = admit_both(&document(&format!( + r##"{marker}"## + ))); + let hard_clips = one_polygon + .items + .iter() + .filter(|item| { + matches!( + item, + FrameItem::ScopeBegin(scope) + if matches!(&scope.effect, ScopeEffect::Clip(clip) if clip.edge_mode() == ClipEdgeMode::Hard) + ) + }) + .count(); + assert_eq!(hard_clips, 2, "one closed point selects start and end"); +} + +#[test] +fn chromium_inapplicable_shape_attributes_and_bare_marker_are_inert() { + for shape in [ + r##""##, + r##""##, + r##""##, + r##""##, + ] { + let source = document(&format!( + "{}{}", + arrow(r##""##), + shape + )); + assert_eq!(admit_both(&source).nodes().len(), 1, "{shape}"); + } +} + +#[test] +fn direct_inheritance_and_none_reset_are_resolved_before_the_frame() { + let marker = arrow(r##""##); + let inherited = admit_both(&document(&format!( + r##"{marker}"## + ))); + assert_eq!(inherited.nodes().len(), 2, "line plus inherited end marker"); + + let reset = admit_both(&document(&format!( + r##"{marker}"## + ))); + assert_eq!(reset.nodes().len(), 1, "explicit none stops inheritance"); +} + +#[test] +fn only_a_selected_resolved_marker_enters_the_combined_opacity_span() { + let opacity_scopes = |source: &str| { + admit_both(source) + .items + .iter() + .filter(|item| { + matches!( + item, + FrameItem::ScopeBegin(scope) + if matches!(scope.effect, ScopeEffect::Opacity(_)) + ) + }) + .count() + }; + let client = |defs: &str, property: &str, path_data: &str| { + document(&format!( + r##"{defs}"## + )) + }; + + assert_eq!( + opacity_scopes(&client( + "", + r##"marker-end="url(#missing)""##, + "M8 48L32 8L56 48Z" + )), + 0, + "a missing target is opacity-equivalent to none" + ); + assert_eq!( + opacity_scopes(&client( + r##""##, + r##"marker-end="url(#wrong)""##, + "M8 48L32 8L56 48Z", + )), + 0, + "a wrong-type target is opacity-equivalent to none" + ); + assert_eq!( + opacity_scopes(&client( + r##"empty"##, + r##"marker-end="url(#m)""##, + "M8 48L32 8L56 48Z", + )), + 1, + "a selected marker resource keeps combined opacity even when its source is empty" + ); + assert_eq!( + opacity_scopes(&client( + r##""##, + r##"marker-end="url(#m)""##, + "M8 48L32 8L56 48Z", + )), + 1, + "a selected marker resource keeps combined opacity when its viewport suppresses paint" + ); + assert_eq!( + opacity_scopes(&document( + r##" +"##, + )), + 0, + "an unselected marker kind is opacity-equivalent to none" + ); +} + +#[test] +fn rejected_source_is_one_transaction_in_strict_and_best_effort() { + let source = document( + r##" + + + +"##, + ); + assert_skipped_by_marker(&source, "paint server"); +} + +#[test] +fn resource_root_rendering_declarations_refuse_before_inheritance_is_lost() { + for marker_declaration in [ + r##"shape-rendering="crispEdges""##, + r##"style="shape-rendering:crispEdges""##, + r##"paint-order="stroke fill""##, + r##"style="paint-order:stroke fill""##, + ] { + let source = document(&format!( + r##" +"## + )); + assert_marker_skip_among_declarations(&source, "resource-root rendering declaration"); + } +} + +#[test] +fn external_and_unresolved_resource_grammar_refuse_by_stable_marker_name() { + let external = document( + r##""##, + ); + assert_skipped_by_marker(&external, "external"); + + let calc = document( + r##" +"##, + ); + assert_skipped_by_marker(&calc, "markerWidth uses calc()"); + + let nested = document( + r##" + + + +"##, + ); + assert_skipped_by_marker(&nested, "nested marker source"); +} + +#[test] +fn position_and_source_fanout_limits_refuse_before_partial_paint() { + let mut path = String::from("M0 32"); + for index in 0..4096 { + path.push_str(&format!("L{} {}", index % 64, (index / 64) % 64)); + } + let too_many_positions = document(&format!( + r##"{}"##, + arrow(r##""##) + )); + assert_skipped_by_marker(&too_many_positions, "authored positions"); + + let source_nodes = (0..65) + .map(|index| { + format!( + r##""##, + index % 10, + index / 10 + ) + }) + .collect::(); + let too_many_source_items = document(&format!( + r##"{}"##, + arrow(&source_nodes) + )); + assert_skipped_by_marker(&too_many_source_items, "source emits 65 frame items"); +} diff --git a/crates/websem/tests/paths_contract.rs b/crates/websem/tests/paths_contract.rs index 33f64d16..a5ad7b89 100644 --- a/crates/websem/tests/paths_contract.rs +++ b/crates/websem/tests/paths_contract.rs @@ -744,23 +744,29 @@ fn the_css_d_property_is_declared_never_silently_dropped() { )); } -/// `marker-start`/`-mid`/`-end` are refused by name on ``. Nothing "reads" -/// a marker property — the property *is* the paint trigger, so this refusal is -/// what keeps Chromium's arrowhead from becoming a silent hole. +/// The admitted direct attributes do not weaken the pinned-cascade boundary: +/// Stylo has no marker longhands, so both CSS ingresses still refuse each +/// property by name instead of silently dropping an authored arrowhead. #[test] -fn marker_patrols_are_load_bearing() { - for attr in [ - r##"marker-start="url(#a)""##, - r##"marker-mid="url(#a)""##, - r##"marker-end="url(#a)""##, - ] { - let source = document(&format!( - r##" "## +fn marker_css_patrols_remain_load_bearing() { + for property in ["marker-start", "marker-mid", "marker-end"] { + let inline = document(&format!( + r##" "## + )); + let inline_error = refusal(&inline); + assert!( + inline_error.to_string().contains(property), + "got {inline_error}" + ); + + let sheet = document(&format!( + r##" + "## )); - let error = refusal(&source); + let sheet_error = refusal(&sheet); assert!( - matches!(error, CompileError::UnsupportedAttribute { ref element, .. } if element == "path"), - "{attr} must refuse by name; got {error}" + sheet_error.to_string().contains(property), + "got {sheet_error}" ); } } diff --git a/crates/websem/tests/points_contract.rs b/crates/websem/tests/points_contract.rs index 74a48c3e..0abc02b7 100644 --- a/crates/websem/tests/points_contract.rs +++ b/crates/websem/tests/points_contract.rs @@ -323,12 +323,13 @@ fn empty_and_missing_points_render_nothing() { } } -/// The points shapes inherit the path marker patrol: the unsupported paint -/// trigger refuses or skips by name, never silently. +/// Direct marker attributes are admitted, but the CSS-property twin remains a +/// pinned-Stylo hole and therefore still refuses or skips by name. #[test] -fn points_shapes_keep_the_marker_patrol() { - let source = - document(r##" "##); +fn points_shapes_keep_the_marker_css_patrol() { + let source = document( + r##" "##, + ); let strict = SvgFrameSource::from_standalone_svg(source.as_str(), viewport()) .expect_err("marker-end: strict refuses"); assert!( diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index f016f496..d8ad8c4e 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -492,6 +492,84 @@ const CORPUS: &[(&str, Departure, &str)] = &[ "unit whose basis", ), ("svg-image", DeclaredByBestEffort, ""), + ( + "svg-marker-author-stylesheet", + DeclaredByBestEffort, + "resource-side selector attribution", + ), + ("svg-marker-external", DeclaredByBestEffort, "external"), + ( + "svg-marker-height-var", + DeclaredByBestEffort, + "markerHeight uses var()", + ), + ( + "svg-marker-orient-var", + DeclaredByBestEffort, + "orient uses CSS math or var()", + ), + ( + "svg-marker-overflow", + DeclaredByBestEffort, + "authored overflow", + ), + ( + "svg-marker-refx-unit", + DeclaredByBestEffort, + "refX uses em units", + ), + ( + "svg-marker-refy-css-wide", + DeclaredByBestEffort, + "refY uses the CSS-wide value initial", + ), + ( + "svg-marker-root-effect", + DeclaredByBestEffort, + "effect on the ", + ), + ( + "svg-marker-root-opacity", + DeclaredByBestEffort, + "opacity on the ", + ), + ( + "svg-marker-root-rendering-style", + DeclaredByBestEffort, + "resource-root rendering declaration", + ), + ( + "svg-marker-root-transform", + DeclaredByBestEffort, + "transform on the ", + ), + ( + "svg-marker-source-animation", + DeclaredByBestEffort, + "animation inside", + ), + ( + "svg-marker-source-gradient", + DeclaredByBestEffort, + "paint server", + ), + ( + "svg-marker-source-nested", + DeclaredByBestEffort, + "nested marker source", + ), + ( + "svg-marker-source-opacity", + DeclaredByBestEffort, + "opacity, clip, mask, or filter scope", + ), + ("svg-marker-source-text", DeclaredByBestEffort, "a "), + ("svg-marker-source-use", DeclaredByBestEffort, "a "), + ( + "svg-marker-width-calc", + DeclaredByBestEffort, + "markerWidth uses calc()", + ), ( "svg-mask-css-properties", DeclaredByBestEffort, @@ -565,9 +643,6 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "declares d", ), - // The half of this fixture stopped declaring when the use/defs - // rung consumed defs; the marker attribute itself is the named hole. - ("svg-path-marker-end", DeclaredByBestEffort, "marker-end"), ( "svg-pattern-affine-precision", DeclaredByBestEffort, diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index 0eed24f7..f60386b0 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -79,20 +79,25 @@ from the dated addenda below: transforms, ``, and the established effect order; admitted filters may execute inside a pattern tile program, and a pattern-painted direct sharp-cornered rectangle may supply a filter's source image; + same-document static vertex markers on line/path/polyline/polygon clients, + with all three direct inherited position attributes, authored path topology + and tangent orientation, marker unit/viewBox/reference mapping, hard marker + viewport clips, solid/context-solid source programs, `` clients, and + client transform/opacity/clip/mask/filter composition; one declared-font, single-run `` profile; viewBox-only root sizing with the full `preserveAspectRatio` grammar; and one exact-time `` on a top-level ``, including a client carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 936 Chromium-baked primitive cells plus 16 sampled frames. +- **The corpus** is 1,026 Chromium-baked primitive cells plus 16 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 172 rows. + register has 189 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 @@ -4099,3 +4104,141 @@ The primitive corpus remains 936 cells and the filter estate remains 451 cells. The sampled corpus moves from 10 to 16 frames. Three focused rows join the refusal register, moving it from 169 to 172. No conformance score was produced, and no FLIP record, rule, or baseline changed. + +## Rung: same-document static SVG vertex markers (2026-08-28) + +The verdict is ADMIT/SPLIT with no checklist closure. A bounded M1 marker +profile now paints same-document static marker instances at authored vertices +of ``, ``, ``, and `` clients. It does not claim +the complete `` element, the complete marker-resource attribute +grammars, any marker CSS property, dynamics, or external resource loading. +Those are distinct M2/M3 and resource-I/O boundaries rather than implied +follow-through from one visible arrowhead. + +Client selection is the direct inherited presentation-attribute route for +`marker-start`, `marker-mid`, and `marker-end`. `none` stops inheritance; +missing, malformed, or wrong-kind references install no marker; same-document +URL tokens carry quoted, escaped, and ordinary fragment forms; and the +whole-document resource table is document-order, first-id-wins while excluding +`` shadow clones. `` may instantiate a marked client, with the +reference inherited either by the target or from the use site. Chromium drops +the bare `marker` attribute and all marker attributes on rect/circle/ellipse; +those inapplicability facts are committed cells, not accidental ignores. The +four CSS marker properties remain at the pinned Stylo cap: this Servo-mode +build has no corresponding longhands, so authored CSS ingress refuses rather +than introducing a second matcher around the one cascade. + +The path crux required two projections from one parse. Raster geometry remains +the existing canonical `rframe::PathData`; authored marker topology stays +producer-only. Chromium's start and end are attached to the whole path, not to +each subpath: the first authored element is start, the last is end, and every +element between them—including later moves and prior subpath endpoints—is mid. +A close contributes the duplicated subpath-start vertex and back-patches its +start angle. Move-only paths and one-point open point lists have one end +marker; a one-point polygon has coincident start and end markers. Completed +segments in an erroneous `d` prefix keep their markers. One authored cubic, +quadratic, or arc remains one marker edge even when the raster projection uses +a different curve decomposition. Degenerate controls, angle wrap, exact +opposite tangents, repeated moves, repeated closes, and zero-length lines are +all independently celled. + +Marker viewport resolution carries the M1 numeric/percentage/`px` subset of +`markerWidth`, `markerHeight`, `refX`, and `refY`; both case-sensitive +`markerUnits` values and the default; explicit angles in unitless, degree, +radian, gradian, and turn forms; `auto` and `auto-start-reverse`; and `viewBox` +through the complete admitted `preserveAspectRatio` mapper. Percentages use +the outer SVG viewport axes even when a marker viewBox changes its content +space, matching Chromium's measured behavior. Reference coordinates are +mapped through the viewBox before placement. Non-positive marker viewport +geometry and a degenerate viewBox paint the measured nothing. Default +`strokeWidth` units scale from the client's computed stroke width even when +the stroke paint itself is `none`. + +The grammar probes found two silent-parser traps before they could become +capability. Chromium accepts surrounding SVG whitespace on numeric orientation +angles, but the `auto` and `auto-start-reverse` keywords are exact authored +strings: surrounding whitespace or wrong case makes them invalid and selects +zero degrees. `markerUnits="userSpaceOnUse"` is likewise exact; surrounding +whitespace makes it invalid and selects initial `strokeWidth`. Both branches +are committed. The amplified direct and percentage numeric aliases selected +Chromium's value through the cssparser token route, including width, height, +references, and orientation. No marker numeric-provenance refusal was invented +because every discriminating candidate reproduced the oracle. Current +Chromium also drops the sampled standard-track `refX`/`refY` alignment keywords +to initial zero; representative drops are celled under the valid-browser-drop +precedent of [gridaco/nothing#77](https://github.com/gridaco/nothing/pull/77), +without claiming the wider grammar closed. + +An M1 source is one checked static solid/context-solid subtree of admitted +shapes and groups with admitted transforms. Empty sources are valid. The +client's context paint owner and reference box survive through marker source +resolution. Client transforms, root viewBox mapping, centred stroke geometry, +opacity, geometric clip, image mask, and filter scopes wrap the ordinary shape +and every marker instance in Chromium's measured order. A marker resource is +non-rendering at its document position, whether or not it lives in ``. + +The raster crux was the marker viewport boundary. Reusing ordinary +anti-aliased geometric clipping produced wrong Chromium edge pixels. Chromium's +marker viewport is a hard clip, including after marker placement. The resolved +contract therefore gained one source-neutral clip-edge enum whose default +preserves every existing anti-aliased clip and whose explicit hard member +states the required raster fact. Each marker instance is still only ordinary +frame nodes inside a generic hard clip scope; no marker, URL, DOM node, +resource id, authored path topology, or marker-specific paint operation crosses +`rframe`. + +Everything outside that source and grammar envelope refuses before painting: +external references; a used marker in a document with author CSS, whose +resource-side selector attribution is unavailable; CSS math, custom-property +substitution, wider units, and CSS-wide marker-resource values; authored +resource-root overflow, transform, opacity, clip, mask, filter, or an inherited +rendering declaration; source-side +paint servers, ``, text, effects, nested markers, and animations; and +checked position/source/client fan-out limits. Marker compilation is a client +transaction. On any failure strict returns the stable marker error, while +best effort truncates the complete client span and restores identity allocation +before declaring the skipped client. An ordinary fill or stroke can therefore +never leak beside a missing marker source. + +Four scratch matrices used Chromium 149.0.7827.55 through the one hash-pinned +capture module, and every candidate rendered through both actual `n0` +admissions. The broad matrix admitted 78 exact candidates and routed six to +intended refusals; the corrected M1 source/effect matrix admitted 37 exact and +refused five; all 44 precision/tangent candidates were exact; and the +resource-grammar matrix admitted 24 exact while twenty reached stable names +(measured, not celled). Ninety candidates ultimately entered the committed corpus +only through `just add`; every new oracle is byte-exact through strict and +best effort with no tolerance. + +The manual law pass found one further inherited-style leak before review. +`shape-rendering="crispEdges"` on the marker root inherits into the source +path exactly like the same child declaration; both attribute and inline-style +routes differ from the default by 128 pixels at maximum channel delta 255. +Root `paint-order="stroke fill"` likewise inherits like its child spelling and +differs from default by 121 pixels at delta 188. Before the patrol, strict and +best effort both rendered the default pixels with no declaration. A focused +resource-root rendering-declaration refusal now guards both ingresses and the +wider unrepresented inherited-property class (measured, not celled). + +A second manual law probe found a client-composition leak. Missing ids, +wrong-kind targets, and a valid `marker-mid` on a client with no mid vertex are +pixel-identical to `none` in Chromium. A selected real marker is different: +even when its viewport is zero or its valid source is empty, it still selects +the combined marker/client opacity composition, changing 997 pixels at maximum +channel delta 2 in the amplified witness. The former compiler selected that +composition for every syntactically present reference, making the missing and +wrong-kind cases 997 pixels at delta 2 wrong and the unselected-mid case 380 +pixels at delta 2 wrong in both admissions. Selection now requires an authored +vertex kind to resolve to an actual marker resource; the selected empty/zero +cases retain the combined route. Five Chromium-baked cells guard both sides, +and all seven probe controls are exact through both actual admissions (two +controls measured, not celled). + +Gate sensitivity was proved at the raster seam. Temporarily changing marker +viewport clips from hard to anti-aliased made `just gate` fail loudly on +seventeen named marker cells. Restoring the hard edge returned the complete +1,026-cell gate to green after the composition repair. The former broad marker refusal graduates; eighteen +focused source, resource, cascade, and value rows replace it, moving the named +register from 172 to 189. The sixteen exact-time frames and 451-cell filter +estate are unchanged. No checklist row closes, no conformance score was +produced, and no FLIP record, rule, or baseline changed. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 4c38d224..e2093498 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -1026,6 +1026,13 @@ excluded. - [ ] `marker-start` - [ ] `marker-mid` - [ ] `marker-end` + +> **2026-08-28 split:** the CSS property quartet stays open at the pinned +> Stylo cap. This build has no `marker-*` longhands, and authored CSS ingress +> refuses rather than growing a matcher around the cascade. The direct +> presentation-attribute route is recorded independently below; Chromium's +> bare `marker` attribute is inert and has its own drop cell. + - [ ] `paint-order` - [x] `path-length` - [ ] `color-interpolation` @@ -1691,6 +1698,21 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [x] `` - [x] `` - [ ] `` + +> **2026-08-28 split:** same-document static vertex markers now paint on +> ``, ``, ``, and ``. The committed matrix covers +> authored path topology and tangents, all three positions, inheritance, +> reference lookup, marker units and viewport mapping, hard viewport clips, +> context solids, `` clients, transforms, stroke geometry, and client +> opacity/clip/mask/filter ordering. The element stays open for the wider +> marker-source program, inherited rendering declarations on the resource +> root, dynamics, external resource loading, and the full resource-attribute/ +> cascade audit. The root-inheritance boundary is a measured named refusal. +> A second law audit found and repaired the no-marker opacity split: a missing +> id, wrong-kind target, or unselected marker kind behaves like `none`, while +> a selected real marker keeps combined opacity composition even when its +> viewport or source paints nothing. Five committed cells guard both sides. + - [ ] `` > **2026-08-24 split:** one isolated alpha/luminance source image, both @@ -1874,6 +1896,18 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `marker-end` - [ ] `marker-mid` - [ ] `marker-start` + +> **2026-08-28 split:** the three direct inherited attributes carry +> same-document `url(#…) | none` on the four applicable shape elements. +> First-id lookup, missing/wrong/malformed fallback, quoted/escaped URLs, +> placement on authored path vertices, `` instances, transforms, stroke, +> and client effects are Chromium-baked. Missing/wrong targets and an +> unselected marker kind are distinguished from a selected empty or +> zero-viewport resource at the client-opacity seam. External URLs, CSS +> ingress, nested +> markers, wider marker-source composition, and dynamics refuse by stable +> marker names, so all three attribute rows remain open. + - [ ] `mask` - [x] `mask-type` @@ -2079,6 +2113,16 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `markerHeight` - [ ] `markerUnits` - [ ] `markerWidth` + +> **2026-08-28 split:** the M1 marker viewport carries numeric and percentage +> width/height, `px`, missing/default, invalid, zero and negative behavior, +> direct-number normalization witnesses, and both case-sensitive +> `strokeWidth | userSpaceOnUse` unit branches. A whitespace-padded +> `userSpaceOnUse` is invalid in Chromium and selects the initial +> `strokeWidth`; that parser distinction is celled. CSS math, `var()`, wider +> units, CSS-wide values, resource-side cascade, and the M3 complete-grammar +> audit remain named work, so none of these rows closes. + - [x] `maskContentUnits` - [x] `maskUnits` @@ -2192,6 +2236,14 @@ for attributes the platform ships ahead of the SVG 2 indexes. > an unconstructable finite area produces the measured transparent result. The > wider invalid-spelling matrix is measured, not all separately celled. - [ ] `orient` + +> **2026-08-28 split:** static marker orientation carries explicit unitless, +> `deg`, `rad`, `grad`, and `turn` angles plus exact `auto` and +> `auto-start-reverse`. The two keywords are case- and surrounding-whitespace +> sensitive in Chromium while numeric angles accept SVG whitespace; invalid +> values select zero. CSS math, `var()`, CSS-wide/resource-cascade behavior, +> animation, and the complete M3 grammar audit keep the row open. + - [ ] `path` - [x] `pathLength` - [x] `patternContentUnits` @@ -2237,6 +2289,15 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `referrerpolicy` - [ ] `refX` - [ ] `refY` + +> **2026-08-28 split:** numeric and percentage marker references are baked +> with and without `viewBox`, including negative values and direct-number +> normalization witnesses. Current Chromium drops the SVG 2 +> `left | center | right` and `top | center | bottom` keyword sets to the +> initial zero; representative drop cells preserve that browser fact. Units, +> CSS-wide values, computed functions, resource-side cascade, and the full M3 +> audit remain guarded, so both rows stay open. + - [ ] `rel` - [ ] `requiredExtensions` - [ ] `result` diff --git a/docs/wg/research/chromium/svg/resources-and-effects.md b/docs/wg/research/chromium/svg/resources-and-effects.md index a521f5a0..d31110ae 100644 --- a/docs/wg/research/chromium/svg/resources-and-effects.md +++ b/docs/wg/research/chromium/svg/resources-and-effects.md @@ -217,12 +217,22 @@ class SVGMarkerDataBuilder : private SVGPathConsumer { ``` `SVGMarkerDataBuilder` walks the path as it's emitted segment-by-segment, -computing tangent vectors. For each marker position: - -- **Start** — first point after `moveto`; angle = outgoing tangent. -- **End** — last point of each subpath; angle = incoming tangent. -- **Mid** — interior segment joins; angle = bisecting tangent (unless - `orient="auto"` vs explicit). +computing tangent vectors. Chromium 149 probes correct an easy but material +misreading of that walk: start and end belong to the **whole path**, not to +each subpath. The first authored path element is the start, the final element +is the end, and every element between them is a mid — including a later +subpath's `moveto` and the preceding subpath's endpoint. A close contributes a +position at the subpath start and back-patches that start position's angle. +A move-only path therefore has one end marker; a one-point open polyline also +has one end, while a one-point closed polygon has start and end at the same +point (measured and Chromium-celled 2026-08-28). + +The start uses its outgoing tangent, the end its incoming tangent, and a mid +uses the bisector of the incoming and outgoing tangents. Degenerate controls +fall through Blink's tangent-recovery rules, exact opposite tangents take its +lower-angle branch, and wraparound is corrected before averaging. Quadratic +and arc normalization remains marker-local: one authored segment creates one +marker edge even when the raster path uses another curve decomposition. ### Marker painting diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index 6436f303..f04661ba 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -259,6 +259,11 @@ is exactly what the engine renders pixel-for-pixel. | *(measured, not celled — pattern/filter split)* | The context-aware filter audit found that an authored context keyword hid the eventual gradient from old source-profile patrols: component transfer changed 738px/Δ1, convolution 2,277px/Δ7, morphology 1,262px/Δ1, and native drop shadow 684px/Δ1. Those routes now reach their existing stable names. Pattern profiles were exact across 154 strict/best comparisons over transparent, alpha, opaque, gradient-bearing, masked, stroked, and nested tiles, and another 112 comparisons covered fill/stroke, both context crossings, and destination paint opacity. Follow-ups made all twelve admitted families visibly exercise a sole nested-pattern draw (26 exact strict/best comparisons including the control), then crossed the six remaining families with direct/context pattern strokes at opaque and partial paint opacity (56 exact comparisons). A blanket exemption was rejected: a pattern-filled cubic path differed under color matrix/component transfer (1px/Δ1), offset (1px/Δ2), and merge (2px/Δ2); a rounded rect differed under blur (15px/Δ1), morphology (1px/Δ7), and drop shadow (4px/Δ4). The new filtered-pattern coverage refusal admits only the complete direct sharp-rect profile. Misclassifying a pattern as an unadmitted server made `just gate` fail on the context/color-matrix cell; restoration returned all 936 cells green. The corpus is now 936 cells plus 10 sampled frames, the separately tracked filter estate remains 451 cells, and the named register has 169 rows. | | `animation/svg-pattern-client-{base,animation}.svg` | Six committed Chromium frames carry the admitted exact-time client through a templated pattern, a source color-matrix filter, and a target color-matrix filter: Base plus samples at 0, 0.25, 1, 2, and 3 seconds. Every strict and best-effort render is exact decoded RGBA, including the frozen endpoint and shuffled retained seeks. | | *(measured, not celled — pattern dynamics split)* | Moving the pattern-painted client is exact; moving pattern source geometry or tile phase is not admitted. Fractional phase is content-dependent: object-box and filtered-source X witnesses changed 192px/Δ1 and 356px/Δ1, while discriminating Y mirrors changed 96px/Δ1 and 576px/Δ1. A review matrix then isolated the object-box client's own fractional origin: unfiltered X/Y controls were exact, while the filtered-source X and Y witnesses each changed 336px/Δ1. A valid midpoint-adjacent animation endpoint selected opposite binary32 neighbours in Chromium and the former direct parser, changing 48px/Δ238. Rust-only trailing-dot and Unicode-whitespace spellings changed 3,072px/Δ238 before the SVG-number syntax patrol. Three focused refusal rows guard these classes. The primitive corpus remains 936 cells, the sampled corpus moves from 10 to 16 frames, the filter estate remains 451 cells, and the named register moves from 169 to 172 rows. No checklist row closes. | +| `svg-path-marker-end.svg` · `svg-marker-{line,path,polyline,polygon,inherited-end,none-reset,missing-reference,wrong-target,duplicate-id-first,url-*,bare-attribute-drop,rect-drop,circle-drop,ellipse-drop}.svg` | Same-document marker selection and applicability: all three position attributes, direct inheritance and reset, first-id lookup, quoted and escaped URL forms, invalid-reference fallback, and Chromium's inert bare `marker` attribute and inapplicable shape attributes. The former marker refusal graduates as the end-marker cell. | +| `svg-marker-{path-*,polyline-one-point,polygon-one-point,line-zero-length,tangent-*}.svg` | Authored topology and tangent evidence: one whole-path start/end pair across subpaths, move-only and one-point cases, valid erroneous-path prefixes, repeated moves and closes, one vertex per authored line/cubic/quadratic/arc, close back-patching, degenerate tangent recovery, angle wrap, and the exact-opposite branch. | +| `svg-marker-{width-*,height-*,units-*,orient-*,refx-*,refy-*,reference-negative,viewbox-*,default-viewport-clip}.svg` | Marker viewport and placement grammar in M1: numeric/percentage/`px` lengths, direct-number normalization witnesses, defaults and invalid/non-positive behavior, both unit systems, explicit angle units plus auto orientation, reference mapping, all three `viewBox` aspect-ratio modes, invalid/empty boxes, and Chromium's hard default viewport clip. Keyword and enum whitespace drops are independently discriminated. | +| `svg-marker-{source-*,context-*,client-*,display-none-resource}.svg` | The admitted static source/client profile: empty and solid shape/group sources, transforms, context fill/stroke, non-rendering resource placement, root mapping, `` clients, computed stroke-width scaling even with no stroke paint, and client transform/opacity/clip/mask/filter ordering. Five client-opacity cells distinguish a selected real marker whose empty source or zero viewport still selects combined composition from missing, wrong-kind, or unselected references that behave like `none`. Every rejected source is transactional, so no client stroke leaks in best effort. | +| *(measured, not celled — marker M1 audit)* | Chromium 149.0.7827.55 was probed through the shared hash-pinned capture module, and every candidate also rendered through both actual CLI admissions. The broad matrix admitted 78 exact sources and routed six to intended refusals; the corrected M1 source/effect matrix admitted 37 exact and refused five; all 44 precision/tangent candidates were exact; and the resource-grammar matrix admitted 24 exact while twenty reached stable names. The probes found two parser distinctions before admission: `auto`/`auto-start-reverse` reject wrong case or surrounding whitespace while numeric angles accept whitespace, and whitespace-padded `markerUnits="userSpaceOnUse"` is invalid and selects `strokeWidth`. Blink-style token decoding reproduces all amplified marker numeric aliases, so no numeric-provenance refusal was invented. The raster crux was marker viewport clipping: ordinary anti-aliasing changed exact Chromium pixels, while a hard clip matched. Deliberately restoring anti-aliasing made `just gate` fail on seventeen named marker cells. The manual law pass then found a resource-root inheritance leak: root `shape-rendering="crispEdges"` differs from default by 128px/Δ255 and root `paint-order="stroke fill"` by 121px/Δ188, with attribute and inline-style routes each identical to the same child declaration. Before the new patrol, both n0 admissions silently reproduced the default (128px/Δ255 wrong for the shape-rendering witness). A second law probe found that missing and wrong-kind references and an unselected `marker-mid` are pixel-identical to `none`, while a selected real marker with a zero viewport or empty source differs by 997px/Δ2 because it still selects combined opacity composition. The former implementation isolated all syntactically present references, making the missing/wrong witnesses 997px/Δ2 wrong and the unselected-mid witness 380px/Δ2 wrong; the repaired route and all seven controls are exact in both admissions. Ninety new cells are byte-exact, the complete 1,026-cell gate is green, the sixteen sampled frames and 451-cell filter estate are unchanged, and replacing the former broad refusal with eighteen focused rows moves the named register from 172 to 189. No checklist row closes. | | `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 323c4ac7..98fe6394 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 (936) +## Chromium-baked cells (1026) Each renders byte-exact against its committed Chromium oracle (seven curved cells and four gradient ramps carry a declared, bounded @@ -594,6 +594,95 @@ to its fixture source. No new image is committed for this view. svg-group-rotate-diagonal svg-group-rotate-quarter svg-group-transform-translate +svg-marker-bare-attribute-drop +svg-marker-circle-drop +svg-marker-client-clip +svg-marker-client-filter +svg-marker-client-mask +svg-marker-client-opacity +svg-marker-client-opacity-empty-source +svg-marker-client-opacity-missing +svg-marker-client-opacity-unselected-mid +svg-marker-client-opacity-wrong-target +svg-marker-client-opacity-zero-viewport +svg-marker-client-root-viewbox +svg-marker-client-stroke-none-width +svg-marker-client-transform +svg-marker-client-use-inherited +svg-marker-client-use-target +svg-marker-context-fill +svg-marker-context-stroke +svg-marker-default-viewport-clip +svg-marker-display-none-resource +svg-marker-duplicate-id-first +svg-marker-ellipse-drop +svg-marker-height-direct-alias +svg-marker-height-percentage +svg-marker-inherited-end +svg-marker-line +svg-marker-line-zero-length +svg-marker-missing-reference +svg-marker-none-reset +svg-marker-orient-auto-start-reverse +svg-marker-orient-degrees +svg-marker-orient-direct-alias +svg-marker-orient-gradians +svg-marker-orient-invalid +svg-marker-orient-radians +svg-marker-orient-turn +svg-marker-orient-whitespace-drop +svg-marker-path +svg-marker-path-arc-authored-vertex +svg-marker-path-cubic-authored-vertex +svg-marker-path-move-only +svg-marker-path-multiple-subpaths +svg-marker-path-quadratic-authored-vertex +svg-marker-path-repeated-close +svg-marker-path-repeated-moves +svg-marker-path-valid-prefix +svg-marker-polygon +svg-marker-polygon-one-point +svg-marker-polyline +svg-marker-polyline-one-point +svg-marker-rect-drop +svg-marker-reference-negative +svg-marker-refx-direct-alias +svg-marker-refx-keyword-drop +svg-marker-refx-percentage-root +svg-marker-refx-percentage-viewbox +svg-marker-refy-keyword-drop +svg-marker-refy-percentage-alias +svg-marker-source-empty +svg-marker-source-group-transform +svg-marker-source-shapes +svg-marker-tangent-angle-wrap +svg-marker-tangent-degenerate-cubic +svg-marker-tangent-degenerate-quadratic +svg-marker-tangent-opposite +svg-marker-units-default +svg-marker-units-invalid +svg-marker-units-stroke-width +svg-marker-units-user-space +svg-marker-units-user-space-whitespace +svg-marker-url-commented +svg-marker-url-escaped +svg-marker-url-malformed-drop +svg-marker-url-quoted +svg-marker-viewbox-empty +svg-marker-viewbox-invalid +svg-marker-viewbox-meet +svg-marker-viewbox-negative +svg-marker-viewbox-none +svg-marker-viewbox-nonzero-origin +svg-marker-viewbox-slice +svg-marker-width-direct-alias +svg-marker-width-invalid +svg-marker-width-negative +svg-marker-width-percentage +svg-marker-width-percentage-alias +svg-marker-width-px +svg-marker-width-zero +svg-marker-wrong-target svg-mask-attribute-grammar svg-mask-clip-intersection svg-mask-content-object @@ -658,6 +747,7 @@ to its fixture source. No new image is committed for this view. svg-path-length-metrics-contours svg-path-length-nonfinite-phase svg-path-length-range +svg-path-marker-end svg-path-polygon-fill svg-path-quadratic svg-path-relative-commands @@ -965,7 +1055,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (172) +## The refusal register (189) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1073,6 +1163,24 @@ its row into the cells above. | `svg-gradient-stop-var` | declared | skipped svg/rect[1]: unsupported fill value "url(#g): a stop-opacity resolves through var(), an indirection this patrol cannot follow" | | `svg-gradient-unit-basis` | declared | skipped svg/rect[1]: unsupported fill value "url(#g): gradient geometry x2=\"4em\" uses a unit whose basis this slice does not consume (numbers, px, and percentages only)" | | `svg-image` | declared | skipped svg/image[1]: unsupported element | +| `svg-marker-author-stylesheet` | declared | skipped svg/line[1]: unsupported SVG marker: a used marker in a document with author CSS needs resource-side selector attribution at the pinned marker cascade boundary | +| `svg-marker-external` | declared | skipped svg/line[1]: unsupported SVG marker: marker-end url(https://example.com/markers.svg#arrow) is external; this compiler owns no resource I/O | +| `svg-marker-height-var` | declared | skipped svg/line[1]: unsupported SVG marker: marker markerHeight uses var(), whose computed length is not represented by the direct resource decoder | +| `svg-marker-orient-var` | declared | skipped svg/line[1]: unsupported SVG marker: marker orient uses CSS math or var(), whose resource-side computed angle is not represented | +| `svg-marker-overflow` | declared | skipped svg/line[1]: unsupported SVG marker: authored overflow on is outside the default hidden viewport-clip profile | +| `svg-marker-refx-unit` | declared | skipped svg/line[1]: unsupported SVG marker: marker refX uses em units, whose basis is not admitted | +| `svg-marker-refy-css-wide` | declared | skipped svg/line[1]: unsupported SVG marker: marker refY uses the CSS-wide value initial, whose resource-side cascade is not represented at this Stylo pin | +| `svg-marker-root-effect` | declared | skipped svg/line[1]: unsupported SVG marker: an effect on the resource root needs marker-source effect composition | +| `svg-marker-root-opacity` | declared | skipped svg/line[1]: unsupported SVG marker: opacity on the resource root needs one source compositing scope | +| `svg-marker-root-rendering-style` | declared | skipped svg/line[1]: unsupported SVG marker: resource-root rendering declaration shape-rendering on can inherit into its source subtree, outside the M1 source profile | +| `svg-marker-root-transform` | declared | skipped svg/line[1]: unsupported SVG marker: transform on the resource root is outside the admitted viewport mapping | +| `svg-marker-source-animation` | declared | skipped svg/line[1]: unsupported SVG marker: animation inside a used marker source needs the marker sampling inventory | +| `svg-marker-source-gradient` | declared | skipped svg/line[1]: unsupported SVG marker: a marker source resolves a paint server outside the admitted solid/context-solid profile | +| `svg-marker-source-nested` | declared | skipped svg/line[1]: unsupported SVG marker: marker #arrow source cannot compile completely: unsupported SVG marker: nested marker source reaches url(#inner); bounded nested marker composition is a later rung | +| `svg-marker-source-opacity` | declared | skipped svg/line[1]: unsupported SVG marker: a marker source needs an opacity, clip, mask, or filter scope outside the admitted simple subtree | +| `svg-marker-source-text` | declared | skipped svg/line[1]: unsupported SVG marker: a inside a used marker source is outside the admitted static solid subtree | +| `svg-marker-source-use` | declared | skipped svg/line[1]: unsupported SVG marker: a inside a used marker source is outside the admitted static solid subtree | +| `svg-marker-width-calc` | declared | skipped svg/line[1]: unsupported SVG marker: marker markerWidth uses calc(), whose computed length is not represented by the direct resource decoder | | `svg-mask-css-properties` | declared | skipped svg/rect[2]: unsupported computed style: style attribute on declares mask-image, which this cascade does not represent | | `svg-mask-cycle` | declared | skipped svg/rect[2]: unsupported SVG mask: mask source cannot be compiled completely: unsupported SVG mask: mask source cannot be compiled completely: unsupported SVG mask: url(#a) forms a cyclic nested mask chain | | `svg-mask-external` | declared | skipped svg/rect[2]: unsupported SVG mask: url(https://example.test/mask.svg#m) is external; this compiler owns no resource I/O; skipped svg/rect[3]: unsupported SVG mask: url(../mask.svg#m) is a relative external reference; this compiler owns no resource base or I/O | @@ -1090,7 +1198,6 @@ its row into the cells above. | `svg-mask-var` | declared | skipped svg/rect[2]: unsupported SVG mask: mask presentation attribute uses var(), whose substitution is not represented at this Stylo pin | | `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-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-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" | diff --git a/fixtures/web-first/chromium/svg-marker-bare-attribute-drop.png b/fixtures/web-first/chromium/svg-marker-bare-attribute-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-circle-drop.png b/fixtures/web-first/chromium/svg-marker-circle-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..8acf4a8c3159121b223465da4a11568b95a0e310 GIT binary patch literal 1652 zcmV-)28;QLP);S-vmMl>-J zP0F^QjU)oa1QR3@FltON(Evu&(Ct%#(B0Y=%A?)A_jqQvMLtO4>^s^0rS06i+nG7{ zKQrgf%ozh&j)5#ECjhdXoB+sjasnXB$q9fgC+h{UkRm*5dS`-ozWm-pj42GB3ec43 zx7Q0s99S6Yr{1AhHR(Lbjto#U)e^mB;PcO?T}q!7_`6uuabwa zyeaP&Z0BwAe^S(j1e0(9Jl?<#BD^ws&x}SGhR^RSJ)u#Il+!zp$X+iduKu?ye#hTb zz6A*+VFKLc4UDsaFXiDMQuiFWrp@t=<(m573^@F*Sh_e;F5{cap|T|xo9PM?Ktcql ztnI6G8R4&_w7bQ`pbg{M_QtYfNFX)!y$ithUNN~$MEK1KnTy(+OS%y!aSKr6?Vko3 z{!tkwx8H~9wiO+|u@{j9CVK;u^1%nBXU&u05C|E0bGv*+9f*Ut1n{ivEu;cRjqMrI z<$eA}_gbU?kGH2uL|CJG!axDf4SZBSh!}}uA|!9^eCLo{%#iD&{>HL(ND=-fcb)PD z;%{b-zm5YDTVf<;0X*K|%gW)2A+1+Tx4iCTdXqAUO<<{1cvgDCLV066j~IzbIdOVL z;`F-3fU!2LSshKqr;!F~YJ2X3NhigQzOZdg>)cb^4^3j~0j3dtOQahola<}+dU!?a zp7K-T3_q1tEVj(hn}`8z0n~e0DqJr!x~qnnw;5?BWYHF7i@1RekvW4V+5(8;8)fXv zkFo7+LtzkUrmMmI7dzN42APh-FF_M+0SLTI93NjXOLrneV3)O1*(4A1Bs9?ypt?RV zCpzKb6XzS~Dl&xjhLQmpmOn}BO!CzA%+krLC4ju>7Dq4WC^8Jjs7^+6KKY71}Z{ zsGC~fUxhTGB2v;AwaONotk)0&F$vJV%Y9aoCbbRaazgtmYqnaPeQyOh&e2GCobgAPtjA&q;*1=F|i|&aICFodJ@6!fYh2bVe&KMck(&j7w z90??8r^Kr30VOPpIb6L+ygr%hM>!BoW^Qzk&Bk~=JoEGH)bvYea%$Z~Q5 yAj`=KfGp=900030|3S+&TL1t621!IgR09C?M?NUN!@^tu0000BB}yrcxYv?7Vu zpu`A-ZmY%%PZ)hbpJ-yz2Q{Gfsjh8ZmNd4z;sz2FcXy8889KH!6m~9WwlMR1*)y|q zzB%)sZ@x3qk-+2l0)WTy1ptrZ3jiKR0T(&5yseK(u#y4fWCq8=XTSX(Dhz^) zq{HoO?lxmEBm?w@&weum6{4a77*tY-MInGeC4^X1MGz4bO@ku1h^s|#Ax47DkW+#k ziPL?bgUeJ@fOK{J^kDSyQ{UZne> za;!h}1-OW-0*tR%H~iv#yI23bbajw6x2O6;hxz`vn%zz#`H(U8hYZ*}nhae4mvFTR zwmtO85B%aO-G4MLo|8(34)J}6xU7iy8n}pS0z?w$)&R0@-qti(o&UYDPXe}6Kh`B& z6F`zmNxi}F{qey&M&^I7?_=!X700?UE^vXUn%zQ0u7$2XM|XHHEI1h4!;j&|#=5eb z!3BgD!RD>!YmF)o8g!%vLf4$?;$7Je`uLP;LcXXeO}`H=$%8Fuk}T+){Q*mjEyKLFkI8*%lCpPyr;=W{Mf^ zOC^Kj<@TFMhDNE+>v{}eLJOj>8bC#C7X^QpQa|%sGPD9L5L2^fC=0?AV;99Z00Iz_ z0gZ|2Wi+MfcaQ7VoDP3 zqxXw2ID}&YFeuEVC;N!fb{HIn$D~^DF>fQ54ueBDB0yww?lCHSYx(;-%2m&UAS9*g zIsF`n>tda`N5KJX3(&Bh1td-TfVWA6o?nmLmIFcX*z}m~);EwH+`i87csQ=1Jito0qjB=YX)wtRF93(>3sGPey51ZD*unp=FnAna z0Pr}z0N`w%uA}Q0S_Yt?tnd#Yi*(Oe8)q5!U@D>P!Tqd!sM|!^F4+lL0XSrVtU| z>uNCSe`NmU7XOfhiT+=Lt-CxRHoAlQ`gn$`D*w*j1sv<&K)&P+RXJ1{{I@SE( z8_>7%p4Y}sE}LM#KuIghr-UB0?i^nVDv%Zdwmk9aPrq2!m~puO*xzaWFqZCd=YPP` z2q6XLknkp`fa(I+>n87`K$@_jloDOd!hDt{H8|OLx&=Ui$mA@{` zm(mD*4-4p6S>TMS0@!POjo7gD*w6kcBecquyOIkntPCq_uk|$mM^qD_@P(@uY|!DE zS7f*E{oAj}m1TK2KlVr^0gjY4PA&&ds3w3(A8Kb?98T_U`(4&AZdKmXY5km1dY07@ zqT*5Dgen3!Y9}7Tm7Ohk;jmz+TWmd1H&X}Ki&}fVZyj)ezW{AiARx{zX5@&s!#udy zs(*cA{S&7`yHr!8ZWap^hPwccVgDYS$amw=A2D3D9DoGL30U^Cx;TQ?I!64vL1DNa zg-h$N-Juhw2l3`jA_$cOE=xBgfxT|RDS-GjPzXc>Q~6-W(#xO_Tt|S8PQS|RdxNlDdBgVbJiC{RlnY{<@FqQT?|C5jc ziV09sO^priV0xQ`42KDXQ9i(i#hiQYm92^;!hZiyT2rmun9v}%)762Ea7qJI6 z-pb|S#hiOYHacqj&qIQwBmi@2@FC@`nO8S=d3bSydnAa*61CG)5`aXahwyLyVqG3y z#M~oxFUv!4D3Q8J3&0#2-0pTjf-Vm)V(t-&uydiEK3rJwl3cx{1z-*hv$qpC4i&Wzl%9E$+|qem~)R{{qLnujbrZ|#4cKbon)4) zldvOH^Jtgtfw0FjZ7}9^ls{LQiqXM?pUbNHR)hm<=cnOO;Y!I!Ph1fa5@6W2dCR2U zd}>7`aNDW7Dt>GJu9^EsSBu4Kt2l-&ZNdo<*C!BlAyetl>CnRTMVSgi`4x8(BWcH{ zuKk-~WCvWkrqZ7v@s0`@X_B;GtBRQww<2;1m4{n%lT4Hr5a zsX!0oW^lvDX_N>B@#@7L0c=P`iS9gWJjkGn>c=3%z>cD9G(Zs6;K8VCejYwC#a#sB z3c!NkbM`x^=yi}GWDEu%Ln-n1Tq+su*@~Y?ZN;WvVrf*QaYsN}z9W=LWWOyA8Ye3N zXq>D7pmDMSfX2xR02)X00{jO60RR6e)4d)5000I_L_t&o0Kp(I^2{p%E&u=k07*qo IM6N<$g0r5rI{*Lx literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-mask.png b/fixtures/web-first/chromium/svg-marker-client-mask.png new file mode 100644 index 0000000000000000000000000000000000000000..871038a848333de46ee7236881d78392e0ea89a7 GIT binary patch literal 1104 zcmV-W1h4yvP)5OzbZK5+Hih>L1gD9xgN6|j0pf+6;7u-NdL2w1N;*%TL zG$N>=AU^u0LQx+SaYuVyS{JlQOSNL7ZSL_qsRg&#-ZuB-CinhG;M}u3oPWOWJ174{ zfyGf^aclu#aclu#aclu#ah#AyQ{f!`so8)8LPammH6X(fS4$+Y;Ce&Bq*BgNg$xLgks2UA@rb{y z^!N6hsHqg9ZA;R1|U^2SRW+THWS)1>?!vqL4$JS9I|4X6&)b+-@ z4LZr|@NN}rVK9aWP+a-;531%nM4L0E`tE7=0Xh+D#9A1PA>Rc86)mUe$SqZukHUV5(SD12t2<84UwcdLfSwdO_7#VDx zPgmC&bZ)nIcDpwqix{*3|19Hfj>LY^rZY8^ba=9eK^H-nBetEkULYJD^)r_zTNso8 zB~{T;Jc0Gp{I1KB4Gby;lmw3?9h?*{Pc|^%2=G_LmeR(j_T}=JodHFFaB382dwZUK z?DCkM0Yw1oPVAz)ZoY7NsPOga@|YRj5#X788Jyx@ldJjpNplet+fSzC)~^U1zRuWp8sL!=a-c|_?7O~Ec|e(~!} zQT)X{00080Nklw5~gzL>cKz zn&dq>Z^9ry!f3Pf&oA$j_vEDM_=l&s&fLH?rxj?LR-kEGfu^ZT;P)7As72uL1TWrr zL~ww6fpALe8x%}vbN~mq72v&2uQm`(<0o4~IKZvIW$$C0p9DJoiQ`|;#-RZD-_T#& z>kbCNnZP*2>-WYw8hynif+|h~;t6NHhuI(VAH2S=JP1w%IQn#w9e4V?hl@G%&=<&v zewx_~J3jQ#7RZXe0jVC!ngUCsZ@{M&JW5Jv3gkrJp@6A(l{jYd(E3&^5xP`#+h1%%Nz;G*{B z2o(?tREfU%R$IHMfJmTX^bHQa<4k_d!dgHj`il^K+JO{nfy=j>3eh(h?qfQG2up#1 zkL?lEna{Dk2N5;`r|0x;6OQQbLUiS;w7|ujUiRW$>N%J6}55# z1D{kI*-T_jLJ0w<(KoOkgdzdQ(Kiq@2}J@mL_dvvlaMP=OY{wFn}p>8bw)ppWs|T( zpx)>kSTzYt1Zs}HflZT;CBT#M(GN6A`SVjR?`{?TX^1B^UV;!@>XXMbo*;}=Pp7lL znD_h{x~s+832(RIi6UT=Y2wCWA&o;XjEJA|N+|pw>D|AFr)!0qbs(d26Wu%Ua`kyv ze|au9#w`f>T+QRr0008GNklfgc5Qb+^5TF)CLgL7Y6Wo9+Cw>9Hms^n#Ac~NR2vwDarY)qUElNN9m4qz2c2%5h zZ11jTysG3!j^$WZoM-pVZpL}IQj$Ba0={>;QKDKnjn5jQ(*!nX$D4vl-CAAaE`E zd2lin{XL+AT3}i9HIOt#qyk0J*El^_y+j15KuPp9HnxBWI)T#Y>(EVVL;?oU*Pxoz zCDx{X9gIT5SQV=xb0+YV`!{qo0RlQmY~06n%~QlUfY{x9Dq}o{RlM zsDZ0Mo#-!un**qUlR&-bYlxq})t?Jj>;)*Ip9kfu=iUkS0w2E;L|{gSPJxq zYVam!_SgAiZ3M<=s`+^k@~7-{<;z$=PMK@#s;PLuFm}80Whx*(Y{1xktT4gt%9oiy ze~8>e74S_f;G2H{00960=l}}300006Nkl$djA*}hFgn{PsLn+_r?vEO(mvz5Pi;pw2j7sL?V4ch=KSXAf!tLlf~&(8 z=4lvOdv2)ZYLVbsaQA3gd0ozH7VhwcXOv{7v~oW4N2NQq9s!ko2O{0A_$|NlSfZQT!ybv96R aGc$-TIXpLUZxA<7gu&C*&t;ucLK6Tp&-DcW literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-opacity-wrong-target.png b/fixtures/web-first/chromium/svg-marker-client-opacity-wrong-target.png new file mode 100644 index 0000000000000000000000000000000000000000..36eb8b2099251f7a87ab457fa0077562c30a6a19 GIT binary patch literal 771 zcmV+e1N{7nP)0008GNklfgc5Qb+^5TF)CLgL7Y6Wo9+Cw>9Hms^n#Ac~NR2vwDarY)qUElNN9m4qz2c2%5h zZ11jTysG3!j^$WZoM-pVZpL}IQj$Ba0={>;QKDKnjn5jQ(*!nX$D4vl-CAAaE`E zd2lin{XL+AT3}i9HIOt#qyk0J*El^_y+j15KuPp9HnxBWI)T#Y>(EVVL;?oU*Pxoz zCDx{X9gIT5SQV=xb0+YV`!{qo0RlQmY~06n%~QlUfY{x9Dq}o{RlM zsDZ0Mo#-!un**qUlR&-bYlxq})t?Jj>;)*Ip9kfu=iUkS0w2E;L|{gSPJxq zYVam!_SgAiZ3M<=s`+^k@~7-{<;z$=PMK@#s;PLuFm}80Whx*(Y{1xktT4gt%9oiy ze~8>e74S_f;G2H{00960=l}}300006Nkl00080Nklw5~gzL>cKz zn&dq>Z^9ry!f3Pf&oA$j_vEDM_=l&s&fLH?rxj?LR-kEGfu^ZT;P)7As72uL1TWrr zL~ww6fpALe8x%}vbN~mq72v&2uQm`(<0o4~IKZvIW$$C0p9DJoiQ`|;#-RZD-_T#& z>kbCNnZP*2>-WYw8hynif+|h~;t6NHhuI(VAH2S=JP1w%IQn#w9e4V?hl@G%&=<&v zewx_~J3jQ#7RZXe0jVC!ngUCsZ@{M&JW5Jv3gkrJp@6A(l{jYd(E3&^5xP`#+h1%%Nz;G*{B z2o(?tREfU%R$IHMfJmTX^bHQa<4k_d!dgHj`il^K+JO{nfy=j>3eh(h?qfQG2up#1 zkL?lEna{Dk2N5;`r|0x;6OQQbLUiS;w7|ujUiRW$>N%J6}55# z1D{kI*-T_jLJ0w<(KoOkgdzdQ(Kiq@2}J@mL_dvvlaMP=OY{wFn}p>8bw)ppWs|T( zpx)>kSTzYt1Zs}HflZT;CBT#M(GN6A`SVjR?`{?TX^1B^UV;!@>XXMbo*;}=Pp7lL znD_h{x~s+832(RIi6UT=Y2wCWA&o;XjEJA|N+|pw>D|AFr)!0qbs(d26Wu%Ua`kyv ze|au9#w`f>T+QRrX>2{9NGXtiREQ4IcUB}S5N>5rwA-EFIGX}jCb9Pin(q_k}J*WBrb-A~f)-PwE2 z%zXFEy?4ITDv;qQkl|znfD9)q0Ax5>0l-AM%UlQCrQ6FPjX*Gw9(%)KekIg#9K%&Pzq=2M8Y5Fo;Q2$Au#fCbWkj)DhZSSkE;f5+h~U^4S6K+nR` znft4P+5I&XpQ?=p{XL6ItFL}eQ(yh zJ^n`RyYPGAs;@j}aEyO+z}9dFeWC#xujGJr{Vx(Zjh!(BE}@>Cchv+~#eF zN?vt-@l#+TrU_8J@9$y+tQAdyN$uRM4pZ9*2{&+GFbVT2z@qLJzmb40yH79k&it6S z={o%+Rwl}=zNTP`0M#$}YQ!K{ilmRa1KzKr_3OInZRI}P*ZHFFF)#t?0@xdA0jgBb zZH?#xoLLi!+F5+2k@Z5jjpTB^N;Z&;bOCbn23_K8#K4Ewx=t?pBUb;elZB!W^>GZ0 z3^ssdqzd3XG-wy!lit7Xk)m%QLApju+BjBJL5cIw&_0US*O4spq@CZY)(SI$FaMBG)Ykb)(SML4 z;T(#kjoVYX$vKF9C~gKx1Or9Qybs=`qsjju7(nU*)R9Hkv}d@<&JH^~Ac^Vj zkp<~#-Z!L3$^tm6yxZ8xnd$P#BH|ti-(Yc7)r~qxk(2~jvENf9W~EIK&E=8BE%!(T z_3`jAq(DjnEU@J4W(Oyl%Oi`3dqiUF4Ie`aBrQPYf#FBQ^i&qh<&j0iJ>p}i+<#*& zBuP>Nuv259&<<|Lba`Yk%{{WHkhGnilmPmOzn)OJFJ6~N77_P|kELp1@pedpgau%S zMnR}a)a8*y#641!*>-yG^7{3$;u98N%ApZ!(*#`}Sxj?}Fk+{7mASTbuW-GitIDIX z;u8{}s>#2B9hzia9$7@(Bev83tbFb*l@wl^Sb-lq#vtyM)ZUHf7u8<^mAn z8P`H6aF=RUa=(E(%T{Qcj21tq0N$l_c|lvQOD9+>PCmS?*4yzbqzOICT{VQbUX*{- zOLxh-yc{SHsd5x9qlxe~1KG=HF>evwF@Pzcj%05pPxp1O9IZeQoCo2GCI9UN8!}%Wo0RR73*kkno000I_L_t&o0HmsPD@SX1H~;_u M07*qoM6N<$g62!?6951J literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-root-viewbox.png b/fixtures/web-first/chromium/svg-marker-client-root-viewbox.png new file mode 100644 index 0000000000000000000000000000000000000000..970cfd990882a22802cf9d740e42962e221efd2d GIT binary patch literal 1805 zcmV+o2lDudP)sKh;*)7yD@3EY`m?k>pecFr82gAOJDB2y*PeZv)g;m zIla&Sp65BwdAd;; zidrt9IRVrTf|?QF%%qw4nzik%<7?jp712Z@v~T*ezaGwCzKJo`)t3f^p1o79f=VPU z2y(+IgT+R-ZSlLuUf7K9e<6fS&G6B8Kt)s+;7rC$Xv%uj$_*dt=Zxsqpa}eDovF0v zAy5g`V!*ldf-5yMR;QnxRJ8j--IV&_*IQ~Q|7I?|w1)s$4=lj1&j%Gyd8L zo#hLv($7vV^7f{|>r!p|QlSEScnN`dCaY&HsDNSugnfL`l0#1)GCp1!I!iW03UK@y zV`O8nCBXKAmwWC9g;8u32xGQhs>9&M{kzFgm$f9yK0jffg$ziYqc*b?6hu)MFk8D9 zaO3gN@24qI8zSgJeF!kCsm${TD2Q?bR5D6HxEbuo_O_ag)@Zx_y+7+ObcG$ko&u}q zV}U{_Hn6$fyAd6122Oof%4PlA5F=x+=&N{NoIwwpUEU3#5Q@Zr{IaVL=!EO7c=O`~ zpvzlo^)`qDQ)zbz1)%{LTp6U}3R`O?`~xzh$PCc2>$|z}m)#6JU9nB}HY?~@<{2@7C+iKQ_JRP$p@ zDk?e-OzZ}Q2#(E2a5qU9+r>W{5+q>(Qgq&JypqE!Iv47)J_dzx)MtJasvd!14E0n- zYU|9g)VAeVg!XL9D^hI|5};tMXFA@@reGxpD>|SO;p!0zznR~ISR1Fo!d=CRD+j@b zQ>DgANB|mO_4p)oxNEEC6&+BC;AauG2M<&DJq*3pX0V*%1z8N1w`vIlPsys?@t9nEhn%8)r!|7h#mQxZyYv+z^&Eyq;S86hSJv&41B3F5oVrcc~o8#*~ zMIqMVlNPlH-9=J>nA1vJi|Gfye}3e#&L;h<^D0mLczDj2kR*O+eBMez``J4@NRlCS zB_;u!4_fA)ADjBcs`jHr{Jj96THqTBPW4K3@~volYE&JFvyanq5l|bYB(D;dv0Cdb?Hi!r?B6#p1 z=QJ$ag2tbdHlFYIK{WwDVEggL1V{t|lf_QoCIAm|?FP$9bn_i(;e8N!MAZZ^cG$PD z0K>1!*lCCpqN94T^FaZ4ki*Eji|EKn@kBxd#(F_Tj2G>vb6xiNm;tZHS0BU(6U0{n z8VquD;7^<)1 z&~PKqZL6X{W@FBOOsbvKejsiE`~xYP5FkvhtD(i{wk;(A0h1+mabyc3B>+1Gv4S-) zqvyI8G)Qh|f3S$sUVo=vd@ckX<-ES;m|B!*;{7xJq?$0-S+ZO?f`%A zbA02}YAha6LkzkAUs~{$fsXQhRG+{|(gR>LgmMSp9Q6O!(6(!7zO)guKy){Vg~awq zOCt_=9Q~~r?jrc!CcCrIHT=A25X^JibJ5E8@g+LVvCOEc02mEo#&aZB0Tf{?zEV(x zpGRK3J6s?d?zjx12#fMuHZ1(ITVHr2Aq`fFsPh06A``#&JIIf5iTU3Mp!lE3X`Orj vJPo6qe*pjh|Nlx8X|(_V00v1!K~w_(8o0``R`H%$00000NkvXXu0mjfYDGY1 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-stroke-none-width.png b/fixtures/web-first/chromium/svg-marker-client-stroke-none-width.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d98d38d62e71bfb353414f1c9657412afb766e GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=9iA?ZAr*7pT;9mp;2^+yapz&P zBV{@_%Z@m0VH9?rS^03&#RKzC_&(ukh+&+d%}~VZu#Vw`D8o)Bg>VKW!Mumd&z0`q z|0n;t;k|dh$kLbDB~F+!A}Ph9{}%&4&?*1_yX6M|2Qt_|ZenIok6~%*{LNzu6k+gm L^>bP0l+XkKzY|66 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-transform.png b/fixtures/web-first/chromium/svg-marker-client-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..a72b912615a9831f26984f9958ae2d89ac1bf8ea GIT binary patch literal 1730 zcmV;z20i(SP)Oh82iNnA)I@{&iQ zCWCGS)Sw1LBC;hInW7MfiGnc#>(-VMa4j1QWqsXy{7x%mVQt6SdwUzYU((!jAE&+N zf4=j--#zzMfHX&dG-pHrNOML6fHX&Q0USkD*Xh&1V7eYOK>G+l$fVD^jv_<5t8nf# zNHQri1Q2mrP=NMiM?*?^00Qch9SxcazoKxGg#gvNN#C+Y&N z6K7={&7aYCGH>!E^y^0H#Gnls1Ou`z-@a?Y6;KgPBcNsCqwBX#d119*&`Xlco^muA zFY@gbmhOGzgl3%(Tu!SEpdy+@KxN&PGC=?DbP8k13tsE68n@ioS61hKNJMC-?Y$kM zAs-qs04kx03Fa?hV*mn&c;m?m_YZ75r?bg;k`_Nw?@Xu+dMuzKnh21k_k2#VH;FNJ zy1&o3IMOkU!KP3F*_)suNf)5f(!GWnUrphCl`*l}^(FR4I}^niqzI6i<^F`59_7A& zMKir<1SQ>es^@2jPnXp6mVm;jE(h_X1S~ z;HF0*zm6_xjGNxMrtD6L1B!wT{5z;vM^TUs3Zj|-<+YbhA=Bf(fzWU}rWXy00y_db zK=TBddNHsJ6hsvPrkL4Xh*H?}7#yNZZ!IK<%*0S)M3L|+)HB5@=(wgqaL;=62WJi>cRd?>p`xX?1VWq`w6T$<}1}3X+H{z($@PBa`cmFL#fHIEhby*-Xbxk0&Lx_5+!os?%1H=-gpEOA&C0 zr^n;HLfq+bTmn>-xf{c#2TotVenBWT(SV!th9Iv1GZP^WhAlvOt-GF7ucheoF%YYr zpJn%gMszeA+en5}JQ6F%U0wolFf0M4E^6`FNEyg~uzMpdw+i9e<6#L9aR^+>e`1WisA?rj z8C`(Fc6v^C^D%aI;M&*FfHI^IG|$*bhR7eH&MB?yz4>N|!&LQ!qp->xtL`8H?53(U zK0P|gFUb@oGv5KoR=^ zEi8D%oE2$5G9l*s1ra@<*wuP3-FP3qbvs@APnlfDhc&IOCa%JH57T7nbJXEk(T@to zjDVQ@Z^!*^+*hUFQO7(uxg~KOUKTIbBJQ3{}VTd6xWFkaELPt4g4==eRS{ew2m81mMt+$fY0nMaSfn6M%#8r=QMoDGl4DWc~hu`@B+Y z0W^I{N)5O#NP@hdnpVKqWzm~iYCjrPggZ_Ej@>Y~05ae(f8#XdV==fR|1ZFOL4>|X z=(@FgsAKQQ2pA3{{}_QZXG8!SXessI20 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-client-use-inherited.png b/fixtures/web-first/chromium/svg-marker-client-use-inherited.png new file mode 100644 index 0000000000000000000000000000000000000000..a3dc5a649bee0b3aab446290716678dea8baa0a2 GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=S3O-ELn`LHxo*wp=*Zyq(0PrM zlGN!946`rXXzfb%bUEr~B9$#5l2?1z{m`Ay^;bnMpE+q`l>Fq_2~~z7&W0E!h3O19 znXJ3dg*|^=vUN$bAZs;yMO~Y?T+Z|Dhg;j)gce+~IC6n0WFOx}%L7Tp_dhBxe_R!B zywvPS%e$_7uD_Ho9OwI?-m8CbAxp+zo@sImz8=zcz0X*CBA#I}2P)$ek1tF4nBxsq8~KqPzkPc>$;(=-7*+%)vovWoVzu9aecq%wxRuHTkp#`Pw)Ak zbDneWznuUnjsPi6S^!9K(gHw=lNJCJ#A&o%bgr^K3P}V31wjGm^kygcq0w~R-U$gN zQ33==xDbgJAV9)|NEQJCC@w_GuL5dF0l@teel%MVfG7@G_muP&|}#}SuY7Mg;UyO{xvtPCq_u5#Cc0&o`~_r+TVY|xRZ zJ+j;V{>t0I%Cd7fKlVr^0ge{cjVuGsa2G(Q_O`Gk4k!25-EG;Qhb!-B&pu5ly~OGW ze(@M^hN}RUs^Ld4v$KTf^9h>jNb8BJsXCZ1s?0U+^}rE+0yI;BfH?RK>P|Qj7XU zyV&f017t)t0ZcWcg?KOD$hIj6m2iPff?QxMDW((~_E~C%^FStK6F?*S+Hme?;Cwyh zXg7QZGQ(nG<3D5X0iyq7kO^4?D6SfQ7L(MIxNeV5OltQ6CnkIu4eUK|g()*vd!7av zh+BYy3aX{xKgqW7rlL`DcsMcJJtCBzVDAla61M;vmFEz1a^|}{yqIy1z^E47%@8MX z2~bq)UX2&i5X|M_MZi5m;rFs@fH;Ut0Krdfl7kb(<>5uZJ;F^5y9S7Zm<7Nj^b+>K zrf@C~FJ{~$vdL2E*$y!hlK{-A!H1N$ruIU+JiM6W9tq;{Snc$f1R#_C!}vEVQkRDp z0r!Yq%Q7zDbba{9YaF0-go%8MV{@l`+!_|*k0OrszdplZ}hZh0& zNV43g?espQb)PfO`nO1+B3%8b1h75*Ju~D7QavykI}({pNh)6mixxm%|3>=3O5T}9U(=CNPvFhmXZ;* z{><{pvAfRXm;T!DZG&PTU4_GGEIp1bt;Y$lpidyGyt=}^3*KP$Lz)YHS*7<7Eos50 zux;3Q>Da~_U2TvgICHH#@Y-I%p&zszTCXk>UxEs-U=F~bM9QE#xQb(B+w5~D@Aa#Y zB$x*6AE~GxhdsR*tc+hZnHws`|CDp(Z7J?20`@X_Y@zuqX8yhEe_w)v(MoZQ^+JZ zM*tS2z-8Csvt3X-%d4FjgCwQE)A6Tiz`mV;pMB@Z$&OKxMm_=pm$nx>}Xrr)-OCi#Qu%m=vZnoKR)J#dJ7&EOB4| z^5-*ot+dKypMB5&uP40zT~W1+x>%Ny!$viiV8$+0#aTQ7e}tXvS@}#dPKst|9hl|5 zp_3u{Si)7tZ8CdSRB^pgtY>^8#ITdaL632QHvl?ZrSCkj&)qiael?s_xy{`{<_N&akXs)--o>q!~RM+{pWbK{=me!8MQ3C z`7cy`2>l-ZApR$(5zopr0CsR<0{{R3 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-default-viewport-clip.png b/fixtures/web-first/chromium/svg-marker-default-viewport-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..d0bfa14e858998bf15ded3fe45f6ca9a216b19a6 GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=r#xL8Ln`LHxxSIN*+7KtA@?b% zz-uQ@M5>=s=+w{&WY{Fg`t5JLD`RWFda~LRwI@OhJ6Rm`7$bP0l+XkKchhAe literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-display-none-resource.png b/fixtures/web-first/chromium/svg-marker-display-none-resource.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b7007f1cf82ce44f5d7e087ef59b18d9dec765 GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Cp=voLn`LHxwe=0kbwl-!^i18 zOH%kx={O!$$!Qf3l4NC(b(RQRvRGl^!JXeTbs~!%PX2S!`^mHustiS(4KYj#(;09x zud?qjyZiKBNwiv2z|QE~Q@=-X{pI3&`}jt`rTuJ%=8cMO>^I)bZ?!M7du#WuUfRi? z<*R?fSsx$SrmO5Tsy;e?^?wlllT!(w6<{|k=l{UO!0`Y76_u1cpr6=4eqv^*JZjh= Tb0x$PD8k_B>gTe~DWM4frDS2) literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-duplicate-id-first.png b/fixtures/web-first/chromium/svg-marker-duplicate-id-first.png new file mode 100644 index 0000000000000000000000000000000000000000..057e05751ed2e6774db43d5cac1d070de5201669 GIT binary patch literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=n><|{Ln`LHxwet_P=LUJfaU~F zm2Zh5DrO1%0=m4(0veYWx|nqS-E}|eSNV3=yzUsr3EB)rtPblKPKYw>WKsxcc*50y zDk#hS)n;BEPt3RX-+zACWj7=9uv0BF&*JB`c|BWMBxZ{w_c9))d$0!0`+UHx3v IIVCg!0PM+FdH?_b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-ellipse-drop.png b/fixtures/web-first/chromium/svg-marker-ellipse-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..9417faa02b1ca51ef09a8bb97d63ef024d10617d GIT binary patch literal 1387 zcmV-x1(f=UP)?~cHUSOFmqL{YsR!~9og~agSgV98VzIkbhNwny50n8BT zgx@$AE?j0F{{k;f5v`p1wWsE)e5QHV%v!{3J*2!14QyOg&7WM}G2y8? zbn$3i3MxR^0$3f#+^c}Jn>$`Tf0sshdKoZH97r5r^9PhcbL3j12KncJ%yvNBvQYjE zkBq}Y^BpW#h_VC-btYftA^vV5&2Q&JM0_*WqyGd;KLcR00r$sy z1MfkJlq5i4&qPgq4nN|~w^|JWIzym!C!+oz;TF)+G4T)yc$>w3!m6KOXjaqPt{lZ# zD3YQD2u3D12{48@YqNX<&`}DD5$|yw6?) zlylXVT4AMW>PT(W1P$?MY~eOt7Y1&+ctXFdnqPe*#LJsTbDvtB2Z|e}wspBXFOhA? z8dZ<*cQDQ(tRgRcBHOB(Us-@qZF=|oEKDi$ruNu)-|FjN6E2^qOPdIf^CWzYlUqYI z=`Pj0Y67sLyn}m9Mokj?e(CrB4mKkZZHV(I=$3KbdA`CgO6l`f5+GQc-pRo_IrI~~ zYx=>a#4~|DYvYC36zWX1tK?A<0KwcLLjrMtQv`Npew%z?A=RpqMeP$n49dVn-x;tw zD6BT}_-|HuTulH1OMt@O-OkJk|D+W`s-OU9oycH)WXcaVN93n{^D$KFo7x;e5SO7B z=0ad|d}!QjMR=p8^1SLkAR&J=AlnN-CRqaTOdVHwUP%Bh?DK`_POv!wDI!A>^qI=@ zYG(o72({FvczuPtxadb?(T1~NL+*(rpZ3U?MQ|n7tFKpCveiBTUSCas_pLbL$>0l_ z2Cx~8UCDI!mI+z6Cd_lPEqqS-CW|Y!{oEVK6jMfNHo-3PhK%F~JQBh( zx$Gt}fd2Tj9xlaXUY1d`B|PyGA39}2dJ^RViPXY^CHb&dTOi=CtV=A?skBMamIKj$dl zOY^rq#!k}nh+;IC@sHeI&e7Wp_YV@_a9jbv;kW{T!*K-whvNzW4#yP$9F8jhI2>01 ta5(<}00960{=T$g00006Nkl%8}FaL zUKe-!kV`CsWo^UC`rnK72K5Gq}65G%eIhJ_!_I@O1Ta JS?83{1OSx8MyUV* literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-height-percentage.png b/fixtures/web-first/chromium/svg-marker-height-percentage.png new file mode 100644 index 0000000000000000000000000000000000000000..429db77e0d4652330cf1df08431b8ae1b4fd0636 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=?Vc`^I)bZ?!M7du#WuUfRi? z<*R?fSsx$SrmO5Tsy;e?^?wlllT!(w6<{|k=l{UO!0`Y76_u1cpr6=4eqv^*JZjh= Tb0x$PD8k_B>gTe~DWM4frDS2) literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-line-zero-length.png b/fixtures/web-first/chromium/svg-marker-line-zero-length.png new file mode 100644 index 0000000000000000000000000000000000000000..973ecdac7c06579a29c00a90864953fabe459ec4 GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=dp%toLn`LHxpt8EkbwZ}MNb#; zh6S?&_;fEYzP-R}Sa?GQU*xrUy>1m#D&s2l_igMq zPCvN&L3M}uPfjI#rh~0t&i{d(f#LuEluOyxK##D2Ji^S7_S$mJuchl$fg%i^u6{1- HoD!M}HyRl{M6Zto5btERjKFXRNfOPm&I@>JgU7v>26^X{Omxl3M043olih7+m` zMVt*dnc>n8%(VUbBR4KueCwR~is*v*;!l4^Pn+YuVXpo6bgLzAYkHYOc8M)|TNBT| zB1X--tlRYZ%SrWCa%S@X&3PB@XZh-$;Nu*-mqmO3xp3(P>!sIUk!##{+FM{Ji-R8H z1aF2XLJZiMmbDGyKp+19FShw-JCMN!@+vcf-K*$^Z6e!Dfg%i^u6{1-oD!M<5>ab+ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-missing-reference.png b/fixtures/web-first/chromium/svg-marker-missing-reference.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-none-reset.png b/fixtures/web-first/chromium/svg-marker-none-reset.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-auto-start-reverse.png b/fixtures/web-first/chromium/svg-marker-orient-auto-start-reverse.png new file mode 100644 index 0000000000000000000000000000000000000000..2803277b3c7fc48fbb40a3aefd1d4b2a05759f09 GIT binary patch literal 334 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU}W}maSW-L^X6J0SCfN;>%-)& zJ&V?U5qPK;<-KK^nV{g4psrhPQOul*ZHxJP3*LVZ3p`hUbOfE8Wkcmp9dGcp60Ri{e!Ld{xh8wP%$g{a7)7c%l=Q36$rTw=x9H? q1KbP@|Nq+^f4vhJDr}(OU}o6=(JyGtt2!~D2!p4qpUXO@geCyoxr?O$ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-degrees.png b/fixtures/web-first/chromium/svg-marker-orient-degrees.png new file mode 100644 index 0000000000000000000000000000000000000000..37feb001bbeb31816e89e4b0251a3cc46f8a8c82 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU}W@kaSW-L^XA&da|Z)t+CDD- zyN_GIxYPAeXhN}k-a*zGIq#0Q-A&r$bwfqZ@u~d;p@lxTzW>$DJ?}aF`4(GW!Rbop z7zLg)bQCijv1E88!%)cAV8QM%hgktl`2UnU>kMPmIQOO(M;=>MI^%o%-S}VX5BSyY zpNXAn=kVDe{+O42-|>hudw2hT9KOKTD(%mVkC~@)Z1+lQZ~C=RNdBPj-qXvj?Ti(V z{W~E*@b|F|^?ngYDrXr^;1)=7?|8ghzdr8e_JeyCy|0a1ImdKI;Vst0H%A0dH?_b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-direct-alias.png b/fixtures/web-first/chromium/svg-marker-orient-direct-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..b27f199be5a42afc067026ea8eaa4c9192e48d07 GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV2to|aSW-L^X9stACsd*+rx4< z4hbRd=3b{I4_U%WOk8Z5Mb$6fJ2cBWBROf2`Ujq{jzyiVC&k>F8fB+zv~u!pIQ!)H z{FKeRH(%ZNvwYImj?W#N8StTQy#os_>P=q!f+@$>TU>s(y7d#4T?_{|evkBA&*bh} zB=At~!K61YcwS$%v+U4!;5hl`Nx|O4wbkd^l;$=ZRZTzt^|73EhhD?)*$u5Czx>(n zie-1@Gwt~;uJBu=>s;aIpw}jm&R>K-?DjA{dVk;B?W@;l9AdDG{CB`~f34>1mTP9( z9eM{!tg7R;$t6sh%l7r+;TfVL9X=l1$>#TeiG6Q$mwzF@?_TVtwiiqRKfMmEu{n9~ z&o0AV_NmT%h2N}uEVvtpK|ADuA^iXUmsDkUAcGAQ#>@=r9d1YBx+8uAMHoC?{an^L HB{Ts5P*JU+ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-gradians.png b/fixtures/web-first/chromium/svg-marker-orient-gradians.png new file mode 100644 index 0000000000000000000000000000000000000000..37feb001bbeb31816e89e4b0251a3cc46f8a8c82 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU}W@kaSW-L^XA&da|Z)t+CDD- zyN_GIxYPAeXhN}k-a*zGIq#0Q-A&r$bwfqZ@u~d;p@lxTzW>$DJ?}aF`4(GW!Rbop z7zLg)bQCijv1E88!%)cAV8QM%hgktl`2UnU>kMPmIQOO(M;=>MI^%o%-S}VX5BSyY zpNXAn=kVDe{+O42-|>hudw2hT9KOKTD(%mVkC~@)Z1+lQZ~C=RNdBPj-qXvj?Ti(V z{W~E*@b|F|^?ngYDrXr^;1)=7?|8ghzdr8e_JeyCy|0a1ImdKI;Vst0H%A0dH?_b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-invalid.png b/fixtures/web-first/chromium/svg-marker-orient-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..48542a5d759da35bb3c37bf5a3513b70dc46cbac GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=`#oJ8Ln`LHxxSJ2kb!{PL-!*h z9v!X{3fdbbZ6j59qZJk$F!fyUaqs`tcYb+(d&%~M?THY>P8J6}#tGgGxR?!nXODk0 zlRsu`f8*lD@`mTnEj~Ud+7+)Jwn}J0r0k-ohP1+tRgAOND}D)1@b~SW@356&wf#XY zMzj8d_n33`Rb2im@}vC?(+O3EBF=^wCWYw?IGB5ZKKTE?NBRNCk8B{XFf;5pVDw|5 S>{b?_2!p4qpUXO@geCy^Vql&C literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-radians.png b/fixtures/web-first/chromium/svg-marker-orient-radians.png new file mode 100644 index 0000000000000000000000000000000000000000..37feb001bbeb31816e89e4b0251a3cc46f8a8c82 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU}W@kaSW-L^XA&da|Z)t+CDD- zyN_GIxYPAeXhN}k-a*zGIq#0Q-A&r$bwfqZ@u~d;p@lxTzW>$DJ?}aF`4(GW!Rbop z7zLg)bQCijv1E88!%)cAV8QM%hgktl`2UnU>kMPmIQOO(M;=>MI^%o%-S}VX5BSyY zpNXAn=kVDe{+O42-|>hudw2hT9KOKTD(%mVkC~@)Z1+lQZ~C=RNdBPj-qXvj?Ti(V z{W~E*@b|F|^?ngYDrXr^;1)=7?|8ghzdr8e_JeyCy|0a1ImdKI;Vst0H%A0dH?_b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-turn.png b/fixtures/web-first/chromium/svg-marker-orient-turn.png new file mode 100644 index 0000000000000000000000000000000000000000..37feb001bbeb31816e89e4b0251a3cc46f8a8c82 GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU}W@kaSW-L^XA&da|Z)t+CDD- zyN_GIxYPAeXhN}k-a*zGIq#0Q-A&r$bwfqZ@u~d;p@lxTzW>$DJ?}aF`4(GW!Rbop z7zLg)bQCijv1E88!%)cAV8QM%hgktl`2UnU>kMPmIQOO(M;=>MI^%o%-S}VX5BSyY zpNXAn=kVDe{+O42-|>hudw2hT9KOKTD(%mVkC~@)Z1+lQZ~C=RNdBPj-qXvj?Ti(V z{W~E*@b|F|^?ngYDrXr^;1)=7?|8ghzdr8e_JeyCy|0a1ImdKI;Vst0H%A0dH?_b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-orient-whitespace-drop.png b/fixtures/web-first/chromium/svg-marker-orient-whitespace-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..84e8621ff813a5b0ad5cc98d16d244f61b2ec415 GIT binary patch literal 1300 zcmV+v1?&2WP)>!nzI(pfF5~T{IaO69k0> z@g=&h2BUsdKS`pACNAg)4N(^pUFR}U64#bYhHTjN9-rrq8+2J|d+$BB4Y$8+&3!rN zx##@vJum&=Qw<(RgU6{00FP4_03N3<09<5zb;noZt2*vM4I$tnB!IlMIfMcdt!GXQ zp~_TC0F$Zv65S||e{HOtb46H%cAK1F>)cz0QGF25|eDSJ@RAkGk zp_O+&C?$GO1p%tk2k;$Fg*&IdS+=%~_nZ9}(?TEs9a~3IAy-9kwGYsRcJi5ZeUrU+ zy?`H<-}LMs$Cu^!A5!%3c`4-K(2j{Ea1mDpc;NnbzxzITbATTAZ$}10Z<)u_sTFVX zF(Jgtz>M}1T*Ng2!rfqcI z&l|^te4P^bI4xYl6#>Fsne9}uJLo=VC!5e$EZtv?NN^fX8;)gq-~!48xbCrYL8{Qp zb9-WU=8bF47fZ|MsD11)QUV9tx1Vi+Vw4Ne?4LQrLmV~kFGiA~4@#v^4~E{CQl8{I zT0uVy#V8dZ63g99Jv(=J`MlQ9U2Z0fCAl#kn(4c<`iK$twIXX75k+Y(wAz7(@?3W{aKG-BiVNIZ8v9KtaH8uWaU+I}Oo*W>BI z;MZ^%j!Bh&kFNo}@HQO65dosH+-B;e?xS&ga(cFDFBCDIZ|vo3&7I$;LEGQb9w1)xsoak>NROSwF% zxa=N@^^vaZBd~}i0obXbFDb9g?OoaBQN{n{0u>9u4h;==PoP4VM-`@fL?Z04Vy9Ou06R2nZ&&K_sKRuQ49neuoxY#|#n$pf zV%eSS&{%bORPp&|kKVgB`_ly5>Csr`$x8Ar=m;&emWS=_#r}(9Y);s5c~r2_OV@H| zq@@Hf8S20N>A#KF*yQr0lc9%DL(DHPzjkk(I zs$cv-;xhqOfj4Vhiv=qwj_{{Y%C%xNy;f~&B_m^<9 z%+Bu3{GOTF*#QL>M}ft$1;FCi0$_10FTjZfz}qqICYovy5Gx6|41`J{u#_Mw<#_=b z;$(=={mJJd{QHm4i-Vz_G8K7-kSFRpM%Ixc?%+nQ=dZa2Ss;Ku{`i7_J`DA^4ks6By7s@9 zDMd5e_`aLD!U})KKb+hX3S9ar=E*z-2x{<;zQ|E|I(D~rIqJ`ZyFDS~fm)xs+zGLr zMcmC2_-nKcOXMD8UhU+>V2$i4_nuDK^u<+TnA79czvTA zK>GgJ6Jj&ZMQ^8jZQ25A9#)qYX<{Hc@jyJ@-|cxF1w_41eL^AZkQ1^pqj=MO1gAX& z7HQ%o*?2a5xX>Ce2?yM-$kiqf&l26KZ%g;Ov;;JCj;s)Zw#a8MDf9>m6~d$EQhK27 zA@z!ME=fy(&~>@Cm%}B{|EB9_6eyu?PoG>W%elazjS8MAn&imR8HxYNN7-0-XBfT{7i` zPY_*n7A7ImOOrfjeJ&^N5=^%ml{FN=4ZmKdyeaV=Ob2}{Kl#ypolz;nP0_V~eZOLS zQ7MjGa#aK-;kT-$L(%QH&)R3RwxYUet2~g4xXUBK#`yAvJ>cGfeOGS@y~kTTFpU5Q z@O&ock_tFp)zo<9_(0jJ6TjevzMYjY3DuF{AsOkp`7%$Qo133hfEH(KwfaLROhlqc zC+isr1<2^fZ)-b9Un}!q66W@TWYL!-6rd%rr5TK^LL@%oM)SgX`vEf2I^kfV!ivDH z$xId^@`)hRxifz}=RF2xq?7HE#Cky^OoAhgUYobS4Vne}{{vugYyq%1wg6ZhTL3JM zEdUnB766N53xLJ31;FCi0$_1$0kAl>09YJb04$Cz02asc0{#U60RR6>4=Td|000I_ cL_t&o0G`30{Z2(0`Tzg`07*qoM6N<$f(t568vpLzIcANRs}rrP%egv(tiIl6Q9QYrBOleR)aKERXj-5bk>~ z_uhBz_ndp~eJ_!~;Yi?cTmiu0xB`H~aRmT};|c%{#}xn^jw=8-99IBvIF1)Ufg%y; zoNgtAAOh6#X^r?GkSKuwC%G^_1TevMzKex@r=SQRtP%ILQQ4IWt%dK<#v^P+gpIkh zXCdZ*XaEV1M^*0`_!twg2CRtyfzFE|F5X2%-?fWGziVWh*j*5B~>j|!R|MjNyH=*=KJna1h3QR!-sPCNK#!2Nv9>6wn&R!hk@IC=L z4#fLB<2m)UZ}tu>(_4|~S?+HaEOvNam*8N$$M+Kyh=K{w5So6SJ9`gBVkH_SkPpOq zJ!hc+1VYnKvJgiA(a)mls8d(8ulfiSfYl4oO;X@5?IZTmO9ZrlARVUlo_AtLaRCa5 zzk}AP^30no?jg3ij0ld!dc6mD2n4HORRY|$k2X}O+9@9VZZ_yLC#*fuKJR&0LmFSu zwiD7>kyJT1ocTB5duHlWoQFKgTY#I*u;!{XlT!F$&M^mB&H z%b9rz(718tZ7v?-oBo&3m0&zv9fKkx?s-L0cM2QW5!(YHtsC+nXVn@)vq7ZP5YIu> z0iWiz`3ty()W4|hm5EMp4x*G!yGA0_L%FoiDFU{%U-lp+KNo}N8-{Q7;041y-hN&g zBElwFg3tUNmp13pJ|_Xn)&IMBcXvAvJ_;MW2cblmSA1*>jR@NmDfw0|Z7oj$p{yb$ zCWX*M(u1)+&+kw|7%PK}Kk|x{Na||MXr%HVEA1>tfFdWmSmAQcwc%15yg;lByd*}1 zjjEJ<#cDfC0*GB07e=QER1ZUm!h?Tbl^NDgf8vKRzIr%l%k+0`9Y_DwNGBfLD(p46@$}**=lDMfHxRM zCpyyiU>ne|l_Oziwwl=m05m$$L9vYh1iE1>zg~MJTU}ED>N{rd<6bH_v&@WyE62bl zEIyo(#u9k_9a@uFT~h&MDYZC<5YBdaOU{U8dS!grfh zGZld6P}R~=(VzEM4e|pU7*4D-t7aYnfQteU&4Jw@*|7P#6EkD_(^zooXaBrf4i~DL zEInD30;9DpgU0U&Up>EkX)n~Z82cCA%AfO3oNi99ZyEv5jht*~kMXNNDzi-l$o!%{ zm+w~GE05Q01a5%02;BZ_4 wz~MMvfU5ui0RR8xaSo>d000I_L_t&o07O2%0fC_3J^%m!07*qoM6N<$f)nL{yZ`_I literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-move-only.png b/fixtures/web-first/chromium/svg-marker-path-move-only.png new file mode 100644 index 0000000000000000000000000000000000000000..c6cb5fb0f9100a74bba8b30de7b6f67ff19e34e1 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=TRmMILn`LHxwet_kbwm2LuCWE zP28#1lC-UixH!HUcs&&T6TR!Z>AKoU^Y8tOiD7fkgAWXwWA;efHEllW zFR+uvL632QH^UPl2JB4B+6H@|yZ`@>D*99cWUzrez|3%Ty{_Gnc^b|@5e83JKbLh* G2~7YT0ay$G literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-multiple-subpaths.png b/fixtures/web-first/chromium/svg-marker-path-multiple-subpaths.png new file mode 100644 index 0000000000000000000000000000000000000000..a0d710943ffcb56b9c6c6ad312d1bb2a22c7bdaa GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV07?waSW-L^XBGWufqW{Z6BW> z@sNoP+Qs#1DMwk+8fW$O2ablOi?2U$lDlB_gQYJXM7??Rg8i(a>x2fuATPy}zyGye zkdw-g=WCx--?W3Jkgvgl-C+*1!Z}8P=L{Xi3`Z;(9z9`RQrfNmGh;?d_iG7@sD1ns zk3UL&os@4k_u_#Ec_Oc`#j9U-IDLhU!}_RirR@K@+Xt4!+}4Yqd&xNSZ;?&hzgMfy zN6dM{$FKi#=9ZW2kNqcJ%Y9h-S6IPj%JjlgRqm62Z@+oj#m4rgJZ`nkWbl11WNC6w^HX}mb0(-?AUa`QRF{7s&A{;gf5tY}8NeWA1BE3sg9x|j@k6y2 R9|1)eJYD@<);T3K0RU7)ruqN? literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-quadratic-authored-vertex.png b/fixtures/web-first/chromium/svg-marker-path-quadratic-authored-vertex.png new file mode 100644 index 0000000000000000000000000000000000000000..087926ef334436ced21bd77e14ecdb3e9817534b GIT binary patch literal 1390 zcmV-!1(EuRP)+*V+Z@+!qQUAgiMuPq! z&M{?_#}+k93?VU05RFck%*7FB;$uP!EoBR#WjrK|(%yUg9mXGmqx5lGy50Lpnx1=m ze)pc=_x#T9{O$z`3WtKiQ3U{nqY3~DM->1Rjw%2s98~~NIH~}ka8v<6;iv+D!cheP zg`)}p3PW_%7&CaMA$ER(q!@SVil?B&M{U>d#q1gOwOBw^eqC12y` zwRCzb191z0cmVspg3jv}`!m)n679>w)>J1%iKq!+G>_Nd#Ty7AiRg+4BzVuqGMXLj zWImAH)+)9kNt`fKVDI8#8Ur>55`Dwgrau8uKz;#g88wZ#ThQ?eoN)=ydhU?4OKWuiqX(!%gLyOS|Wjr$SQ!Td_qSOrA}<_LFXrd(Wh;#=@%dZ zjF#(<6A-&`pvf3XeQaKMK2$LSA)5eMWh^Oav-<)N@(?=jBuu+|ur*}}WWZo{-zNa{ zVKijpwSS!x^d;At$sEXlJas@B6B2cvvrEqZL4|}Oxt!+=w`Tn9m)IkiMX%tDJ-os1 z`HJ5YWIz@H@;AHB;J|ia!m{~AP02vB&I2;zXwMv*^Cy(x*5JTM*lp6ezW^DCSOBBh zeFA6xIlktl1F{9Xh|naMoZ+7k8Q?MvLbqh=8yKz-=lI!@iNXB?dKvZTgE4BWx|{KjY9}=WWWQ z*iz7O1>{~iwtW=3rhf_&U2J! zV_b`)q9D8o>x56z9K-@?43lD^$>OPnFbG*SQ-!+_tuEn*Ng{b{s9paHtRsfQ<2waP z&H-PEL@2OZ(|!-dKJ*A6tOFJ2!Zoh9^PJ)K%%5;f$8k&oVPYGENJs+YSB$@knRh!6 zeZQ!G2jYb31bBOCDHP?K-PNJk1}%W0a&`;$uH$V6q-L$Bx>(|F%A92sUgK??ocenX z&7=ow8?*odyMbFC*~FjG?N)sk#2NOM%rp2R<$H{Rlq8L}F<85x1TdK=igD4ufX?5b zerka@#YeT4n*|b-8!O#Ag0TrQ0;HvqlQayR(qDx*#o3ZFjBDUS-lh^;{B~yT>v3e2 zUS7>+O#w_6_cMlBS0R6$HK)-xK)mA>j`FeA_kU30%-)CT3Q7ZY5BOG?!4B=QKRY@e zoEm`Y;<`Gyp24HZcR!YQ!{W=6;8-;RMzZ%Cf7`Zg;K1m&sR9%IY@_}oh;y9TrMw6# zS$Q6Ff`2ueTMB^d21`uoa;op2Jbs+AFf(R5M!XQ`NP@SBNzk1Rjw%2s98~~N wIH~}kaQ+1V0RR6;$&|zZ000I_L_t&o0H6xPQ3@pzQUCw|07*qoM6N<$f`R9Mg8%>k literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-repeated-close.png b/fixtures/web-first/chromium/svg-marker-path-repeated-close.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8ea080c33839a81904a06d349ea0d0b31496ca GIT binary patch literal 703 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU@Gx+aSW-L^JbQBws4?G+kN9% zSz_B3xq6+PuKn_qy}&~4$?uM=-CA9$&e8Nvq)S2X^}YU5aXq8Lr5sK52i8tbnXo`( zTh>OCXBBEM9ejGr)XuqAy<R!xVd|1Vhg|1RZIT&U11{Zp&jz4G>!vKMI! zW2=9>JaXyFb)EHHr``sg-q+VrSTwa%X_vtd>1K1UwZ95m6him}vM2|$<*w(H~GA7ONEG{gg(2#CWTW~&P`T(8agJdERSAt|7Y~Q^;<=`-FU6xugoW$E>#n3QMFUWCn!Y)QD!H0RDwZ;uIkve)~UE*DE#b(g(3 z{fEtDQ7yrD-VXU3lfI^;H2)LXp<14G%)6(f-KhVrmQiFD0!_Nm#6SS+D9X~x1qV=r#(_sphgUoH$h42^VLl`2lqSmEP2E! zs9zv+XjP4la+;9ngp>z%og5y{59~H_c?3VOd$rm@$=dn96leXNuKo^}?Z@0Y1C$S> zN3>Wm+U?^IEZ{RtENfqg68Q^czB1J@GBEuAKcnG=HZU2ofzlr{1K*JYUUOf>0%aH& NJYD@<);T3K0RU_;ERp~K literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-repeated-moves.png b/fixtures/web-first/chromium/svg-marker-path-repeated-moves.png new file mode 100644 index 0000000000000000000000000000000000000000..7e70df74f8cc1783fe5fe9cd8a0f521ff35ca148 GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVC3|4aSW-L^XB?)UM53zopr E0I*ko0{{R3 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path-valid-prefix.png b/fixtures/web-first/chromium/svg-marker-path-valid-prefix.png new file mode 100644 index 0000000000000000000000000000000000000000..30590b07db1479319aa5ac20a65d80696cb6c499 GIT binary patch literal 700 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU@G)^V*EI52OpD)z$58~Q-BaY zZg+c75r5_BpKtTTFMr(gCz(6K`l`*R?Oy(ld)~FZb-&Q#cQ;z;(3=y!k1kg}JyylC z>s-v{_^oygFBot9V(sOabFF*9y<2xKS00g`U-BmVaogNbJ7Xp*^{Dg{%bxR`xqQw{ z%kbI)PRFymSKlg&->SP+zO4S&id|nWU;5t55UQDbe!=A*D@@MuFBjZs#&jq2e`xFC z)vJ!4&a7N?Dol5EpY5|lzwR8>i(DTXzW)98azC33ZVo+{qyMa7%h?wF`Q3)2=O1m5 z_e*W=@d%C|qiui~Pc#`4it?!ynk2@k%BtyGnODm_X*ALSpmJoSI$j^6YFVdQ&MBb@0Nm|6$^ZZW literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-path.png b/fixtures/web-first/chromium/svg-marker-path.png new file mode 100644 index 0000000000000000000000000000000000000000..5490457bc4b069399e44f9e51e159460436e822c GIT binary patch literal 857 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVCM65aSW-L^Jb=hwy>i_`~90L zSC(A7v})yzC7wCTdn-O&a^^qU(dqR;UWuzJDBwbKyu*hp-4A?iMOgf~n!2Vx5s2C% z#xEzbH-c%InqbsXXX#l>Jj>>A_N;NdBZpD zC&VymF-yaf0`Uo7k3TQ^_rv512ZzOGnU3VDBNESK8OoV|KC$6&xgq4hv8;xKwSw5#K1XamczW--pvXNieoh_}L*?&jafrHxn-EL${ z{aIc9=FQoYGd~<(*vS*wo4DlqeJT6H%z3sAY=`}7AKq)9dExSflZUGBKY5tH`JU*n zxF1=CHyX{w1Qrxt;@PxfnXj(x&0q$;0>J}^CSQ|3)L>ltWMxb>Vq*i~E>!>Dv9xM!+^ zs)TmpP5J2mc0#-FovV@M6cC;F(VjD9{YHUF7A?wzad?VEd-yd9T{92d3=*f*j&%<9U`z6sao`osm}`iYd>3lVc2q}`DaQ^_{M$HJ9hY#O^cuKm@TvI zT-WK!V`pTRRWa<{bAiu0e*5i|iqFaQhk{+F3VU3xS-kT}^IE(7Us@l+nKUi4WVdlH zd(YhT^NQam_az+$b)2_3-ubsx@qVq4QE}&FI2p*~P&9>Mf{ZEy0q%2tV7~bO|Lui- gaUg>Ylrxwa9?pK9xb8;cX`l#$r>mdKI;Vst0P7fi3;+NC literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-polygon-one-point.png b/fixtures/web-first/chromium/svg-marker-polygon-one-point.png new file mode 100644 index 0000000000000000000000000000000000000000..973ecdac7c06579a29c00a90864953fabe459ec4 GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=dp%toLn`LHxpt8EkbwZ}MNb#; zh6S?&_;fEYzP-R}Sa?GQU*xrUy>1m#D&s2l_igMq zPCvN&L3M}uPfjI#rh~0t&i{d(f#LuEluOyxK##D2Ji^S7_S$mJuchl$fg%i^u6{1- HoD!ManAEs8cf{IlXKO%@91T7d} zYC?p@P<=cGHF$-QAS7X&}3kdwb3#yKK@Xxx0Jk&ce*^WoGW2 zb3gub?>%Q{)&QSlfX@j80G|^G06r%W0DO*i0ivPEi3_1fBNPb?G&5Ee5p)y+22Fs@ zs_}tA@DR_I0AtX|8Jsv(+kD!~8R6LsSd3RQ!uOk?1s*e0ejj{Q_ZSSXYj|m-|BfNh zjAsJSzy~(%`r+8dofEhpbl|$^!QyS88SQ(($?#KucWvEQKV06Bn6<1rDl={SEbfXG z9N2-gy&JkgBU%O6{LIvymY5#JW>*7;JtKXU-%xpddu$ILzQ(O^%4WBP&-JYS4>X~5 z6EyjBHw_-A`(S$T#7J-D5h|m5b2j?WCP26?@d#t=aRFk+p0!#R-={LVM>b>&n$RLZ zO^YZ4#(U>4JEQ&O(Zx1SnSl443$%57G@wNQ(~NiGW)s2zo*BJT`SDVAqgN|GA{(+L zAJBm70)#sfTbO`X=v){^3w+0Bmo&zEjtxRSp){kUbmNP-VC&Vqx5PkEPH;wtSWakD{C)Ev#Q7%AgZQ^b0?&g(ZzAWk7 zfa*-HNxTD!Q7S-fN9-QlwqB$26=S|E*|`A|2j|*Qhf)EIS{S##NSXE^JQ6> z8&DteCvO32S}xV#kM`H;Tv^PQWkGHLZ#sR+le`76OuLT(D^(ryWy#JBs4tKwc?nS0 z7T=4TP@`KhUzV)*293wIJa;@AYWfp$L|TCJcRk!p$J%|9>kQzZIE607d|9${gNd?= zofwT8=;JF2@uO@V(gFy>Ix-e&`ep64sYCP?$L*LeOFB0&1fq;UalTLx?|(SKy*1j>5rqhrY3P(mEUkf2#yVz;4#rqr5 zt-kQnACM!`0^quz!s(T&89%E0#iR=E&SUU|_J4>J8^K>?!RG`5fX@j80G|^G06r%W z0DMj$0Qj6h0Ps220RRC1|4bXreEAKoU^Y8tOiD7fkgAWXwWA;efHEllW zFR+uvL632QH^UPl2JB4B+6H@|yZ`@>D*99cWUzrez|3%Ty{_Gnc^b|@5e83JKbLh* G2~7YT0ay$G literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-polyline.png b/fixtures/web-first/chromium/svg-marker-polyline.png new file mode 100644 index 0000000000000000000000000000000000000000..5490457bc4b069399e44f9e51e159460436e822c GIT binary patch literal 857 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVCM65aSW-L^Jb=hwy>i_`~90L zSC(A7v})yzC7wCTdn-O&a^^qU(dqR;UWuzJDBwbKyu*hp-4A?iMOgf~n!2Vx5s2C% z#xEzbH-c%InqbsXXX#l>Jj>>A_N;NdBZpD zC&VymF-yaf0`Uo7k3TQ^_rv512ZzOGnU3VDBNESK8OoV|KC$6&xgq4hv8;xKwSw5#K1XamczW--pvXNieoh_}L*?&jafrHxn-EL${ z{aIc9=FQoYGd~<(*vS*wo4DlqeJT6H%z3sAY=`}7AKq)9dExSflZUGBKY5tH`JU*n zxF1=CHyX{w1Qrxt;@PxfnXj(x&0q$;0>J}^CSQ|3)L>ltWMxb>Vq*i~E>!>Dv9xM!+^ zs)TmpP5J2mc0#-FovV@M6cC;F(VjD9{YHUF7A?wzad?VEd-yd9T{92d3=*f*j&%<9U`z6sao`osm}`iYd>3lVc2q}`DaQ^_{M$HJ9hY#O^cuKm@TvI zT-WK!V`pTRRWa<{bAiu0e*5i|iqFaQhk{+F3VU3xS-kT}^IE(7Us@l+nKUi4WVdlH zd(YhT^NQam_az+$b)2_3-ubsx@qVq4QE}&FI2p*~P&9>Mf{ZEy0q%2tV7~bO|Lui- gaUg>Ylrxwa9?pK9xb8;cX`l#$r>mdKI;Vst0P7fi3;+NC literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-rect-drop.png b/fixtures/web-first/chromium/svg-marker-rect-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..cc21967fccbaa1bc819ae81158df8244321b3e64 GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=b39!fLn`LHxwVnEIe>@ZBKIjS zNyW*A;*$-ToDXn);AYuXy><2Er>BIAgo{`m)-jwAW!TB2u$=$HUZX$rx6c)in128H zn;hu_4ce{mO*So*b$r6r5W_e@n*m96I0H_>_#~c+)B7Jt{^G58vj6^Et0su~U>$cD o85sWmzu@!O4(LuckUNUGZ<#bHhZ(pu>El%Y59TuU&XOob&s2Y;s1Yg9`R(LKiEM2U}mWGG$?nB SF6RM?FnGH9xvX|tu^Q|!^W^l@^@iSKs3s;7JXKdGCaFg>BlP{i2~!=x~s0XOq1 z>y9mde_s1Gd*yTSkY~5PKHWO)(1w=Ua(kZp=fb5EzGxqL!|?j<;t$9Dj=#PA*I)6A zbHIO=u#l`*0&DaRT<$PsF#P||tfD3h^b#A$OUw)tb{ICi T?ppW(D8k_B>gTe~DWM4fOz>!; literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-refx-percentage-root.png b/fixtures/web-first/chromium/svg-marker-refx-percentage-root.png new file mode 100644 index 0000000000000000000000000000000000000000..c1896f53f5690972fece3917562f02d494000b13 GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=4?SHRLn`LHxn{_9$Uvg);q_On zB6n_i%2aV5Unh6qmdKI;Vst E0Lq$mlK=n! literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-refx-percentage-viewbox.png b/fixtures/web-first/chromium/svg-marker-refx-percentage-viewbox.png new file mode 100644 index 0000000000000000000000000000000000000000..b540e3dc8d302de5e0d234225e1c27326b3a292e GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=D?MEtLn`LHxw4V>kO2ehMdc(` zFPEcwI*vzU-ZVzEas?W(Z%^Tn#ad6SNtMSRK|eoDgN$ z$%HDHcmB}5is{T^}iD@9r>c4@R5I|e?jsr zh9X}K1JF&q%>IL!f#LuEcx9H$Ku5EI9L>y7yhVV=?{v;8pa_GftDnm{r-UW|wun~D literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-refy-keyword-drop.png b/fixtures/web-first/chromium/svg-marker-refy-keyword-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..5dc682059a390995499ba3938272f61b10e8d883 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=7d%}YLn`LHxxSY7kOPDJNB2b5 z6xCU#H<~>SG{O@2Q*4#l?5~tT(-XIjtaL{`7Szb*7AL zOBGjf8~iYJiDT3Xu3}lmY4A;GPHn?RZuv9+g#Lv8W5Q?W9J>ST3=IGOhprEJ2lN{o b$ZyOH*R%}VP1)jgfFcZ@u6{1-oD!M<`1NGr literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-refy-percentage-alias.png b/fixtures/web-first/chromium/svg-marker-refy-percentage-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..0e5bd2886fd9104b484dfaf4321907ece3e1cd2d GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC={hlt4Ar*7pTs9PJ4q#xp$orYs z#j(k(WFc=r10$DCz{NNFeyy6jlM@J;6v7#va5cm*PS9p3Vs%)@a6*&;iy&{Dk^b}i zg17$L#2@cpd%sz{gb||aGW!pO>#TS*`^i55y6pddxy8+XK&P>RoW{(+_lv!0k>tDy PKoJH{S3j3^P6FVdQ&MBb@06R)5 Ar~m)} literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-source-group-transform.png b/fixtures/web-first/chromium/svg-marker-source-group-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..d97e963e08d1d2c00229a5f81fb36d86c7a92324 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ZJsWUAr*7pTz2Gba9}uc;2tY) z$06od9~v}#8k&DkF5CAzdz0|a&KSlC+6+ak4(k|Bh%)SCQV3^w!qtE(*gOCB`p28U$z5X#0GK^GXswvOVi4|GfaRY44$rj JF6*2UngFwr3aV8iD`bhc9Pems(uS6Bb!`<=yMo7Iibu zK6<|XG5cQTx?tvexyQ@Pwko`^6Ih4ul1@c;jioZb^a1{)|4nHknD WGEHApA^H_4!rfuD+NYapUnmmS_K^JnjihGI=6Z#MuzTq%fV~gen70=AF*Qeb0}V z=Dt08=<}z_`;rT69PGbvnX_qg8r;6H_b}5Ln-c$ePCbzq{>;|PAJ%hDdy~4l=Hgz7 zSqyt6=N(+gm^NX(0Zzl=w*O+V2YTTDf1j|4sXzuB$S2GU_xCHh_SD@j0E#epy85}S Ib4q9e0HLX0B>(^b literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-tangent-degenerate-cubic.png b/fixtures/web-first/chromium/svg-marker-tangent-degenerate-cubic.png new file mode 100644 index 0000000000000000000000000000000000000000..25ecfc0345a67c9adaebce2bf5de7407748cd2f2 GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8$4YcLn`LHxwe9w`1tA^p8vu0!S3dn`nyXWl$Cei5!=b6uswOg^N%Hu52yaGo6o=gb+e-x z&nNB=kv8uaC!G;W$h<0H%og#hx~EOybL3ZUpvVmY8OaCh;+i+V5OG__a6*&;Xht~0 z6Rw6B#tGUCMXV0Q3;M}F;AUX>|36&z{1%|w*+6b*W?1;aZrjvt*-}6e22WQ%mvv4F FO#mK!TF3wZ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-tangent-degenerate-quadratic.png b/fixtures/web-first/chromium/svg-marker-tangent-degenerate-quadratic.png new file mode 100644 index 0000000000000000000000000000000000000000..25ecfc0345a67c9adaebce2bf5de7407748cd2f2 GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8$4YcLn`LHxwe9w`1tA^p8vu0!S3dn`nyXWl$Cei5!=b6uswOg^N%Hu52yaGo6o=gb+e-x z&nNB=kv8uaC!G;W$h<0H%og#hx~EOybL3ZUpvVmY8OaCh;+i+V5OG__a6*&;Xht~0 z6Rw6B#tGUCMXV0Q3;M}F;AUX>|36&z{1%|w*+6b*W?1;aZrjvt*-}6e22WQ%mvv4F FO#mK!TF3wZ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-tangent-opposite.png b/fixtures/web-first/chromium/svg-marker-tangent-opposite.png new file mode 100644 index 0000000000000000000000000000000000000000..98665606839d220395af5b5adf5e5f4f8f41df3d GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=H$7b(Ln`LHxqguIP=Ex>L(dId z(igne9E@6fgzHiZ`%_saN2k7~U=I=I8-DhcY=#YQzf4XSV2pUa z|DXRnN0x;APb-Z(7~c5Wm9i~o{&T)w7eveW+7`kn;ZMITd-gCXtYM3XYbVTL3p8?xXiy94YS|qgwa)Kfyxc_TiP{WBtPblKPKYw>WKsxcc*4~X z!#Dv+P*(cut@+C=zw^I8{^9i=L5Fp+2Yim7ett~K=Dbc~SwrH3Q}UTCGcsrG*v!|x z=SXAkEBOr*m=w0(l-)4J4Tq6Xd*;|35NBZc|6kj()e-1+Hjvwy8Iqpx^UrJ4JpvSA N@O1TaS?83{1OTH`Sy=!8 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-units-invalid.png b/fixtures/web-first/chromium/svg-marker-units-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d98d38d62e71bfb353414f1c9657412afb766e GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=9iA?ZAr*7pT;9mp;2^+yapz&P zBV{@_%Z@m0VH9?rS^03&#RKzC_&(ukh+&+d%}~VZu#Vw`D8o)Bg>VKW!Mumd&z0`q z|0n;t;k|dh$kLbDB~F+!A}Ph9{}%&4&?*1_yX6M|2Qt_|ZenIok6~%*{LNzu6k+gm L^>bP0l+XkKzY|66 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-units-stroke-width.png b/fixtures/web-first/chromium/svg-marker-units-stroke-width.png new file mode 100644 index 0000000000000000000000000000000000000000..f05998caec6d1796cb10d8b8384c99b0aeb3be77 GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8$4YcLn`LHxwet_kbwxpMeie0 z9c_sbO>M3XYbVTL3p8?xXiy94YS|qgwa)Kfyxc_TiP{WBtPblKPKYw>WKsxcc*4~X z!#Dv+P*(cut@+C=zw^I8{^9i=L5Fp+2Yim7ett~K=Dbc~SwrH3Q}UTCGcsrG*v!|x z=SXAkEBOr*m=w0(l-)4J4Tq6Xd*;|35NBZc|6kj()e-1+Hjvwy8Iqpx^UrJ4JpvSA N@O1TaS?83{1OTH`Sy=!8 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-units-user-space-whitespace.png b/fixtures/web-first/chromium/svg-marker-units-user-space-whitespace.png new file mode 100644 index 0000000000000000000000000000000000000000..ff08a0069dd26b8f1bcaf2e002f4aabd1f9572cf GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV3hZCaSW-L^XBGm-a`f=tq%_; z^SnslKc(Zy+{_TIDAyF|aKX@};gOj~$BY{j*Ps76`^wKxoAQ4ttmps4!hjQPV&BoT z>T+(#*$FG;jNV(Vd-Z+ex;-|w zy-&>RLR7E9erW&2L{yt{|`#OmjfAWprB%Am?g9Dso4G1 QPk|x~p00i_>zopr0NIg@SO5S3 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-units-user-space.png b/fixtures/web-first/chromium/svg-marker-units-user-space.png new file mode 100644 index 0000000000000000000000000000000000000000..49e2c31b48d5a5befb2808c9b4a395147d054fc8 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=OFdm2Ln`LHxw?_}P=LUJ0QDq( zscVZ8Cmsm%=*ZNNRN6AdbCt%o*L5GIU;CLWnJa}eJmG4HVVt1NP{iu6j^TtT!%ijz zbiuh1FN=QPySecDYpcKe&(D9p;mQ4142KUKY23oJ#^k)sjpe?RcV_tatugK(WRfU@ rWo?5z(4GJPmpK=H2Qt_|4rXRBd&b4;a-xO>D8k_B>gTe~DWM4ffAUq^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-url-commented.png b/fixtures/web-first/chromium/svg-marker-url-commented.png new file mode 100644 index 0000000000000000000000000000000000000000..48542a5d759da35bb3c37bf5a3513b70dc46cbac GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=`#oJ8Ln`LHxxSJ2kb!{PL-!*h z9v!X{3fdbbZ6j59qZJk$F!fyUaqs`tcYb+(d&%~M?THY>P8J6}#tGgGxR?!nXODk0 zlRsu`f8*lD@`mTnEj~Ud+7+)Jwn}J0r0k-ohP1+tRgAOND}D)1@b~SW@356&wf#XY zMzj8d_n33`Rb2im@}vC?(+O3EBF=^wCWYw?IGB5ZKKTE?NBRNCk8B{XFf;5pVDw|5 S>{b?_2!p4qpUXO@geCy^Vql&C literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-url-escaped.png b/fixtures/web-first/chromium/svg-marker-url-escaped.png new file mode 100644 index 0000000000000000000000000000000000000000..48542a5d759da35bb3c37bf5a3513b70dc46cbac GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=`#oJ8Ln`LHxxSJ2kb!{PL-!*h z9v!X{3fdbbZ6j59qZJk$F!fyUaqs`tcYb+(d&%~M?THY>P8J6}#tGgGxR?!nXODk0 zlRsu`f8*lD@`mTnEj~Ud+7+)Jwn}J0r0k-ohP1+tRgAOND}D)1@b~SW@356&wf#XY zMzj8d_n33`Rb2im@}vC?(+O3EBF=^wCWYw?IGB5ZKKTE?NBRNCk8B{XFf;5pVDw|5 S>{b?_2!p4qpUXO@geCy^Vql&C literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-url-malformed-drop.png b/fixtures/web-first/chromium/svg-marker-url-malformed-drop.png new file mode 100644 index 0000000000000000000000000000000000000000..e57117b85f11733e09192ff2b4df38b072515beb GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8J;eVAr*7pTwvq{@|q0N{$F5g zbpJ6yx`FVdQ&MBb@06R)5 Ar~m)} literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-url-quoted.png b/fixtures/web-first/chromium/svg-marker-url-quoted.png new file mode 100644 index 0000000000000000000000000000000000000000..48542a5d759da35bb3c37bf5a3513b70dc46cbac GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=`#oJ8Ln`LHxxSJ2kb!{PL-!*h z9v!X{3fdbbZ6j59qZJk$F!fyUaqs`tcYb+(d&%~M?THY>P8J6}#tGgGxR?!nXODk0 zlRsu`f8*lD@`mTnEj~Ud+7+)Jwn}J0r0k-ohP1+tRgAOND}D)1@b~SW@356&wf#XY zMzj8d_n33`Rb2im@}vC?(+O3EBF=^wCWYw?IGB5ZKKTE?NBRNCk8B{XFf;5pVDw|5 S>{b?_2!p4qpUXO@geCy^Vql&C literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-viewbox-empty.png b/fixtures/web-first/chromium/svg-marker-viewbox-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..e57117b85f11733e09192ff2b4df38b072515beb GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=8J;eVAr*7pTwvq{@|q0N{$F5g zbpJ6yx`FVdQ&MBb@06R)5 Ar~m)} literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-viewbox-invalid.png b/fixtures/web-first/chromium/svg-marker-viewbox-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..8c624269bc6538814481eb735b0d45d5c93e42e5 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=?Vc`Q3fo6x_{2>eE0o& z{qDYx`({HGzGUWkG7F!km&`Umm;C?#wU2KvkiiCW5;Md8ZkDFXlg~~AMHoC?{an^L HB{Ts57Y#-< literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-viewbox-meet.png b/fixtures/web-first/chromium/svg-marker-viewbox-meet.png new file mode 100644 index 0000000000000000000000000000000000000000..ad4be9e6a8f630aeedf1fbd69721c3c0d41754bc GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=?Vc`Q3fo6x_{2>eE0o& z{qDYx`({HGzGUWkG7F!km&`Umm;C?#wU2KvkiiCW5;Md8ZkDFXlg~~AMHoC?{an^L HB{Ts57Y#-< literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-viewbox-none.png b/fixtures/web-first/chromium/svg-marker-viewbox-none.png new file mode 100644 index 0000000000000000000000000000000000000000..8752685a6e1e1e30d06cafb54b52977b88de0485 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=t)4E9Ar*7pT;9mp;J|S7!kvdw zkF5IMSUpm>bt~HQ)2{n$|6KBv!Wo`$HN-GZ&}JxNby&x6LX=@AlLC_9F9vS=;&s2z zvfnQ4`TmXxF8_s*ttb{vEv5n0KzIEAKaqcT3y{GEau73vN-j&&zUMkMKoJH{S3j3^ HP6VKW!Mumd&z0`q z|0n;t;k|dh$kLbDB~F+!A}Ph9{}%&4&?*1_yX6M|2Qt_|ZenIok6~%*{LNzu6k+gm L^>bP0l+XkKzY|66 literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-viewbox-slice.png b/fixtures/web-first/chromium/svg-marker-viewbox-slice.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0654c9ad6e3d261ae0f38171451284f4882446 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=t)4E9Ar*7pT;9mp;J|S7!kvdw zkF5IMSUpm>bt~HQ)2{n$|6KBv!Wo`$HN-GZ&}JxNby&x6LX=@AlLAaI`p3-TyZLrE z)+O&L7l+F(=Qo(}nh8}UR_&MBD}e6!|G!^-hCh(O267NHgNX`D)5NW3*8oKrJYD@< J);T3K0RT5*MV9~o literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-direct-alias.png b/fixtures/web-first/chromium/svg-marker-width-direct-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..cec854093e460524560a1ef85ffcb5f0ec55178a GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=U7jwEAr*7pTy^Aaa1c0p!RbPS zhRp&#{*FT|F@YNf~tDS%=33={S_8G+Q4R6);6#+F#P|2KDF%-&{b?8S1~i_$g(u)FUmO# P6k+gm^>bP0l+XkK|A<9# literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-invalid.png b/fixtures/web-first/chromium/svg-marker-width-invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..b064f3cb7cd837a6baa0391d74b24e5bef2ade2d GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=_dQ)4Ln`LHxn{`MWFXP@Q2Yy* zp4;N??FxbBdJmuM+{0dYsDMYjGf!^OqeJ&LG2H*|BYWHY?fhpQrv;xgbQCijv1E88 z!%)cAV8QM%hgsnqqX4S#j+5*e+fy&EJKf|`yn~UId)D=3YdklksBP*|=3K-qv+n+Z z8F%#me%inNdG1s}$2tzTt5S>eIKG(|eP6feyzy*F0VaMSpMRVs?CU=tsQkauPi4j* zVTn`rj)d$5Ij5#U9_YjW|9iDWb^#e|Ag?krNPLu@aX|a7C{TpK)78&qol`;+0IUXY Aa{vGU literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-negative.png b/fixtures/web-first/chromium/svg-marker-width-negative.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-percentage-alias.png b/fixtures/web-first/chromium/svg-marker-width-percentage-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..59b4031e6d2d76ffb18eab6a153a9fb46ede9398 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=^F3W0Ln`LHxwetl$&u$kK(W)6 z9EQ&um_Bc0`h0-xib#i;;eor;-&|K++O5I7voVHof;K}DtHU~m6QT?|nH0hqo^UlF z3BHYdQ?Ywa-Sg{T|D`N?%E%`#SHD_TxRe>L>nqph8`TAA3s>ep7ef-pYS>HW8b$_& l|Nj$oB@}>8Wdk{tnPHA1d%MfCnGb;?44$rjF6*2UngDifP^JI? literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-percentage.png b/fixtures/web-first/chromium/svg-marker-width-percentage.png new file mode 100644 index 0000000000000000000000000000000000000000..1de5d6871e62ed0e2afb72bef290ed1deab53e81 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyCH&6{1AN#6k+gm^>bP0l+XkKEd^GG literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-px.png b/fixtures/web-first/chromium/svg-marker-width-px.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b7007f1cf82ce44f5d7e087ef59b18d9dec765 GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Cp=voLn`LHxwe=0kbwl-!^i18 zOH%kx={O!$$!Qf3l4NC(b(RQRvRGl^!JXeTbs~!%PX2S!`^mHustiS(4KYj#(;09x zud?qjyZiKBNwiv2z|QE~Q@=-X{pI3&`}jt`rTuJ%=8cMO>^I)bZ?!M7du#WuUfRi? z<*R?fSsx$SrmO5Tsy;e?^?wlllT!(w6<{|k=l{UO!0`Y76_u1cpr6=4eqv^*JZjh= Tb0x$PD8k_B>gTe~DWM4frDS2) literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-width-zero.png b/fixtures/web-first/chromium/svg-marker-width-zero.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-marker-wrong-target.png b/fixtures/web-first/chromium/svg-marker-wrong-target.png new file mode 100644 index 0000000000000000000000000000000000000000..4774ab01d88ce9fae72539addf999f428ef2b7a7 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=ot`d^Ar*7pTyf-Wa1c0pVbeh- zp@25MDT<7O3lj5NzRi7p#pjX>}0|ae964V=T`sk zhlke{N2k5}DsK?_x?jqX2#r9~#2Fa=|M#Dn$p~~58^}@247Cj`O)GEcF9(V+c)I$z JtaD0e0st4KMY8|^ literal 0 HcmV?d00001 diff --git a/fixtures/web-first/chromium/svg-path-marker-end.png b/fixtures/web-first/chromium/svg-path-marker-end.png new file mode 100644 index 0000000000000000000000000000000000000000..4b3427668347244f03b3fc68a8dfe89133e28514 GIT binary patch literal 519 zcmV+i0{H!jP)*hTI$5J==_2uh)zE0j? zEb{=aq@MmBoW6h5s%J2iML&YOm^~u*Qq$(xP;RYr7(!_CFEDPdyO4U3nh3%vj39J* z5jw%_ec(G*5PH1|TR`Z&7GOsRvKV4U2(}tzMF_eaW<&_SAHaqXWkZAsA=-`*3qsT_ zF$RR_dxF{#l5C1&ehO{7LZH;L_$le>wji91A zniD}ycQ_Y diff --git a/fixtures/web-first/svg-marker-circle-drop.svg b/fixtures/web-first/svg-marker-circle-drop.svg new file mode 100644 index 00000000..8dea1f28 --- /dev/null +++ b/fixtures/web-first/svg-marker-circle-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-clip.svg b/fixtures/web-first/svg-marker-client-clip.svg new file mode 100644 index 00000000..b66b4738 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-filter.svg b/fixtures/web-first/svg-marker-client-filter.svg new file mode 100644 index 00000000..de51ba6d --- /dev/null +++ b/fixtures/web-first/svg-marker-client-filter.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-mask.svg b/fixtures/web-first/svg-marker-client-mask.svg new file mode 100644 index 00000000..1e1c6451 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-mask.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-opacity-empty-source.svg b/fixtures/web-first/svg-marker-client-opacity-empty-source.svg new file mode 100644 index 00000000..722136e7 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity-empty-source.svg @@ -0,0 +1,5 @@ + + empty + + + diff --git a/fixtures/web-first/svg-marker-client-opacity-missing.svg b/fixtures/web-first/svg-marker-client-opacity-missing.svg new file mode 100644 index 00000000..c8e76363 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity-missing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/svg-marker-client-opacity-unselected-mid.svg b/fixtures/web-first/svg-marker-client-opacity-unselected-mid.svg new file mode 100644 index 00000000..fa897262 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity-unselected-mid.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fixtures/web-first/svg-marker-client-opacity-wrong-target.svg b/fixtures/web-first/svg-marker-client-opacity-wrong-target.svg new file mode 100644 index 00000000..3312d3eb --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity-wrong-target.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fixtures/web-first/svg-marker-client-opacity-zero-viewport.svg b/fixtures/web-first/svg-marker-client-opacity-zero-viewport.svg new file mode 100644 index 00000000..da1be6a7 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity-zero-viewport.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fixtures/web-first/svg-marker-client-opacity.svg b/fixtures/web-first/svg-marker-client-opacity.svg new file mode 100644 index 00000000..c7d8344c --- /dev/null +++ b/fixtures/web-first/svg-marker-client-opacity.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-root-viewbox.svg b/fixtures/web-first/svg-marker-client-root-viewbox.svg new file mode 100644 index 00000000..52c58d1e --- /dev/null +++ b/fixtures/web-first/svg-marker-client-root-viewbox.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-stroke-none-width.svg b/fixtures/web-first/svg-marker-client-stroke-none-width.svg new file mode 100644 index 00000000..126bc686 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-stroke-none-width.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-transform.svg b/fixtures/web-first/svg-marker-client-transform.svg new file mode 100644 index 00000000..f78e6181 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-use-inherited.svg b/fixtures/web-first/svg-marker-client-use-inherited.svg new file mode 100644 index 00000000..6ecca254 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-use-inherited.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-client-use-target.svg b/fixtures/web-first/svg-marker-client-use-target.svg new file mode 100644 index 00000000..a9612df9 --- /dev/null +++ b/fixtures/web-first/svg-marker-client-use-target.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-context-fill.svg b/fixtures/web-first/svg-marker-context-fill.svg new file mode 100644 index 00000000..a7fa3a00 --- /dev/null +++ b/fixtures/web-first/svg-marker-context-fill.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-context-stroke.svg b/fixtures/web-first/svg-marker-context-stroke.svg new file mode 100644 index 00000000..c7d33b76 --- /dev/null +++ b/fixtures/web-first/svg-marker-context-stroke.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-default-viewport-clip.svg b/fixtures/web-first/svg-marker-default-viewport-clip.svg new file mode 100644 index 00000000..dbaa4f76 --- /dev/null +++ b/fixtures/web-first/svg-marker-default-viewport-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-display-none-resource.svg b/fixtures/web-first/svg-marker-display-none-resource.svg new file mode 100644 index 00000000..5fde0dcb --- /dev/null +++ b/fixtures/web-first/svg-marker-display-none-resource.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-duplicate-id-first.svg b/fixtures/web-first/svg-marker-duplicate-id-first.svg new file mode 100644 index 00000000..27f2c61c --- /dev/null +++ b/fixtures/web-first/svg-marker-duplicate-id-first.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-ellipse-drop.svg b/fixtures/web-first/svg-marker-ellipse-drop.svg new file mode 100644 index 00000000..5f78eaba --- /dev/null +++ b/fixtures/web-first/svg-marker-ellipse-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-height-direct-alias.svg b/fixtures/web-first/svg-marker-height-direct-alias.svg new file mode 100644 index 00000000..61bb5a74 --- /dev/null +++ b/fixtures/web-first/svg-marker-height-direct-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-height-percentage.svg b/fixtures/web-first/svg-marker-height-percentage.svg new file mode 100644 index 00000000..ebdc41e9 --- /dev/null +++ b/fixtures/web-first/svg-marker-height-percentage.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-inherited-end.svg b/fixtures/web-first/svg-marker-inherited-end.svg new file mode 100644 index 00000000..85b3679d --- /dev/null +++ b/fixtures/web-first/svg-marker-inherited-end.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-line-zero-length.svg b/fixtures/web-first/svg-marker-line-zero-length.svg new file mode 100644 index 00000000..3d5caf8f --- /dev/null +++ b/fixtures/web-first/svg-marker-line-zero-length.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-line.svg b/fixtures/web-first/svg-marker-line.svg new file mode 100644 index 00000000..1842bbfa --- /dev/null +++ b/fixtures/web-first/svg-marker-line.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-missing-reference.svg b/fixtures/web-first/svg-marker-missing-reference.svg new file mode 100644 index 00000000..6194fd0d --- /dev/null +++ b/fixtures/web-first/svg-marker-missing-reference.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-none-reset.svg b/fixtures/web-first/svg-marker-none-reset.svg new file mode 100644 index 00000000..376dca63 --- /dev/null +++ b/fixtures/web-first/svg-marker-none-reset.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-auto-start-reverse.svg b/fixtures/web-first/svg-marker-orient-auto-start-reverse.svg new file mode 100644 index 00000000..d0f1cf6d --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-auto-start-reverse.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-degrees.svg b/fixtures/web-first/svg-marker-orient-degrees.svg new file mode 100644 index 00000000..a8b99095 --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-degrees.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-direct-alias.svg b/fixtures/web-first/svg-marker-orient-direct-alias.svg new file mode 100644 index 00000000..bf6dedec --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-direct-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-gradians.svg b/fixtures/web-first/svg-marker-orient-gradians.svg new file mode 100644 index 00000000..dc824e02 --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-gradians.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-invalid.svg b/fixtures/web-first/svg-marker-orient-invalid.svg new file mode 100644 index 00000000..d1d2ddeb --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-invalid.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-radians.svg b/fixtures/web-first/svg-marker-orient-radians.svg new file mode 100644 index 00000000..066b2166 --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-radians.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-turn.svg b/fixtures/web-first/svg-marker-orient-turn.svg new file mode 100644 index 00000000..ebeef4d2 --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-turn.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-orient-whitespace-drop.svg b/fixtures/web-first/svg-marker-orient-whitespace-drop.svg new file mode 100644 index 00000000..16bb94e7 --- /dev/null +++ b/fixtures/web-first/svg-marker-orient-whitespace-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-arc-authored-vertex.svg b/fixtures/web-first/svg-marker-path-arc-authored-vertex.svg new file mode 100644 index 00000000..89c5e84c --- /dev/null +++ b/fixtures/web-first/svg-marker-path-arc-authored-vertex.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-cubic-authored-vertex.svg b/fixtures/web-first/svg-marker-path-cubic-authored-vertex.svg new file mode 100644 index 00000000..0d3482e6 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-cubic-authored-vertex.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-move-only.svg b/fixtures/web-first/svg-marker-path-move-only.svg new file mode 100644 index 00000000..33499f5b --- /dev/null +++ b/fixtures/web-first/svg-marker-path-move-only.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-multiple-subpaths.svg b/fixtures/web-first/svg-marker-path-multiple-subpaths.svg new file mode 100644 index 00000000..04fdb8a2 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-multiple-subpaths.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-quadratic-authored-vertex.svg b/fixtures/web-first/svg-marker-path-quadratic-authored-vertex.svg new file mode 100644 index 00000000..507fa361 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-quadratic-authored-vertex.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-repeated-close.svg b/fixtures/web-first/svg-marker-path-repeated-close.svg new file mode 100644 index 00000000..7f81f5a8 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-repeated-close.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-repeated-moves.svg b/fixtures/web-first/svg-marker-path-repeated-moves.svg new file mode 100644 index 00000000..c1070540 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-repeated-moves.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path-valid-prefix.svg b/fixtures/web-first/svg-marker-path-valid-prefix.svg new file mode 100644 index 00000000..1f92e5b0 --- /dev/null +++ b/fixtures/web-first/svg-marker-path-valid-prefix.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-path.svg b/fixtures/web-first/svg-marker-path.svg new file mode 100644 index 00000000..43eedfc8 --- /dev/null +++ b/fixtures/web-first/svg-marker-path.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-polygon-one-point.svg b/fixtures/web-first/svg-marker-polygon-one-point.svg new file mode 100644 index 00000000..b3625675 --- /dev/null +++ b/fixtures/web-first/svg-marker-polygon-one-point.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-polygon.svg b/fixtures/web-first/svg-marker-polygon.svg new file mode 100644 index 00000000..07256959 --- /dev/null +++ b/fixtures/web-first/svg-marker-polygon.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-polyline-one-point.svg b/fixtures/web-first/svg-marker-polyline-one-point.svg new file mode 100644 index 00000000..531931ef --- /dev/null +++ b/fixtures/web-first/svg-marker-polyline-one-point.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-polyline.svg b/fixtures/web-first/svg-marker-polyline.svg new file mode 100644 index 00000000..04e5ff8c --- /dev/null +++ b/fixtures/web-first/svg-marker-polyline.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-rect-drop.svg b/fixtures/web-first/svg-marker-rect-drop.svg new file mode 100644 index 00000000..b1adb968 --- /dev/null +++ b/fixtures/web-first/svg-marker-rect-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-reference-negative.svg b/fixtures/web-first/svg-marker-reference-negative.svg new file mode 100644 index 00000000..08f5db22 --- /dev/null +++ b/fixtures/web-first/svg-marker-reference-negative.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refx-direct-alias.svg b/fixtures/web-first/svg-marker-refx-direct-alias.svg new file mode 100644 index 00000000..25bca6f3 --- /dev/null +++ b/fixtures/web-first/svg-marker-refx-direct-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refx-keyword-drop.svg b/fixtures/web-first/svg-marker-refx-keyword-drop.svg new file mode 100644 index 00000000..464641fe --- /dev/null +++ b/fixtures/web-first/svg-marker-refx-keyword-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refx-percentage-root.svg b/fixtures/web-first/svg-marker-refx-percentage-root.svg new file mode 100644 index 00000000..ccbfa2e6 --- /dev/null +++ b/fixtures/web-first/svg-marker-refx-percentage-root.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refx-percentage-viewbox.svg b/fixtures/web-first/svg-marker-refx-percentage-viewbox.svg new file mode 100644 index 00000000..012e4040 --- /dev/null +++ b/fixtures/web-first/svg-marker-refx-percentage-viewbox.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refy-keyword-drop.svg b/fixtures/web-first/svg-marker-refy-keyword-drop.svg new file mode 100644 index 00000000..3b37e13c --- /dev/null +++ b/fixtures/web-first/svg-marker-refy-keyword-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-refy-percentage-alias.svg b/fixtures/web-first/svg-marker-refy-percentage-alias.svg new file mode 100644 index 00000000..e847db9a --- /dev/null +++ b/fixtures/web-first/svg-marker-refy-percentage-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-source-empty.svg b/fixtures/web-first/svg-marker-source-empty.svg new file mode 100644 index 00000000..833d2914 --- /dev/null +++ b/fixtures/web-first/svg-marker-source-empty.svg @@ -0,0 +1 @@ +empty marker diff --git a/fixtures/web-first/svg-marker-source-group-transform.svg b/fixtures/web-first/svg-marker-source-group-transform.svg new file mode 100644 index 00000000..fc87ddea --- /dev/null +++ b/fixtures/web-first/svg-marker-source-group-transform.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-source-shapes.svg b/fixtures/web-first/svg-marker-source-shapes.svg new file mode 100644 index 00000000..565c2ed3 --- /dev/null +++ b/fixtures/web-first/svg-marker-source-shapes.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-tangent-angle-wrap.svg b/fixtures/web-first/svg-marker-tangent-angle-wrap.svg new file mode 100644 index 00000000..925b84cb --- /dev/null +++ b/fixtures/web-first/svg-marker-tangent-angle-wrap.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-tangent-degenerate-cubic.svg b/fixtures/web-first/svg-marker-tangent-degenerate-cubic.svg new file mode 100644 index 00000000..65f646ea --- /dev/null +++ b/fixtures/web-first/svg-marker-tangent-degenerate-cubic.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-tangent-degenerate-quadratic.svg b/fixtures/web-first/svg-marker-tangent-degenerate-quadratic.svg new file mode 100644 index 00000000..961f46e0 --- /dev/null +++ b/fixtures/web-first/svg-marker-tangent-degenerate-quadratic.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-tangent-opposite.svg b/fixtures/web-first/svg-marker-tangent-opposite.svg new file mode 100644 index 00000000..16775570 --- /dev/null +++ b/fixtures/web-first/svg-marker-tangent-opposite.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-units-default.svg b/fixtures/web-first/svg-marker-units-default.svg new file mode 100644 index 00000000..f1a52b3c --- /dev/null +++ b/fixtures/web-first/svg-marker-units-default.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-units-invalid.svg b/fixtures/web-first/svg-marker-units-invalid.svg new file mode 100644 index 00000000..6375f61e --- /dev/null +++ b/fixtures/web-first/svg-marker-units-invalid.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-units-stroke-width.svg b/fixtures/web-first/svg-marker-units-stroke-width.svg new file mode 100644 index 00000000..3d65d401 --- /dev/null +++ b/fixtures/web-first/svg-marker-units-stroke-width.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-units-user-space-whitespace.svg b/fixtures/web-first/svg-marker-units-user-space-whitespace.svg new file mode 100644 index 00000000..9f1ee5c2 --- /dev/null +++ b/fixtures/web-first/svg-marker-units-user-space-whitespace.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-units-user-space.svg b/fixtures/web-first/svg-marker-units-user-space.svg new file mode 100644 index 00000000..673bae8a --- /dev/null +++ b/fixtures/web-first/svg-marker-units-user-space.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-url-commented.svg b/fixtures/web-first/svg-marker-url-commented.svg new file mode 100644 index 00000000..9848beba --- /dev/null +++ b/fixtures/web-first/svg-marker-url-commented.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-url-escaped.svg b/fixtures/web-first/svg-marker-url-escaped.svg new file mode 100644 index 00000000..7a7194ce --- /dev/null +++ b/fixtures/web-first/svg-marker-url-escaped.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-url-malformed-drop.svg b/fixtures/web-first/svg-marker-url-malformed-drop.svg new file mode 100644 index 00000000..b3478485 --- /dev/null +++ b/fixtures/web-first/svg-marker-url-malformed-drop.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-url-quoted.svg b/fixtures/web-first/svg-marker-url-quoted.svg new file mode 100644 index 00000000..efb62069 --- /dev/null +++ b/fixtures/web-first/svg-marker-url-quoted.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-empty.svg b/fixtures/web-first/svg-marker-viewbox-empty.svg new file mode 100644 index 00000000..417e3e08 --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-empty.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-invalid.svg b/fixtures/web-first/svg-marker-viewbox-invalid.svg new file mode 100644 index 00000000..aa3ad849 --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-invalid.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-meet.svg b/fixtures/web-first/svg-marker-viewbox-meet.svg new file mode 100644 index 00000000..bd9b277a --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-meet.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-negative.svg b/fixtures/web-first/svg-marker-viewbox-negative.svg new file mode 100644 index 00000000..06c10f9a --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-negative.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-none.svg b/fixtures/web-first/svg-marker-viewbox-none.svg new file mode 100644 index 00000000..84222883 --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-none.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-nonzero-origin.svg b/fixtures/web-first/svg-marker-viewbox-nonzero-origin.svg new file mode 100644 index 00000000..84fbce19 --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-nonzero-origin.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-viewbox-slice.svg b/fixtures/web-first/svg-marker-viewbox-slice.svg new file mode 100644 index 00000000..5ba906f9 --- /dev/null +++ b/fixtures/web-first/svg-marker-viewbox-slice.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-direct-alias.svg b/fixtures/web-first/svg-marker-width-direct-alias.svg new file mode 100644 index 00000000..ebc68ea3 --- /dev/null +++ b/fixtures/web-first/svg-marker-width-direct-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-invalid.svg b/fixtures/web-first/svg-marker-width-invalid.svg new file mode 100644 index 00000000..fed30ac9 --- /dev/null +++ b/fixtures/web-first/svg-marker-width-invalid.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-negative.svg b/fixtures/web-first/svg-marker-width-negative.svg new file mode 100644 index 00000000..1b9d67f7 --- /dev/null +++ b/fixtures/web-first/svg-marker-width-negative.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-percentage-alias.svg b/fixtures/web-first/svg-marker-width-percentage-alias.svg new file mode 100644 index 00000000..5bd791dd --- /dev/null +++ b/fixtures/web-first/svg-marker-width-percentage-alias.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-percentage.svg b/fixtures/web-first/svg-marker-width-percentage.svg new file mode 100644 index 00000000..380413bd --- /dev/null +++ b/fixtures/web-first/svg-marker-width-percentage.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-px.svg b/fixtures/web-first/svg-marker-width-px.svg new file mode 100644 index 00000000..d90c14ae --- /dev/null +++ b/fixtures/web-first/svg-marker-width-px.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-width-zero.svg b/fixtures/web-first/svg-marker-width-zero.svg new file mode 100644 index 00000000..86b07446 --- /dev/null +++ b/fixtures/web-first/svg-marker-width-zero.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-marker-wrong-target.svg b/fixtures/web-first/svg-marker-wrong-target.svg new file mode 100644 index 00000000..20f46ffc --- /dev/null +++ b/fixtures/web-first/svg-marker-wrong-target.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-path-marker-end.svg b/fixtures/web-first/svg-path-marker-end.svg similarity index 100% rename from fixtures/web-first/unsupported/svg-path-marker-end.svg rename to fixtures/web-first/svg-path-marker-end.svg diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index 7c6a4e0d..e75c8537 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -29,7 +29,12 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-preserve-aspect-ratio-defer.svg` | Reject the SVG 1.1 `defer` prefix as malformed grammar: SVG2 dropped it and Chromium treats the whole value as unparseable. | | `svg-width-percentage.svg` | Reject percentage root sizing by name — its basis is the host window itself, a cell the element-capture baker cannot express, so it graduates only with a host-level oracle. (Shape-geometry and stroke-width percentages graduated with the percentages rung.) | | `svg-path-css-d-property.svg` | Declare a stylesheet's `d: path(…)`: Chromium honors it in place of the attribute, and the pinned Stylo build drops the declaration entirely. | -| `svg-path-marker-end.svg` | Refuse by name — **load-bearing**. Nothing else reads a marker property: the property *is* the paint trigger, so this refusal is what keeps Chromium's arrowhead from becoming a silent hole. | +| `svg-marker-author-stylesheet.svg` | Refuse a used marker in a document with author CSS. The pinned cascade has no marker longhands, and the flattened resource walk cannot attribute selector matches inside the marker source; dropping the sheet could change either the client reference or source content silently. | +| `svg-marker-external.svg` | Refuse an external marker URL by stable marker name. `websem` owns no resource base or I/O, so dropping a resolvable arrowhead would be a silent hole. | +| `svg-marker-height-var.svg` · `svg-marker-width-calc.svg` · `svg-marker-orient-var.svg` · `svg-marker-refx-unit.svg` · `svg-marker-refy-css-wide.svg` | Refuse custom-property substitution, CSS math, a wider unit basis, and CSS-wide resource values by the exact marker field. Chromium computes those standard-track routes; their independent value-family rows carry the missing context while the marker rows remain open. | +| `svg-marker-overflow.svg` · `svg-marker-root-effect.svg` · `svg-marker-root-opacity.svg` · `svg-marker-root-transform.svg` | Refuse authored changes to the marker resource root's clipping, effect, opacity, or transform. M1 admits the default hidden viewport and simple source subtree only; silently flattening any of these scopes would change the marker image. | +| `svg-marker-root-rendering-style.svg` | Refuse a rendering declaration inherited from the marker resource root into its source. Chromium makes root `shape-rendering="crispEdges"` identical to the child spelling and 128 pixels at Δ255 from default; `paint-order="stroke fill"` behaves the same way and differs by 121 pixels at Δ188. Before the patrol, both admissions silently painted default. The fixture guards inline style; the marker contract independently guards both properties in attribute and style form (measured, not celled). | +| `svg-marker-source-animation.svg` · `svg-marker-source-gradient.svg` · `svg-marker-source-nested.svg` · `svg-marker-source-opacity.svg` · `svg-marker-source-text.svg` · `svg-marker-source-use.svg` | Bound the static marker-source program. Dynamics, paint servers, nested markers, source effects, text, and source-side `` refuse transactionally: best effort removes the complete client span, including its ordinary fill/stroke, rather than leaking a plausible partial result. | | `svg-stroke-dasharray-escape.svg` | Refuse by name — a CSS escape can hide a basis-less unit from the authored-text patrol (`1\76 w` tokenizes as `1vw`), so the presentation attribute, style attribute, and stylesheet spellings all refuse rather than silently use the pinned device basis. | | `svg-stroke-dasharray-font-basis.svg` | Refuse by name — `em`/`rem` are admitted only while their cascaded `font-size` basis is trustworthy. A basis-less unit, `var()`, or escape in that basis would resolve a different cycle from Chromium; the exact poison classes and all ancestor/sheet ingresses share the stroke-width rung's guarded patrol. | | `svg-stroke-dasharray-sheet-unit.svg` | Refuse by name — the stylesheet spelling of a dash interval in a unit whose basis this build lacks. Viewport-, container-, font-metric-, and root-font-metric-relative unit classes are all guarded; the units' own checklist rows carry those gaps, following the stroke-width precedent. | @@ -213,4 +218,6 @@ the register to 131. Later filter and pattern rungs, recorded in the [D-N register](../../../docs/wg/consolidation/svg-engine-of-record.md), move the live total to 169. The exact-time pattern-client rung adds the fractional tile phase, animation-number provenance, and animation-number syntax rows, taking -the current register to 172.) +the register to 172. Marker M1 then graduates the former broad marker row into +ninety exact cells and adds eighteen focused source, resource, cascade, +and value rows, taking the current register to 189.) diff --git a/fixtures/web-first/unsupported/svg-marker-author-stylesheet.svg b/fixtures/web-first/unsupported/svg-marker-author-stylesheet.svg new file mode 100644 index 00000000..3504a345 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-author-stylesheet.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-external.svg b/fixtures/web-first/unsupported/svg-marker-external.svg new file mode 100644 index 00000000..fdd8a952 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-external.svg @@ -0,0 +1,3 @@ + + + diff --git a/fixtures/web-first/unsupported/svg-marker-height-var.svg b/fixtures/web-first/unsupported/svg-marker-height-var.svg new file mode 100644 index 00000000..daa10092 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-height-var.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-orient-var.svg b/fixtures/web-first/unsupported/svg-marker-orient-var.svg new file mode 100644 index 00000000..beca5209 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-orient-var.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-overflow.svg b/fixtures/web-first/unsupported/svg-marker-overflow.svg new file mode 100644 index 00000000..9ba20f82 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-overflow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-refx-unit.svg b/fixtures/web-first/unsupported/svg-marker-refx-unit.svg new file mode 100644 index 00000000..a2646748 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-refx-unit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-refy-css-wide.svg b/fixtures/web-first/unsupported/svg-marker-refy-css-wide.svg new file mode 100644 index 00000000..627e39fd --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-refy-css-wide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-root-effect.svg b/fixtures/web-first/unsupported/svg-marker-root-effect.svg new file mode 100644 index 00000000..2482b7d9 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-root-effect.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-root-opacity.svg b/fixtures/web-first/unsupported/svg-marker-root-opacity.svg new file mode 100644 index 00000000..aa0dd79a --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-root-opacity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-root-rendering-style.svg b/fixtures/web-first/unsupported/svg-marker-root-rendering-style.svg new file mode 100644 index 00000000..0dabc517 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-root-rendering-style.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-root-transform.svg b/fixtures/web-first/unsupported/svg-marker-root-transform.svg new file mode 100644 index 00000000..40ab773e --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-root-transform.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-animation.svg b/fixtures/web-first/unsupported/svg-marker-source-animation.svg new file mode 100644 index 00000000..e7774b4f --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-animation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-gradient.svg b/fixtures/web-first/unsupported/svg-marker-source-gradient.svg new file mode 100644 index 00000000..4329458d --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-gradient.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-nested.svg b/fixtures/web-first/unsupported/svg-marker-source-nested.svg new file mode 100644 index 00000000..c470ea86 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-nested.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-opacity.svg b/fixtures/web-first/unsupported/svg-marker-source-opacity.svg new file mode 100644 index 00000000..6a7d3166 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-opacity.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-text.svg b/fixtures/web-first/unsupported/svg-marker-source-text.svg new file mode 100644 index 00000000..3899686a --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-text.svg @@ -0,0 +1,6 @@ + + + M + + + diff --git a/fixtures/web-first/unsupported/svg-marker-source-use.svg b/fixtures/web-first/unsupported/svg-marker-source-use.svg new file mode 100644 index 00000000..8131ffa6 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-source-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fixtures/web-first/unsupported/svg-marker-width-calc.svg b/fixtures/web-first/unsupported/svg-marker-width-calc.svg new file mode 100644 index 00000000..1e02f7e4 --- /dev/null +++ b/fixtures/web-first/unsupported/svg-marker-width-calc.svg @@ -0,0 +1,4 @@ + + + + From 121926e11ce04524814da07ba58938ee6a12df7a Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 28 Aug 2026 19:11:48 +0900 Subject: [PATCH 2/2] docs: format rframe capability table --- crates/rframe/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/rframe/README.md b/crates/rframe/README.md index 26506433..0790cd2c 100644 --- a/crates/rframe/README.md +++ b/crates/rframe/README.md @@ -19,12 +19,12 @@ producer (e.g. websem, from SVG) ## What it holds -| 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 | +| 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, conservative bounds, and an explicit anti-aliased or hard edge policy | Two details are load-bearing enough to state here. A node's `bounds` is the