Skip to content

Support SVG filter effects in anyrender_svg - #90

Open
nicoburns wants to merge 1 commit into
mainfrom
devin/1786917676-svg-filter-effects
Open

Support SVG filter effects in anyrender_svg#90
nicoburns wants to merge 1 commit into
mainfrom
devin/1786917676-svg-filter-effects

Conversation

@nicoburns

Copy link
Copy Markdown
Member

Summary

Addresses the "Filter effects" item of #88. Previously render_group silently dropped group.filters(); now filtered groups are rendered inside a layer with an anyrender::Filter attached, leaving actual filter evaluation to the backend (no rasterization, pure PaintScene abstraction).

New module anyrender_svg::filter converts a usvg filter chain into a single anyrender::Filter DAG:

pub(crate) fn to_anyrender_filter(filters: &[Arc<usvg::filter::Filter>]) -> Option<Filter>
  • Maps all usvg primitives with a direct FilterEffect counterpart: blend, color matrix (incl. saturate/hueRotate/luminanceToAlpha), component transfer, composite (incl. arithmetic), convolve matrix, lighting, displacement map, drop shadow, flood, gaussian blur, morphology, offset, tile, turbulence.
  • feMerge is lowered to a chain of Composite(Over) nodes.
  • in="SourceAlpha" in a chained filter is synthesized via a helper ColorMatrix node; named result references resolve through a result map.
  • Returns None (→ whole conversion fails) for the inexpressible cases: feImage, anisotropic blur/morphology/turbulence, non-square convolution kernels, non-white lighting colors.

In render_group, when a group has filters:

if !g.filters().is_empty() {
    match (filter::to_anyrender_filter(g.filters()), g.filters_bounding_box()) {
        (Some(f), Some(region)) => scene.push_layer(Mix::Normal, 1.0, transform, &region, Some(Arc::new(f)), None),
        _ => error_handler(scene, node), // report instead of silently dropping; render unfiltered
    }
}

The filter layer is pushed inside the existing clip/opacity/blend layer and clipped to the SVG filter region, so filtering applies to group content before the group's clip/opacity/blend composite — matching the SVG rendering model.

One small API extension in the anyrender crate: Filter::set_output is now pub, so multi-node graphs built outside the crate can select their output node.

Known approximations (documented in filter.rs): primitive subregions, color-interpolation-filters, convolve targetX/targetY/edgeMode/kernelUnitLength, and turbulence stitchTiles are ignored.

Unit tests cover blur, drop shadow, named-result references, merge lowering, and unsupported-case fallbacks.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/742382aca6144666b1a508e6897f72fc
Requested by: @nicoburns

@staging-devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration

Copy link
Copy Markdown

Tested this PR by rendering SVGs headlessly to PNG with anyrender_svg + anyrender_vello_cpu (--features filters), comparing filtered output against identical unfiltered controls (pre-PR, filters were silently ignored so filtered output was pixel-identical to the control).

feGaussianBlur (stdDeviation=3) — renders correctly

Blur: control vs filtered

feDropShadow (dx=6 dy=6 stdDeviation=2) — renders correctly

Drop shadow: control vs filtered

Details & additional checks
  • Blur: edge scanline shows a gradual alpha ramp (0→27→136→235→255) vs a hard 0→255 step in the control.
  • Drop shadow: shadow-only region below-right of the circle contains translucent black pixels (e.g. rgba(0,0,0,118)) that are fully transparent in the control; diff bbox extends past the circle by the shadow offset.
  • Unsupported filter fallback: an feImage filter invokes the custom error handler and the element still renders unfiltered (not blank), as intended.
  • Note: vello_cpu applies layer filters only with the filters feature enabled and multithreading disabled.

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.

1 participant