Skip to content

SVG rendering completeness: preserveAspectRatio, clipPath/mask, patterns, image, full filter primitives, CSS styling, feBlend - #29

Merged
Malcolmnixon merged 26 commits into
mainfrom
feature/svg-full-support
Sep 28, 2026
Merged

Malcolmnixon merged 26 commits into
mainfrom
feature/svg-full-support

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

Summary

Full SVG rendering-completeness roadmap: 8 phases implemented on a shared long-lived branch, each independently code-reviewed and formally reviewed locally (per cost-saving workflow - see phase notes below) before this single consolidated PR.

Phases

  1. preserveAspectRatio + percentage geometry - foundational viewBox/percentage math.
  2. clipPath / mask elements - hard-clipping and luminance/alpha masking.
  3. Pattern fills - <pattern> paint server, tile-wrap math, patternUnits/patternContentUnits/patternTransform.
  4. <image> element - raster embedding via data: URIs (external href is a documented safe no-op; no filesystem/network access).
  5. Remaining filter primitives - feColorMatrix (all types), feComponentTransfer, feMorphology, feConvolveMatrix, feDisplacementMap, feTile, feDropShadow, feImage.
  6. Lighting/turbulence primitives - feDiffuseLighting/feSpecularLighting (feDistantLight/fePointLight/feSpotLight), feTurbulence (spec-exact Perlin noise).
  7. CSS <style> element / selector support - parser, selector matching, 3-tier cascade (presentation attribute < stylesheet rule < inline style).
  8. feBlend (all 14 CSS Compositing Level 1 blend modes) + a feComposite operator="arithmetic" fix (was a silent no-op, causing filter chains combining lighting + arithmetic composites to render as opaque rectangular blobs instead of following the source shape's silhouette).

Verification

  • Every phase independently verified via clean build.ps1 (0 warnings) and lint.ps1 (markdownlint-cli2, cspell, yamllint, dotnet format, reqstream, versionmark, reviewmark, sysml2tools) before and after every fix.
  • Every phase passed an independent code-review pass (several found real defects via executed reproduction cases and hand/spec-verified numeric checks, not just static inspection) and a formal-review pass (compliance/traceability/documentation rigor), with fixes re-reviewed until clean.
  • Requirements traceability (ReqStream) linked at both system (docs/reqstream/canvas-net.yaml) and subsystem (docs/reqstream/canvas-net/codecs.yaml) levels for every new requirement; SysML2 model sourceRef kept current.
  • Test count grew from 4356 to 4899, all passing across net8.0/net9.0/net10.0.
  • Visually verified against a real-world third-party fixture (SvgFixtures/InkscapeFilters.svg, 59 distinct filter definitions) compared against ImageMagick's reference rendering of the same file.

Test plan

  • pwsh ./build.ps1 - 4899/4899 tests passing, 0 warnings
  • pwsh ./lint.ps1 - clean
  • Independent code-review and formal-review passes per phase, all approved

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Malcolm Nixon and others added 25 commits September 27, 2026 09:27
… support

- Implement preserveAspectRatio parsing ('[defer] <align> [<meetOrSlice>]', all 10
  aligns, meet/slice) via a shared ComputePreserveAspectRatioFit helper reused by:
  - root <svg> viewBox fitting (default unchanged: implicit case already was
    'xMidYMid meet', confirmed via a dedicated pixel-for-pixel regression test)
  - symbol/use viewBox-fit (new capability, resolving use/symbol width/height with
    a use -> symbol -> viewport fallback chain)
  - marker's own explicit preserveAspectRatio attribute only; the pre-existing
    default (no-attribute) marker fit is preserved byte-for-byte unchanged
- Add viewport-relative percentage-based geometry resolution through the existing
  ParseGeometryCoordinate/GetFloatAttribute/GetOptionalFloat choke point, so
  existing finite/magnitude guards still apply after resolution:
  - horizontal/vertical attributes resolve against current viewport width/height
  - axis-agnostic attributes (r, stroke-width, stroke-dasharray) resolve against
    the diagonal basis sqrt(w^2+h^2)/sqrt(2)
  - font-size resolves against the parent element's own cascaded font-size
  - stroke-miterlimit remains the sole exception (a unitless ratio, not a length)
  - current viewport threaded via new RenderState.ViewportWidth/ViewportHeight
- Descope gradient userSpaceOnUse percentage plumbing (judgment call #5) as a
  documented, bounded follow-up simplification; unrelated gradient coordinate
  resolution (GetGradientCoordinateOrDefault) is untouched and unaffected.
- Update companion docs: design (svg-codec.md ViewBox Fitting Policy rewrite,
  new Percentage-Based Geometry Resolution section, marker/use/symbol behavior),
  reqstream (ViewBoxFitting rewrite, PercentageGeometryRejected ->
  PercentageGeometryResolved, new MarkerPreserveAspectRatio/SymbolViewBoxFitting
  requirements, all linked into codecs.yaml/canvas-net.yaml child chains with
  reqstream --enforce confirming zero orphans), verification scenarios, and the
  stale user-guide preserveAspectRatio claim.
- Add/repurpose tests: an 18-row root align/meetOrSlice Theory, an align=none
  stretch test, an explicit default-vs-xMidYMid-meet pixel-identity regression,
  symbol/use viewBox-fit tests, a marker explicit-preserveAspectRatio slice test
  (meet is provably pixel-identical to the default path once refX/refY
  ref-anchoring is applied, so slice is the only visibly distinguishable case),
  and a full percentage-geometry matrix (rect/circle/line/gradient/stroke-width/
  font-size/stroke-dasharray), repurposing the two prior percentage-rejection
  facts into percentage-resolution proofs rather than deleting their coverage.

All 4446 tests pass across net8.0/net9.0/net10.0; build.ps1 and lint.ps1 both
clean; reqstream --enforce reports zero orphaned requirements.
- Add clip-path=/mask= support for SVG elements/groups (userSpaceOnUse and
  objectBoundingBox unit modes for clipPathUnits/maskUnits, separate
  maskContentUnits for mask content coordinate system).
- clip-path implemented as offscreen alpha-buffer coverage intersection
  (no native clip-region primitive exists on the Canvas/Surface
  abstraction); union of clip children falls out of shared coverage
  buffer rasterization.
- mask implemented via luminance (0.2125R+0.7154G+0.0721B, SVG default)
  attenuation of an offscreen-rendered mask-content buffer, reusing the
  existing filter SourceGraphic-buffer/compositing infrastructure.
- Ordering fixed for combined filter + clip-path/mask cases: clip then
  mask now apply to pre-filter content before the filter chain runs,
  matching spec order geometry -> fill/stroke -> clip -> mask -> filter
  -> group opacity.
- Resource safety: clip/mask offscreen buffer charges reuse the existing
  shared FilterWorkBudget; new flat MaxClipPathShapesPerClipPath (256)
  cap (silent truncation) on clipPath children, which are rasterized
  directly and cannot cycle. Fixed a genuine pre-existing stack-overflow
  bug where elementDepth was not incremented before recursing into mask
  content.
- Scoped out (documented in code + design doc): nested clip/mask/filter
  on a clipPath's own children or on the clipPath/mask element itself,
  use/g/line as clip-path children, alpha mask mode, href/xlink:href
  inheritance for clipPath/mask elements.
- 21 new SvgCodecTests cases: clipPath unit modes, multi-shape union,
  mask luminance semantics, mask region/content unit variants, combined
  filter+clip/mask ordering, budget-exhaustion fallback for both
  clip-path and mask sharing the same FilterWorkBudget, 256-shape cap
  truncation, and regressions confirming pre-existing filter-only and
  plain-shape rendering are unaffected.
- Updated design/verification docs and reqstream requirement chain
  (CanvasNet-Lib-SvgClipPathMaskRendering ->
  CanvasNet-Codecs-SvgClipPathMaskRendering -> 4 new unit requirements),
  0 orphans confirmed via reqstream --lint.
RenderShapeEffectsPipeline/RenderGroupWithEffects each charged exactly one

region-area work unit against the shared FilterWorkBudget before allocating

the primary content buffer, regardless of how many of clip-path/mask/filter

actually applied - so a combined clip+mask (or clip/mask+filter) element

allocated up to 3x the memory it was charged for. Extract a shared

ComputeEffectsPipelineWorkUnits helper used by both call sites: it charges

one region-area unit for the content buffer (scaled by the filter's own

primitive count when a filter applies, unchanged from before), plus one

additional flat region-area unit for each of clip-path/mask that applies,

covering ApplyClipPath's own coverage buffer and ApplyMask's own

maskSource buffer. Updated the two pre-existing single-effect

budget-exhaustion tests' expected shape counts to match the corrected

per-application charge, and added a new regression test combining

clip-path and mask together to prove the combined charge (not just one

effect's own charge) correctly exhausts the shared cumulative budget.

Clarified the design doc's resource-safety discussion accordingly.
- Add SvgCodec.Patterns.cs implementing <pattern> element resolution:
  href/xlink:href inheritance chain with cycle detection, patternUnits
  and patternContentUnits (objectBoundingBox/userSpaceOnUse) matching
  existing gradient/clipPath precedent, patternTransform composition,
  viewBox/preserveAspectRatio reuse from Phase 1, tile rendering to an
  offscreen buffer with per-pixel inverse-transform wrap sampling and
  coverage-multiply compositing clipped to the actual fill/stroke path.
- Thread pattern-aware params through RenderShape/RenderFill/RenderStroke
  and dispatch pattern paints before existing ResolvePaint/gradient path;
  gradients, solid colors, and fill=url(#nonexistent) fallback behavior
  are unaffected.
- Bound tile-buffer allocation via Surface.MaxDimension and existing
  FilterWorkBudget charging; bound pattern-of-pattern recursion via the
  existing MaxElementDepth guard.
- Add 18 deterministic tests covering patternUnits x patternContentUnits
  (4 combinations), patternTransform, viewBox/preserveAspectRatio, href
  inheritance (including cycles), fill+stroke usage, nonexistent/wrong-type
  pattern references, and resource-safety behavior (no wall-clock timing
  assertions).
- Update design docs, reqstream requirements (linked into codecs.yaml
  children chain, 0 orphans confirmed), verification docs, and the
  SysML2 svg-codec unit sourceRef list for the new source file.
Add CanvasNet-Lib-SvgPatternRendering system-level requirement, linking
down to the previously-orphaned CanvasNet-Codecs-SvgPatternRendering
subsystem requirement, matching the structure of its sibling
filter/clip-path-mask/marker/gradient system-level requirements.
- Add <image> element rendering (SvgCodec.Image.cs): data: URI base64
  raster decode reusing existing PNG/JPEG/BMP/TIFF/GIF codecs
- Reuse Phase 1 preserveAspectRatio helper for placement-rect fitting
- Non-tiled per-pixel inverse-transform compositing (Phase 3 pattern
  precedent, without tiling/wrap)
- Dispatch through the shared element-rendering pipeline so
  clip-path/mask/filter/opacity apply automatically
- Security: non-data hrefs (external file/URL) and nested SVG-in-image
  (data:image/svg+xml or .svg refs) are explicit, documented no-ops;
  no filesystem/network access is added
- Resource/DoS safety: FilterWorkBudget charged with region area +
  base64 length before decode; decoded dimensions bounded by each
  raster codec's existing Surface.MaxDimension check
- Add 15 new tests: PNG/JPEG data URIs, preserveAspectRatio variants,
  clip-path/mask/filter/opacity on <image>, malformed/truncated
  base64, external href no-op, nested-SVG no-op, deterministic
  oversized-budget test (no wall-clock timing)
- Update companion artifacts: design doc, verification doc, sysml2
  sourceRef, and requirement chain at unit/subsystem/system levels
  (system-level link in docs/reqstream/canvas-net.yaml verified this
  time, addressing the gap missed in Phase 3)
…unsupported embedded raster images

RenderImageWithEffects's raster-decode catch filter only handled
FormatException, InvalidDataException, and ArgumentOutOfRangeException.
PngCodec.Load throws UnsupportedImageFeatureException (derives from
IOException, not InvalidDataException) for well-formed but unsupported
raster features such as Adam7-interlaced PNG, so an embedded interlaced
PNG <image> href propagated an uncaught exception all the way out of
SvgCodec.Load, aborting the entire SVG render instead of skipping just
that element per this codec's documented tolerant per-element no-op
contract.

Widened the catch filter to also catch IOException, so any recognized-
but-unsupported raster feature (from any sibling raster codec, not just
PNG) is tolerantly skipped. Checked JPEG/BMP/TIFF/GIF: none currently
define a similar unsupported-feature exception type - their unsupported-
feature cases are already thrown as InvalidDataException, which was
already caught.

Added a regression test embedding PngSuite's own interlaced
basi0g01.png fixture as a data:image/png;base64 href, proving
SvgCodec.Load completes without throwing and that sibling elements
before/after the skipped image still render correctly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The kernel coefficient lookup in ApplyConvolveMatrixPremultiplied and
ApplyConvolveMatrixPreservingAlpha performed plain correlation (no
rotation) instead of true convolution per the SVG Filter Effects spec's
kernelMatrix(orderX-J-1, orderY-I-1) term. This produced spec-incorrect
output for any non-point-symmetric kernel.

- Flip the kernel-matrix index in both call sites in
  SvgCodec.Filters.Convolution.cs; sampleX/sampleY offset math is
  unchanged (already spec-correct).
- Correct SvgCodec_Load_FeConvolveMatrixEdgeDetectKernel_ProducesExpectedEdgeResponse
  and SvgCodec_Load_FeConvolveMatrixEdgeModeWrap_SamplesAcrossOppositeEdge
  assertions to the spec-correct hand-derived expected pixel values.

No requirement/design/verification/review-set changes needed - this is
a pure correctness fix to already-documented behavior, not new
user-visible behavior.
Add four tests covering blue/alpha channel selectors, a negative scale
(direction reversal), and a larger-magnitude scale, bringing
feDisplacementMap test depth in line with its 7 sibling filter
primitives (only R/G selectors and one scale value were previously
covered).
Phase 6 of the SVG filter roadmap. Adds spec-exact implementations:

- SvgCodec.Filters.Lighting.cs: shared Sobel surface-normal estimation
  (9 kernel variants: interior + 4 edges + 4 corners), feDistantLight /
  fePointLight / feSpotLight resolution (including pointsAt/limitingConeAngle/
  specularExponent falloff), feDiffuseLighting (Lambertian), and
  feSpecularLighting (Blinn-Phong, alpha = max(R,G,B) of the specular color).
- SvgCodec.Filters.Turbulence.cs: the SVG spec's own reference Perlin noise
  algorithm (permutation table init, noise2/turbulence, turbulence vs
  fractalNoise remap, per-channel gradient offsets, baseFrequency/numOctaves/
  seed). stitchTiles=stitch tolerantly falls back to noStitch (documented).
- SvgCodec.Core.cs: MaxTurbulenceOctaves = 32 cap, enforced via
  InvalidDataException, charged through the existing FilterWorkBudget
  pattern used by feConvolveMatrix/feMorphology.
- SvgCodec.Filters.cs: dispatch cases for the 3 new primitives; work-unit
  accounting for feTurbulence numOctaves; trimmed the no-op primitive list
  to feBlend only (the sole remaining unimplemented primitive).
- SvgCodecTests.cs: 16 new facts covering distant/point/spot light x
  diffuse/specular hand-computed values, specular alpha != 1 proof, the
  spec's own PRNG conformance test vector (1043618065), determinism,
  turbulence-vs-fractalNoise remapping, numOctaves variation/cap, and
  stitch-tiles tolerant fallback.

Verified via full regression run: all 1574 pre-existing tests pass
unchanged, plus all 16 new tests (1590 total). The InkscapeFilters.svg
fixture's hard-coded pixel assertion was independently re-verified against
real rendering output and requires no change.
- docs/design/canvas-net/codecs/svg-codec.md: new prose paragraphs describing
  the shared Sobel surface-normal estimation, the three light-source element
  types, the diffuse/specular formulas (including the Sa=max(R,G,B) specular
  alpha subtlety), and the feTurbulence reference-algorithm-fidelity approach
  (naming the Park-Miller PRNG conformance test vector); trimmed the no-op
  primitive list to feBlend only; extended the resource-safety-caps sentence
  with MaxTurbulenceOctaves.
- docs/reqstream/canvas-net/codecs/svg-codec.yaml: added
  CanvasNet-Codecs-SvgCodec-FeDiffuseLighting, -FeSpecularLighting, and
  -FeTurbulence unit requirements; extended FilterPrimitiveResourceSafety's
  title/justification/tests for the new numOctaves cap.
- docs/reqstream/canvas-net/codecs.yaml: linked the 3 new unit ids into
  CanvasNet-Codecs-SvgFilterRendering's children/title/tests.
- docs/reqstream/canvas-net.yaml: updated CanvasNet-Lib-SvgFilterRendering's
  title/tests to name the three new primitives.
- docs/verification/canvas-net/codecs/svg-codec.md: 3 new subsections
  mirroring the existing FeMorphology/FeConvolveMatrix style; extended the
  FilterPrimitiveResourceSafety subsection for the new numOctaves cap test.
- docs/sysml2/model/canvas-net/codecs/svg-codec.sysml: appended the 2 new
  source file paths to the unit's sourceRef comment.
- .reviewmark.yaml: added the 2 new source files under
  CanvasNet-Codecs-SvgCodec's review paths, tagged '# phase 6 filter partial'.
…erms to cspell

- docs/verification/canvas-net/codecs/svg-codec.md: fixed a markdown-fixer
  line-wrap artifact from fix.ps1 (a mid-sentence dash was reflowed onto its
  own line, forming an accidental list item); reworded with parentheses
  instead to avoid the ambiguity.
- .cspell.yaml: added the genuine technical terms introduced by this phase
  (Sobel, Lambertian, Blinn, Phong, Perlin, PRNG/Prng, Schrage) to the
  project word list.
…view findings

- SvgCodec.cs class remarks: filter primitive no-op paragraph listed
  feTurbulence, feDiffuseLighting, feSpecularLighting, feColorMatrix,
  feConvolveMatrix, feComponentTransfer, feMorphology, feDisplacementMap,
  feImage, feTile, and feDropShadow as unimplemented no-op passthroughs.
  All are now implemented (Phases 5-6). Rewrote the paragraph to state
  that only feBlend remains a tolerant no-op passthrough, matching the
  wording already used by EvaluateFilterChain's XML doc remarks, and
  added the feImage MaxUseDepth recursion-guard note already documented
  in docs/design/canvas-net/codecs/svg-codec.md.
- Renamed test SvgTurbulencePrng_10000thValueFromSeedOne_MatchesSpecPublishedTestVector
  to SvgCodec_TurbulenceTablesRandom_10000thValueFromSeedOne_MatchesSpecPublishedTestVector
  to conform to the {ClassName}_{MethodUnderTest}_{Scenario}_{ExpectedBehavior}
  naming convention (method under test is SvgCodec.TurbulenceTables.Random).
  Updated the matching reference in SvgCodec.Filters.Turbulence.cs XML docs,
  and all requirements traceability references in
  docs/reqstream/canvas-net.yaml, docs/reqstream/canvas-net/codecs.yaml,
  docs/reqstream/canvas-net/codecs/svg-codec.yaml, and
  docs/verification/canvas-net/codecs/svg-codec.md.
- CssMatchWorkBudget now charges each stylesheet's precomputed
  MatchWorkPerElement (sum, per rule, of every comma-separated
  selector's own worst-case weight: 1 for its rightmost compound, +1
  per further child-combinator segment, +MaxElementDepth per further
  descendant-combinator segment) instead of the bare Rules.Count, so
  the charge genuinely tracks real per-element matching CPU cost
  (selector-list length and descendant ancestor-walk cost), closing
  the gap that let a selectors-per-rule-heavy stylesheet drive ~29x
  real wall-clock cost for an identical charged budget value.
- ParseRawRules/FindMatchingBrace no longer let an unterminated
  <style> rule swallow every later rule in the stylesheet: on failing
  to find a rule's own closing brace, the parser resynchronizes onto
  the next rule attempt (via the new FindSelectorRecoveryStart) rather
  than aborting the rest of the scan, so well-formed rules after a
  single malformed one are still parsed and applied.
- Strengthened SvgCodec_Load_CssMalformedRule_SkipsOnlyThatRuleAndStillLoads
  to assert the specific, now-documented recovery behavior instead of
  hedging, and added regression tests proving the CSS match-work
  budget scales with selector-list/combinator-chain length rather than
  bare rule count.
- Updated svg-codec.md's CSS malformed-input/resource-safety section
  to describe both fixes.
Task 1 - feBlend:
- New SvgCodec.Filters.Blend.cs implementing ApplyFeBlend with in/in2
  two-input resolution and all 14 CSS Compositing Level 1 modes (normal,
  multiply, screen, darken, lighten, color-dodge, color-burn, hard-light,
  soft-light, difference, exclusion, hue, saturation, color, luminosity).
  Non-separable modes use the spec's exact whole-RGB Lum/ClipColor/SetLum/
  Sat/SetSat reference algorithm. Blending operates on straight color,
  composited via the CSS Compositing simple-alpha formula, mirroring
  CompositeFeOperator's premultiply/un-premultiply convention.
- Wired case "feBlend" into EvaluateFilterChain's primitive switch.

Task 2 - feComposite arithmetic bug fix:
- ApplyFeComposite's "arithmetic" operator was previously unimplemented
  and fell through to a no-op passthrough, causing feDiffuseLighting's
  always-opaque alpha to propagate unchanged through filter50's chain
  (InkscapeFilters.svg), producing an opaque rectangular blob instead of
  following the source silhouette.
- Added ApplyFeCompositeArithmetic implementing the premultiplied
  k1*i1*i2 + k2*i1 + k3*i2 + k4 formula (color and alpha), clamped to
  [0,1], per CSS Filter Effects spec (the one Porter-Duff-family operator
  defined on premultiplied values).
- Also fixes the same class of bug in InkscapeFilters.svg's filter48,
  recomputing that fixture test's pixel expectations accordingly.

Tests: 20 new hand-computed tests (11 feBlend separable modes via Theory,
normal-equals-over cross-check, alpha-compositing, 2 non-separable
luminosity cases, mode-fallback, 4 arithmetic-operator/regression tests).
Repurposed FilterUnsupportedPrimitive test off feBlend onto a genuinely
bogus primitive tag.

Docs: updated design/verification docs, 3-level reqstream requirements,
SysML2 sourceRef, .reviewmark.yaml, and user guide per the verified
implementation plan in
.agent-logs/planning-febend-arithmetic-alpha-bug-8f3c21e0.md.
- introduction.md: remove image/pattern from the out-of-scope list (both
  are fully implemented - pattern in Phase 3, image in Phase 4) and add
  them to the supported-feature narrative alongside filter/clipPath/mask
- svg-codec.md: update the 'In-scope subset' filter bullet to list all
  17 currently-implemented fe* primitives instead of the stale 5-item
  Phase 5/6-era list, reconciled against the accurate narrative section
  elsewhere in the same document
…ction

Fix stale filter primitive enumeration in introduction.md's SvgCodec
narrative (feFlood/feGaussianBlur/feOffset/feComposite/feMerge only),
found during Phase 8 final formal-review recheck as inconsistent with
the just-corrected svg-codec.md design doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 28, 2026 10:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate findings remain across CSS behavior, filter correctness and tolerance, and requirements traceability.

Review effort: Lite
Findings: 5 Medium severity

Open (5)
What changed in this PR

Expands SVG rendering support with CSS styling, aspect-ratio handling, patterns, images, masking, and advanced filters.

Changes:

  • Adds CSS parsing, selectors, cascading, and fixtures.
  • Implements additional geometry, paint servers, images, and filter primitives.
  • Updates documentation, requirements traceability, tests, and review configuration.
File Summary
test/​DemaConsulting.CanvasNet.Tests/​SvgFixtures/​README.md Registers SVG fixtures.
test/​DemaConsulting.CanvasNet.Tests/​SvgFixtures/​css-styling.svg Adds CSS styling coverage.
test/​DemaConsulting.CanvasNet.Tests/​Codecs/​SvgFixtureTests.cs Adds fixture assertions.
test/​DemaConsulting.CanvasNet.Tests/​CanvasNetTests.cs Adds integration coverage.
src/​DemaConsulting.CanvasNet/​Codecs/​SvgCodec.cs Updates SVG integration and documentation.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Text.cs Adds percentage geometry handling for text.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Shapes.cs Adds percentage-based shape geometry.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.PreserveAspectRatio.cs Implements aspect-ratio fitting.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.PathRender.cs Adds pattern rendering.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Parsing.cs Applies root fitting behavior.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Gradients.cs Adds bounds-based paint mapping.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.Tile.cs Implements feTile.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.Morphology.cs Implements feMorphology.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.FeImage.cs Implements feImage.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.DropShadow.cs Implements feDropShadow.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.DisplacementMap.cs Implements displacement mapping.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.Convolution.cs Implements convolution filters.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.ComponentTransfer.cs Implements component-transfer filters.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.ColorMatrix.cs Implements color-matrix filters.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Filters.Blend.cs Implements blend-mode evaluation.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Css.Selectors.cs Parses and matches CSS selectors.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Css.Parser.cs Parses CSS rules and declarations.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Core.cs Adds filter and resource safety limits.
src/​DemaConsulting.CanvasNet/​Codecs/​Svg/​SvgCodec.Attributes.cs Adds percentage parsing.
README.md Updates SVG capability documentation.
docs/​user_guide/​introduction.md Documents expanded SVG support.
docs/​sysml2/​model/​canvas-net/​codecs/​svg-codec.sysml Updates SysML traceability.
docs/​reqstream/​canvas-net/​codecs.yaml Updates subsystem requirements and verification.
docs/​reqstream/​canvas-net.yaml Updates system requirements and verification.
.reviewmark.yaml Registers reviewed files.
.cspell.yaml Adds technical vocabulary.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/DemaConsulting.CanvasNet/Codecs/Svg/SvgCodec.Css.Cascade.cs
Comment thread src/DemaConsulting.CanvasNet/Codecs/Svg/SvgCodec.Css.Selectors.cs Outdated
Comment thread src/DemaConsulting.CanvasNet/Codecs/Svg/SvgCodec.Filters.Blend.cs
…t-combinator backtracking, and CSS cascade coverage for filter/clip-path/mask/clip-rule/stop-color/stop-opacity

- Add missing feBlend overlay mode (Overlay(Cb,Cs) = HardLight(Cs,Cb) per CSS
  Compositing Level 1), the one blend mode omitted from Phase 8's original 14.
- Fix CSS selector matching: descendant-combinator segments previously
  committed to the nearest matching ancestor without backtracking, causing
  selectors like 'g > .a .target' to under-match when the nearest .a wasn't
  the one satisfying the child combinator. Now backtracks recursively through
  every candidate ancestor. Updated CssMatchWorkBudget's cost model from an
  additive to a multiplicative (saturating) formula to correctly bound the
  new backtracking algorithm's real worst-case cost.
- Route filter/clip-path/mask/clip-rule/stop-color/stop-opacity through the
  same 3-tier CSS cascade chokepoint (ResolveElementProperty) already used
  for fill/stroke/font/marker properties, so stylesheet rules and inline
  style declarations now actually apply to these previously
  cascade-bypassed, XML-attribute-only properties. Added a per-element style
  context cache to avoid double-charging the CSS budget across the multiple
  call sites that now resolve properties on the same element.

Investigated 2 further Copilot findings (feColorMatrix/feComponentTransfer
allegedly needing premultiplied-alpha handling) and determined both are
false positives: this codebase's Surface.PremultiplyAlpha docs state
straight/unassociated alpha is the representation every codec uses, and the
SVG Filter Effects spec explicitly defines both primitives to operate on
non-premultiplied values - the existing straight-byte implementation is
already correct per spec and per this codebase's own architecture.

Test count: 4899 -> 4926, all passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 28, 2026 19:35
@Malcolmnixon

Malcolmnixon commented Sep 28, 2026 •

Copy link
Copy Markdown
Member Author

Thanks for catching this - fixed in ca525dc. Presentation properties (\filter, \clip-path, \mask, \clip-rule, \stop-color, \stop-opacity) now route through the same 3-tier CSS cascade chokepoint (\ResolveElementProperty) already used for fill/stroke/font/marker properties. Added regression tests proving a stylesheet-only rule (no XML attribute) now correctly applies \filter/\clip-path/\mask. Also added a per-element style-context cache so the extra call sites don't double-charge the CSS work budget.

@Malcolmnixon
Malcolmnixon merged commit 324a664 into main Sep 28, 2026
7 checks passed
@Malcolmnixon
Malcolmnixon deleted the feature/svg-full-support branch September 28, 2026 19:49
@Malcolmnixon
Malcolmnixon removed the request for review from Copilot September 28, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants