-
Notifications
You must be signed in to change notification settings - Fork 110
Description
SVG Export: Skia Does Not Properly Support <mask> Tag
Summary
When exporting canvas content with masks to SVG format, Skia's SVG canvas backend does not generate proper SVG <mask> elements. This results in incorrect or missing mask effects in the exported SVG files.
Background
Grida Canvas uses Skia as its primary rendering backend for 2D graphics. The canvas supports three types of masks:
- Geometry Mask - Hard, binary shape mask (resolution-independent, vector-based)
- Alpha Mask - Per-pixel alpha controls opacity, A(x,y) ∈ [0,1]
- Luminance Mask - Luminance (Y) of mask drives opacity (white=opaque, black=transparent)
Current Implementation
The mask rendering is implemented in crates/grida-canvas/src/painter/painter.rs:
- Geometry masks use Skia's
clipPath()withcanvas.save()andcanvas.restore() - Alpha/Luminance masks use
saveLayer()→ draw content → apply mask withBlendMode::DstIn→restore()
For luminance masks specifically, the implementation applies skia_safe::luma_color_filter::new() to convert RGB values to alpha before blending.
SVG Export Process
SVG export is handled in crates/grida-canvas/src/export/export_as_svg.rs:
let canvas = svg::Canvas::new(bounds, None);
renderer.render_to_canvas(&canvas, width, height);
let data = canvas.end();The export uses Skia's skia_safe::svg::Canvas which is supposed to translate Skia drawing commands into SVG markup.
The Problem
When rendering masks to Skia's SVG canvas, the masking effects are not properly exported as SVG <mask> elements. According to the SVG specification and our documentation (docs/wg/feat-masks/index.md), masks should be exported as:
Expected SVG output for Alpha/Luminance masks:
<mask id="myMask" mask-type="alpha">
<!-- mask definition -->
</mask>
<g mask="url(#myMask)">
<!-- masked content -->
</g>Expected SVG output for Geometry masks:
<clipPath id="myClip">
<!-- clip path definition -->
</clipPath>
<g clip-path="url(#myClip)">
<!-- clipped content -->
</g>However, Skia's SVG canvas does not generate these structures. The saveLayer() + BlendMode::DstIn approach used for rendering masks in raster/screen contexts does not translate to proper <mask> elements in SVG output.
Reproduction
We have a dedicated example that demonstrates the mask functionality:
File: crates/grida-canvas/examples/grida_mask.rs
This example creates four panels demonstrating:
- No mask (baseline)
- Geometry mask (circular clip)
- Alpha mask (radial gradient alpha)
- Luminance mask (radial gradient luminance)
Steps to Reproduce
- Run the mask example:
cargo run --example grida_mask - Modify the example to export to SVG instead of displaying in a window
- Inspect the exported SVG file
- Observe that mask effects are either missing or incorrectly represented
Expected: SVG contains proper <mask> elements with references
Actual: Masks are either flattened, omitted, or improperly rendered in the SVG output
Impact
This limitation affects:
- Design-to-code workflows - Masked designs cannot be accurately exported as vector SVGs
- SVG interoperability - Exported SVGs cannot be properly edited in other vector tools
- File portability - Loss of mask information when round-tripping through SVG format
- Performance - Cannot leverage vector-based masking in exported SVGs (may result in rasterization)
Technical Context
Mask Types Affected
- ✅ Geometry masks - These use
clipPath()which may export to<clipPath>(needs verification) - ❌ Alpha masks - Use
saveLayer()+BlendMode::DstIn, do not export to<mask>withmask-mode: alpha - ❌ Luminance masks - Use
saveLayer()+ luma color filter +BlendMode::DstIn, do not export to<mask>withmask-mode: luminance
Expected SVG Structures
Based on our documentation (docs/wg/feat-masks/index.md), the expected mappings are:
| Grida Mask Type | Expected SVG Output |
|---|---|
| Geometry | <clipPath> with path data |
| Alpha | <mask> with mask-mode: alpha (default) |
| Luminance | <mask> with mask-mode: luminance or feColorMatrix type="luminanceToAlpha" |
Skia SVG Canvas Limitations
The root cause is that skia_safe::svg::Canvas does not properly translate:
saveLayer()+ blend mode operations to SVG masking primitives- Color filters (like luma filter) to SVG filter effects
- Compositing operations to appropriate SVG masking structures
Related Files
crates/grida-canvas/src/export/export_as_svg.rs- SVG export implementationcrates/grida-canvas/src/painter/painter.rs- Mask rendering implementation (lines 841-875)crates/grida-canvas/examples/grida_mask.rs- Demonstration examplecrates/grida-canvas/examples/golden_sk_mask.rs- Golden test for mask renderingdocs/wg/feat-masks/index.md- Mask specification and design documentation
Example Code
The mask rendering implementation that works for raster output but doesn't translate to SVG:
fn draw_image_mask_group(&self, group: &PainterMaskGroup, mask_type: ImageMaskType) {
self.canvas.save_layer(&SaveLayerRec::default());
self.draw_render_commands(&group.content_commands);
let mut paint = SkPaint::default();
paint.set_blend_mode(skia_safe::BlendMode::DstIn);
if let ImageMaskType::Luminance = mask_type {
paint.set_color_filter(skia_safe::luma_color_filter::new());
}
self.canvas
.save_layer(&SaveLayerRec::default().paint(&paint));
self.draw_render_commands(&group.mask_commands);
self.canvas.restore();
self.canvas.restore();
}Environment
- Skia version: Using
skia-safecrate (version inCargo.toml) - Platform: All platforms (macOS, Linux, Windows)
- Affected format: SVG export only (PNG/JPEG exports work correctly as they're raster)
Notes
- This is a known limitation of Skia's SVG backend, not a bug in our implementation
- Raster exports (PNG, JPEG) work correctly and show masks as expected
- PDF export may have similar issues (needs verification)
- The issue affects both alpha and luminance mask types; geometry masks may work partially