Donner implements all 17 SVG filter primitives from the Filter Effects Module Level 1 spec, plus CSS shorthand filter functions (blur(), brightness(), drop-shadow(), etc.). Filters run on both shipping backends, TinySkia (CPU) and Geode (GPU); the removed full-Skia backend had its own native lowering path.
Guarantees:
Tracking: #151
| Layer | Location | Responsibility |
|---|---|---|
| SVG element classes | donner/svg/SVGFe*.h | DOM API, attribute accessors |
| Attribute parsing | donner/svg/parser/AttributeParser.cc | XML attribute -> ECS component |
| ECS components | donner/svg/components/filter/ | Data storage (FE*Component, FilterGraph) |
| Filter system | donner/svg/components/filter/FilterSystem.cc | DOM -> FilterGraph construction |
| Renderer driver | donner/svg/renderer/RendererDriver.cc | Orchestrates capture + graph execution |
| TinySkia executor | donner/svg/renderer/FilterGraphExecutor.cc | CPU pixmap-based graph execution |
| TinySkia filter lib | third_party/tiny-skia-cpp/src/tiny_skia/filter/ | Pure pixel math (all primitives) |
| Historical full-Skia backend | origin/skia_archive | Former native SkImageFilter DAG construction |
TinySkia backend (FilterGraphExecutor.cc):
Historical full-Skia backend (removed in 2026-04, archived at origin/skia_archive):
feImage with href="#elementId" renders a same-document element into the filter output:
fragmentRegionTopLeft on filter_primitive::Image carries the fallback filter-region origin. Explicit or partially specified primitive subregions retain their unclipped resolved coordinates for placement separately from their pixel-space clipping bounds.
//donner/svg/renderer/tests:renderer_public_api_tests enforces exact complete/partial-coordinate, protrusion and translated-source placement in FeImageFragmentUsesItsOwnSubregionOrigin, using nearest sampling. //donner/svg/renderer/tests:resvg_test_suite_default_text enforces the three with-x-y comparisons with ordinary budgets. The configured Geode counterparts are //donner/svg/renderer/tests:renderer_public_api_tests_geode and //donner/svg/renderer/tests:resvg_test_suite_geode; full-text coverage uses the API target with --config=text-full and //donner/svg/renderer/tests:resvg_test_suite_max. These bounded cases do not establish arbitrary host scale, rotation, skew or reflection correctness.
Filter primitives default to linearRGB per the color-interpolation-filters property. CSS shorthand functions always use sRGB.
The executor wraps each node: convert inputs to the node's color space, execute the primitive, tag the output. The final output converts back to sRGB before compositing.
Conversion uses LUT-based srgbToLinear() / linearToSrgb() in the tiny-skia-cpp library.
primitiveUnits controls how primitive subregion coordinates and attribute values are interpreted:
The executor resolves all coordinates to absolute pixel values before invoking filter operations.
Under host rotation/skew, deviceFromFilter transforms between filter user space and device pixels. The filter region is clipped using per-pixel point-in-rect testing against the user-space rectangle (not the AABB) to produce correct rotated boundaries.
Each filter primitive has a corresponding SVGFe*Element class (e.g., SVGFEGaussianBlurElement).
CSS filter property supports both url() references and shorthand functions:
Shorthand functions map to equivalent filter graph nodes internally.
For feConvolveMatrix with preserveAlpha="false", alpha is the divided convolution sum plus unscaled bias. RGB receives bias multiplied by that raw resulting alpha, before clamping. Stored alpha is clamped to [0, 1] and RGB to [0, stored alpha]. With preserveAlpha="true", the filter convolves straight RGB, adds unscaled bias, and premultiplies by the unchanged source alpha. See Filter Effects 1, section 9.9.
| Target | Backend | Description |
|---|---|---|
| //donner/svg/renderer/tests:resvg_test_suite_default_text | TinySkia | Full resvg golden image suite |
| //donner/svg/renderer/tests:renderer_tests | TinySkia | Deterministic golden tests |
| //donner/svg/renderer/tests:filter_graph_executor_tests | TinySkia | Unit tests for executor |
| //donner/svg/renderer/tests:convolve_matrix_bias_tests | TinySkia | Literal byte/float convolution bias expectations |
| third_party/tiny-skia-cpp/... | N/A | Per-primitive pixel math unit tests |
Set `DONNER_RENDERER_TEST_VERBOSE=1` and `LLM=0` when running tests to get per-pixel diff output, transform traces, and filter pipeline debug logging.
| Primitive | tiny-skia-cpp API | Skia API | Notes |
|---|---|---|---|
| feGaussianBlur | filter::gaussianBlur | SkImageFilters::Blur | 3-pass box blur approx |
| feFlood | filter::flood | SkImageFilters::Shader | Solid color fill |
| feOffset | filter::offset | SkImageFilters::Offset | Integer + bilinear subpixel |
| feColorMatrix | filter::colorMatrix | SkColorFilters::Matrix | matrix/saturate/hueRotate/luminanceToAlpha |
| feComposite | filter::composite | SkImageFilters::Blend/Arithmetic | Porter-Duff + arithmetic |
| feBlend | filter::blend | SkImageFilters::Blend | CSS blend modes |
| feComponentTransfer | filter::componentTransfer | CPU rasterization | LUT-based per-channel |
| feDropShadow | decomposed | decomposed | flood+composite+offset+blur+merge |
| feMorphology | filter::morphology | SkImageFilters::Dilate/Erode | erode/dilate |
| feConvolveMatrix | filter::convolveMatrix | CPU rasterization | Full 2D convolution |
| feTile | filter::tile | CPU rasterization | Modular tiling |
| feTurbulence | filter::turbulence | CPU rasterization | SVG-spec Perlin noise |
| feImage | renderer-only | renderer-only | External image / fragment ref |
| feDisplacementMap | filter::displacementMap | CPU rasterization | Channel-based displacement |
| feDiffuseLighting | filter::diffuseLighting | CPU rasterization | SVG-spec spotlight |
| feSpecularLighting | filter::specularLighting | CPU rasterization | Phong model |
Light sources (feDistantLight, fePointLight, feSpotLight) are children of lighting primitives. Point/spot light coordinates are scaled from user space to pixel space through deviceFromFilter. Z coordinates use the RMS scale factor.
Shared utility used in FilterGraphExecutor.cc (and previously by the removed full-Skia backend) for rasterizing images through affine transforms. Used by:
Uses bilinear interpolation with pixel-center sampling. Sample centers outside the source image produce transparent pixels (no edge clamping) to prevent images from bleeding beyond their preserveAspectRatio placement rectangle.