Skip to content

[vector_graphics_compiler] Support SVG alpha masks (mask-type="alpha") - #13025

Merged
auto-submit[bot] merged 2 commits into
flutter:mainfrom
edwin-hollen:upstream-svg-alpha-mask
Oct 12, 2026
Merged

auto-submit[bot] merged 2 commits into
flutter:mainfrom
edwin-hollen:upstream-svg-alpha-mask

Conversation

@edwin-hollen

Copy link
Copy Markdown
Contributor

[vector_graphics_compiler] Support SVG alpha masks (mask-type="alpha")

Fixes flutter/flutter#158734

This PR adds support for SVG alpha masks (mask-type="alpha" and style="mask-type: alpha") in vector_graphics_compiler, split out from #11909 per review feedback.

Summary of Changes

  • SVG Parser & AST (vector_graphics_compiler):
    • Parses the mask-type attribute (and inline style="mask-type: ..." property) on <mask> elements in SvgParser.
    • Propagates isAlphaMask through ResolvedMaskNode and MaskNode.
    • Emits BlendMode.dstIn for alpha masks instead of BlendMode.srcIn with a luminance-to-alpha color filter matrix (BlendMode.modulate).
  • MaskingOptimizer Compatibility:
    • Updates MaskingOptimizer so single-path masks using gradients with translucent stops (stop-opacity < 1.0) are preserved rather than replaced with solid geometry intersection (which previously discarded the gradient's alpha channel).

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

TAG=agy
CONV=1bad96cc-81aa-438f-98f6-cb909289144a

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2

@github-actions github-actions Bot added p: vector_graphics triage-engine Should be looked at in engine triage labels Sep 26, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for SVG alpha masks (mask-type="alpha") in the vector graphics compiler by parsing and propagating the maskType attribute through the optimizer and visitor passes, and rendering it using a save layer with BlendMode.dstIn. The review feedback suggests restricting the single-path mask optimization for luminance masks to only solid white paths to prevent visual regressions, and trimming and lowercasing the parsed mask-type attribute to ensure robust, case-insensitive matching.

Comment on lines +218 to +223
final bool canOptimizeSingleMaskPath =
singleMaskPathNode != null &&
(maskNode.maskType != 'alpha' ||
(singleMaskPathNode.paint.fill?.shader == null &&
singleMaskPathNode.paint.fill?.color.a == 255 &&
singleMaskPathNode.paint.stroke == null));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

For luminance masks (when maskType != 'alpha'), optimizing a single-path mask to a clip path is only correct if the mask is solid white (i.e., its color is 0xffffffff and it has no shader or stroke). If the luminance mask has a gradient or a non-white color, its luminance is not binary 1.0/0.0, and optimizing it to a clip path will discard the luminance/gradient information, causing visual regressions. We should restrict the optimization for luminance masks to only solid white paths.

    final bool canOptimizeSingleMaskPath = singleMaskPathNode != null &&
        (maskNode.maskType == 'alpha'
            ? (singleMaskPathNode.paint.fill?.shader == null &&
                singleMaskPathNode.paint.fill?.color.a == 255 &&
                singleMaskPathNode.paint.stroke == null)
            : (singleMaskPathNode.paint.fill?.shader == null &&
                singleMaskPathNode.paint.fill?.color == const Color(0xffffffff) &&
                singleMaskPathNode.paint.stroke == null));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please respond, point seems valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! We looked into this carefully: in the existing compiler on main, MaskingOptimizer intentionally simplifies single-path luminance masks even when they have gradients or non-white colors (e.g. existing test Handles masks with blends and gradients correctly explicitly tests that single-path luminance masks with gradients are optimized to path intersections). Restricting luminance masks to solid white 0xffffffff without shaders would break existing optimization behavior for standard luminance masks.

For alpha masks (which this PR introduces), checking singleMaskPathNode.paint.fill?.color.a == 255 ensures we only optimize when the alpha mask is fully opaque, preserving partial opacity, transparent gradients (e.g. stop-opacity < 1.0), and group opacity. Retaining maskNode.maskType != MaskType.alpha || ... keeps existing luminance behavior intact while correctly protecting semi-transparent alpha masks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest creating a separate issue to deal with doing the right thing for luminance masks. It may be existing behavior, but it is not the right behavior, is it?

Also, an optimization that asks the shader if it is "all opaque" might expand the cases where we can use the optimization. We might have to pessimistically respond "no" for custom shaders, but the existing shaders could simply check their colors and tile modes to know if they fully opacify the path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on both points. I can file a follow up issue to track the luminance mask behavior and shader opacity check.

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +218 to +223
final bool canOptimizeSingleMaskPath =
singleMaskPathNode != null &&
(maskNode.maskType != 'alpha' ||
(singleMaskPathNode.paint.fill?.shader == null &&
singleMaskPathNode.paint.fill?.color.a == 255 &&
singleMaskPathNode.paint.stroke == null));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please respond, point seems valid.

@edwin-hollen
edwin-hollen force-pushed the upstream-svg-alpha-mask branch from 466b9cb to b0c037a Compare October 1, 2026 14:49
@edwin-hollen

Copy link
Copy Markdown
Contributor Author

Thanks for the review @gaaclarke! I've addressed all the feedback and pushed an update:

  1. Typed MaskType enum: Added enum MaskType { alpha, luminance } in lib/src/paint.dart (exported via vector_graphics_compiler.dart) and updated SvgParser, SvgAttributes, and ResolvedMaskNode to use MaskType? instead of raw strings.
  2. Robust parsing: parseMaskType trims and lowercases before matching. Unit tests for MaskType parsing (covering whitespace, uppercase, invalid values) have been added to parsers_test.dart and parser_test.dart.
  3. Docstrings: Updated docstrings on ResolvedMaskNode and its maskType property.
  4. Group opacity in alpha masks (<mask mask-type="alpha"><g opacity="0.5"><rect fill="black"/></g></mask>):
    • We verified this and discovered two fixes were needed:
      1. In ParentNode.createLayerPaint(), opacity was only checked via attributes.fill?.opacity, ignoring group-level opacity on <g> elements (which don't have a fill). Updated to check attributes.fill?.opacity ?? attributes.opacity, which properly creates a SaveLayerNode(opacity: 0.5).
      2. In MaskingOptimizer.getSingleChild(), SaveLayerNode was being traversed as a ParentNode (since it extends ParentNode), which would have stripped the SaveLayerNode and treated the inner rect as opaque geometry. Updated getSingleChild() to require node is ParentNode && node is! SaveLayerNode.
    • With those fixes, the mask is properly preserved as a 50% opacity mask rendered via BlendMode.dstIn. A dedicated unit test (Preserves alpha mask with group opacity) was added in masking_optimizer_test.dart.
  5. Luminance mask optimizer (bot feedback): Replied inline. In brief, MaskingOptimizer on main intentionally optimizes single-path luminance masks even with gradients (tested in Handles masks with blends and gradients correctly). Restricting luminance masks to solid white would break existing compiler behavior, so we retain that behavior while restricting alpha mask optimization to fully opaque paths (color.a == 255).
  6. Golden tests in flutter_svg: Yes, definitely! I will follow up with golden tests in flutter_svg once this compiler PR lands.

@gaaclarke
gaaclarke self-requested a review October 1, 2026 16:37
@gaaclarke

Copy link
Copy Markdown
Member

@edwin-hollen friendly reminder please hit "rerequest review"when you are ready. (i did it for you this time)

@gaaclarke

Copy link
Copy Markdown
Member

We verified this and discovered two fixes were needed

BTW, who is "we"?

@gaaclarke gaaclarke added the CICD Run CI/CD label Oct 1, 2026

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, looking good.

Golden tests in flutter_svg: Yes, definitely! I will follow up with golden tests in flutter_svg once this compiler PR lands.

Let's do that as part of this PR.

@edwin-hollen

Copy link
Copy Markdown
Contributor Author

We verified this and discovered two fixes were needed

BTW, who is "we"?

Whoops, 'we' is what happens when I try to use a LLM to write my replies to varying levels of success 😆 I'm working on preparing the goldens this morning!

@edwin-hollen

Copy link
Copy Markdown
Contributor Author

Done! I've added golden widget tests for SVG alpha masks in third_party/packages/flutter_svg/test/widget_svg_test.dart (with corresponding goldens in test/golden_widget/):

  1. alpha_mask.opaque_style.png: Opaque #000000 mask using inline CSS style="mask-type:alpha".
  2. alpha_mask.fill_opacity.png: Semi-transparent mask using mask-type="alpha" and fill-opacity="0.5".
  3. alpha_mask.group_opacity.png: Group opacity inside an alpha mask (<mask mask-type="alpha"><g opacity="0.5">...</g></mask>).
  4. alpha_mask.gradient.png: Linear gradient alpha mask fading stop-opacity from 1 to 0.

I also added a dependency_overrides entry in flutter_svg/pubspec.yaml pointing to vector_graphics_compiler so the new golden tests run against the updated compiler in CI.

@edwin-hollen
edwin-hollen force-pushed the upstream-svg-alpha-mask branch from b0c037a to 7ceab9e Compare October 2, 2026 15:06
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 2, 2026
@edwin-hollen
edwin-hollen requested a review from gaaclarke October 2, 2026 15:07
Comment on lines +26 to +29
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
vector_graphics_compiler: {path: ../../../packages/vector_graphics_compiler}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making this and proving correctness. I think we may have to land this as a separate PR though. We can't land it like this and we have to list a version number which can't happen until the other PR lands. Github has stacked PRs we might be able to use.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've removed the flutter_svg changes from this PR so vector_graphics_compiler (1.3.1) can land first. Since PRs are opened from a fork and the repository currently has a 2-open-PR limit per author, I've saved the flutter_svg golden tests on a separate branch (upstream-svg-alpha-mask-goldens) and will open that follow-up PR as soon as this lands and vector_graphics_compiler 1.3.1 is published.

@gaaclarke gaaclarke added the CICD Run CI/CD label Oct 7, 2026
@gaaclarke
gaaclarke self-requested a review October 7, 2026 17:46
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 7, 2026
@gaaclarke gaaclarke added the CICD Run CI/CD label Oct 9, 2026

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@gaaclarke
gaaclarke requested a review from flar October 9, 2026 22:11
@gaaclarke

Copy link
Copy Markdown
Member

@flar can you be the second reviewer on this please?

@flar

flar commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@flar can you be the second reviewer on this please?

breaking out my Dart/Flutter decoder ring...

@flar flar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some questions. I'll approve pending the answers...

if (node is ResolvedPathNode) {
return node;
} else if (node is ParentNode && node.children.length == 1) {
} else if (node is ParentNode && node is! SaveLayerNode && node.children.length == 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears unrelated to resolving masks, should it be separated into a different PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for reviewing! This is needed for alpha masks with group opacity (e.g. <mask mask-type="alpha"><g opacity="0.5"><path .../></g></mask>). The <g opacity="0.5"> node resolves to a SaveLayerNode. Without node is! SaveLayerNode, getSingleChild walks through the SaveLayerNode and extracts the inner opaque ResolvedPathNode, causing MaskingOptimizer to turn the mask into a path intersection and drop the group opacity.

Comment on lines +218 to +223
final bool canOptimizeSingleMaskPath =
singleMaskPathNode != null &&
(maskNode.maskType != 'alpha' ||
(singleMaskPathNode.paint.fill?.shader == null &&
singleMaskPathNode.paint.fill?.color.a == 255 &&
singleMaskPathNode.paint.stroke == null));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest creating a separate issue to deal with doing the right thing for luminance masks. It may be existing behavior, but it is not the right behavior, is it?

Also, an optimization that asks the shader if it is "all opaque" might expand the cases where we can use the optimization. We might have to pessimistically respond "no" for custom shaders, but the existing shaders could simply check their colors and tile modes to know if they fully opacify the path.

case 'luminance':
return MaskType.luminance;
}
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SVG mask attribute mentioned a default of luminance, is this enforced somewhere else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! parseMaskType returns null when omitted so SvgAttributes.applyParent can inherit parent.maskType when unspecified. Downstream in visitor.dart (visitResolvedMaskNode) and masking_optimizer.dart, any maskType other than MaskType.alpha (null or MaskType.luminance) defaults to the luminance branch (_builder.addMask()).

@edwin-hollen
edwin-hollen requested a review from flar October 12, 2026 14:00

@flar flar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@gaaclarke gaaclarke added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 12, 2026
@auto-submit
auto-submit Bot merged commit c63648d into flutter:main Oct 12, 2026
14 checks passed
andywolff pushed a commit to andywolff/flutter that referenced this pull request Oct 13, 2026
flutter/packages@d8d9e97...a1ed23c

2026-10-13 [email protected] [material_ui] Clarify Slider labels
for continuous sliders (flutter/packages#13121)
2026-10-12 [email protected] [in_app_purchase_storekit] Remove
availability checks below the package minimum iOS 13 and macOS 10.15
(flutter/packages#13217)
2026-10-12 [email protected] [google_sign_in] Convert unit tests
to Kotlin (flutter/packages#13000)
2026-10-12 [email protected]
[vector_graphics_compiler] Support SVG alpha masks (mask-type="alpha")
(flutter/packages#13025)
2026-10-11 [email protected] [camera_platform_interface] Add
zero-shutter-lag API (flutter/packages#12996)
2026-10-10 [email protected] [material_ui]
Ensure textScaler is taken into account for SnackBar action overflow
(flutter/packages#12737)
2026-10-10 [email protected] [material_ui] Add more 0x0 size tests
part 14 (flutter/packages#13107)
2026-10-10 [email protected]
[cupertino_ui] Fix CupertinoPicker throwing when disposed during the
tap-to-select animation (flutter/packages#13012)
2026-10-09 [email protected] Roll Flutter (stable) from
5fc3468 to abaf9c5 (3 revisions) (flutter/packages#13186)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
auto-submit Bot pushed a commit that referenced this pull request Oct 15, 2026
Adds golden widget tests in `flutter_svg` for SVG alpha masks (`mask-type="alpha"` and `style="mask-type:alpha"`) and bumps the `vector_graphics_compiler` dependency constraint to `^1.3.1`.

Follow-up to #13025
Part of flutter/flutter#158734

### Added Golden Tests
* `alpha_mask.opaque_style.png`: Opaque `#000000` mask using inline CSS `style="mask-type:alpha"`.
* `alpha_mask.fill_opacity.png`: Semi-transparent mask using `mask-type="alpha"` and `fill-opacity="0.5"`.
* `alpha_mask.group_opacity.png`: Group opacity inside an alpha mask (`<mask mask-type="alpha"><g opacity="0.5">...</g></mask>`).
* `alpha_mask.gradient.png`: Linear gradient alpha mask fading `stop-opacity` from `1` to `0`.

## Pre-Review Checklist
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
flutter/packages@d8d9e97...a1ed23c

2026-10-13 [email protected] [material_ui] Clarify Slider labels
for continuous sliders (flutter/packages#13121)
2026-10-12 [email protected] [in_app_purchase_storekit] Remove
availability checks below the package minimum iOS 13 and macOS 10.15
(flutter/packages#13217)
2026-10-12 [email protected] [google_sign_in] Convert unit tests
to Kotlin (flutter/packages#13000)
2026-10-12 [email protected]
[vector_graphics_compiler] Support SVG alpha masks (mask-type="alpha")
(flutter/packages#13025)
2026-10-11 [email protected] [camera_platform_interface] Add
zero-shutter-lag API (flutter/packages#12996)
2026-10-10 [email protected] [material_ui]
Ensure textScaler is taken into account for SnackBar action overflow
(flutter/packages#12737)
2026-10-10 [email protected] [material_ui] Add more 0x0 size tests
part 14 (flutter/packages#13107)
2026-10-10 [email protected]
[cupertino_ui] Fix CupertinoPicker throwing when disposed during the
tap-to-select animation (flutter/packages#13012)
2026-10-09 [email protected] Roll Flutter (stable) from
5fc3468 to abaf9c5 (3 revisions) (flutter/packages#13186)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: flutter_svg p: vector_graphics triage-engine Should be looked at in engine triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[flutter_svg] Support mask-type:alpha

3 participants