DioxusLabs / DioxusLabs/anyrender

anyrender_svg: feature and correctness gaps vs resvg

Open
#88 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
106
Forks
31
Avg merge
2h 8m
Merged PRs (30d)
11

Description

# `anyrender_svg` vs `resvg`: feature and correctness comparison

Both crates render a `usvg::Tree`, so SVG *parsing/normalization* coverage is identical (usvg handles `use`, markers, text-to-path flattening, transforms, visibility, etc.). All gaps below are in the *rendering* stage (`anyrender_svg/src/render.rs` + `util.rs` vs resvg's `crates/resvg/src/*`).

## Missing functionality (supported by resvg, not by anyrender_svg)

- [ ] **Masks** (resvg `mask.rs`). `group.mask()` is never read. Both luminance and alpha masks, mask regions (`mask.rect()`), and chained masks (`mask.mask()`) are unsupported. Also *silently dropped* — the error handler is not invoked, so there is no red-box fallback.
- [ ] **Filter effects** (resvg's entire `filter/` module: feGaussianBlur, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDisplacementMap, feDropShadow, lighting, feMorphology, feTurbulence, feFlood, feTile, feImage...). `group.filters()` is never read. Also silently dropped (no error-handler call).
- [ ] **Patterns** (`usvg::Paint::Pattern`). `util::to_brush` returns `None` → red error box. resvg renders the pattern subtree to a pixmap and tiles it (`path.rs::render_pattern_pixmap`).
- [x] **Gradient spread method**. resvg maps `SpreadMethod::{Pad,Reflect,Repeat}` to the shader's spread mode; anyrender_svg never set `peniko::Extend` (always Pad). Fixed in #86.
- [ ] **Full clip-path support**. Only a clipPath whose root has a **single `Path` child** is supported. Not supported (resvg `clip.rs` handles all of these):
- multiple children in one `clipPath` (union of shapes)
- group/text children inside a `clipPath`
- `clip-path` on `clipPath` children (XOR-composited in resvg via `clip_group`)
- chained clip paths (`clip.clip_path()`)
- the clipPath's own `transform` (and transforms on its children — `to_bez_path` uses raw child geometry)
- the clip path's `clip-rule`: `push_layer` receives no fill rule, so even-odd clips may be wrong
When any of these appear, clipping is simply wrong or absent — no error box.
- [x] **`image-rendering` quality modes**. resvg maps OptimizeQuality/HighQuality → Bicubic, Smooth → Bilinear, OptimizeSpeed/CrispEdges/Pixelated → Nearest; anyrender_svg ignored `img.rendering_mode()`. Fixed in #87.
- [ ] **`shape-rendering`** (`path.rendering_mode().use_shape_antialiasing()` in resvg): AA is never disabled for `crispEdges`/`optimizeSpeed` (acknowledged in lib.rs docs).
- [ ] **Group isolation** (`isolation: isolate`). resvg isolates any group where `should_isolate()` is true; anyrender_svg only pushes a layer for clip/opacity/blend, so an explicit `isolation: isolate` with no other properties does nothing.
- [ ] **System-font loading in the `render_svg_str*` entry points**. These construct `usvg::Options::default()`, whose `fontdb` is empty — text in an SVG string resolves no fonts even though the `text` feature enables usvg's `system-fonts`. Callers can work around it via `render_svg_tree` with their own `Options`, but the string APIs are a text-rendering trap.

## Correctness gaps in features both support

- [x] **Radial gradients: focal/center points swapped and `fr` ignored** (`util.rs`, `to_brush`). Should be start (focal) circle = `(fx, fy, fr)`, end circle = `(cx, cy, r)` as in resvg; anyrender_svg used start = `(cx, cy, 0)`, end = `(fx, fy, r)`, so any gradient with an offset focal point rendered "inverted" and non-zero `fr` was dropped. Fixed in #84.
- [x] **Group opacity/blend fallback clip uses a wrongly-transformed rect** (`render.rs`). `g.layer_bounding_box()` is in the group's local coordinates, but the rect is pushed with `global_transform * transform` which already includes `node.abs_transform()` — for groups whose ancestors carry transforms the clip rect lands in the wrong place, clipping away content of semi-transparent/blended groups. resvg computes `layer_bounding_box().transform(transform)` explicitly and outsets it 2px for AA; anyrender_svg has no AA outset either, so content at the exact bbox edge can clip.
- [ ] **Blend-mode compositing edge cases**: for a group with `mix-blend-mode`, resvg renders the group into an isolated offscreen pixmap (outset 2px for anti-aliasing) and blends the whole pixmap over the backdrop; anyrender_svg pushes a layer clipped exactly to the group's bbox and lets the backend apply the mix. Results can differ at the edges: AA fringes just outside the bbox get clipped (and thus blend differently), and what counts as the "backdrop" for the blend depends on each backend's layer semantics. Not a concrete known-wrong rendering — a "may differ from resvg in edge cases, backend-dependent" note. Minor.
- [ ] **Zero-area fills**: resvg skips filling paths whose bounds have zero width/height (a horizontal/vertical line with `fill` should paint nothing); anyrender_svg fills them anyway — behavior depends on the backend. Very minor.
- [ ] **`stroke-linejoin: miter-clip`** is mapped to plain `Miter` (kurbo has no miter-clip). resvg/tiny-skia also lacks true miter-clip, so this is parity with resvg, not a gap — noting neither is correct per SVG 2.
- [ ] **Error-handler coverage is inconsistent with the docs**: red boxes are drawn only for pattern paints and undecodable/disabled raster images. Masks, filters, multi-path clips, and (previously) spread methods degrade *silently*, which makes gaps hard to notice downstream (e.g. in Blitz).

Raster image decoding coverage is roughly equivalent (PNG/JPEG/GIF/WebP, first frame only in both; anyrender_svg uses the `image` crate with unpremultiplied RGBA + `ImageAlphaType::Alpha`, resvg uses premultiplied pixmaps).

## Summary

The big four missing features are **masks, filters, patterns, and full clip-path support** (the first three are acknowledged in `lib.rs`; the clip-path limitations are only partially so). The most impactful remaining silent correctness issues are **clipPath transform/clip-rule handling** and the **mistransformed opacity-layer clip rect**. Mask support could plausibly be built on `push_layer`-style luminance masking if/when anyrender's `PaintScene` grows a mask primitive; filters are the only structurally hard item.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in anyrender_svg/src/render.rs and util.rs, then compare the selected unchecked rendering gap with the corresponding resvg module named in the issue. This issue covers masks, filters, patterns, clip paths, and several correctness cases, so choose one bounded item before changing anything. Done means the selected case matches resvg behavior and no longer degrades silently where the issue requires error handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.