Skip to content

mask feature wont work with export as svg, with proper <mask> tag. #442

@softmarshmallow

Description

@softmarshmallow

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:

  1. Geometry Mask - Hard, binary shape mask (resolution-independent, vector-based)
  2. Alpha Mask - Per-pixel alpha controls opacity, A(x,y) ∈ [0,1]
  3. 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() with canvas.save() and canvas.restore()
  • Alpha/Luminance masks use saveLayer() → draw content → apply mask with BlendMode::DstInrestore()

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:

  1. No mask (baseline)
  2. Geometry mask (circular clip)
  3. Alpha mask (radial gradient alpha)
  4. Luminance mask (radial gradient luminance)

Steps to Reproduce

  1. Run the mask example: cargo run --example grida_mask
  2. Modify the example to export to SVG instead of displaying in a window
  3. Inspect the exported SVG file
  4. 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> with mask-mode: alpha
  • Luminance masks - Use saveLayer() + luma color filter + BlendMode::DstIn, do not export to <mask> with mask-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 implementation
  • crates/grida-canvas/src/painter/painter.rs - Mask rendering implementation (lines 841-875)
  • crates/grida-canvas/examples/grida_mask.rs - Demonstration example
  • crates/grida-canvas/examples/golden_sk_mask.rs - Golden test for mask rendering
  • docs/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-safe crate (version in Cargo.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

Metadata

Metadata

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions