Skip to content

Commit f4e0eff

Browse files
committed
Support SVG filters (blur, offset, feMerge) and alpha masks
• Added support for parsing and resolving SVG filters including feGaussianBlur, feOffset, and feMerge. • Added support for alpha masks (mask-type=alpha). • Updated vector_graphics, vector_graphics_codec, and vector_graphics_compiler packages to support encoding/decoding and rendering of paint blurs. • Fixed handling of directional blurs where one axis is zero (e.g., stdDeviation="25 0"). • Fixed text rendering alignment shift by resolving filters at the TextPositionNode level. • Addressed PR review feedback: - Supported comma-separated stdDeviation values. - Implemented default 'in' attribute for feMergeNode (fallback to previous primitive's result or SourceGraphic). - Preserved original fill/stroke opacity in SourceAlpha filters. - Added cycle detection during filter tracing to prevent stack overflow. - Refactored feMerge to be parsed as a primitive, enabling arbitrary DAG chaining (not just as final operation). - Implemented multi-layer filter support for group nodes (ParentNode) by duplicating children resolution per layer. - Removed redundant layers.isNotEmpty check in parser. • Added comprehensive unit tests and updated the example app with a filter gallery. TAG=agy CONV=27cb188a-9ad9-4434-8918-8ef71bdba17c
1 parent aa964a3 commit f4e0eff

19 files changed

Lines changed: 1627 additions & 60 deletions

packages/vector_graphics/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.2.3
22

3+
* Adds support for SVG filters (blur) and mask-type=alpha.
34
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.
45

56
## 1.2.2

packages/vector_graphics/example/lib/main.dart

Lines changed: 222 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,139 @@
1-
// Copyright 2013 The Flutter Authors
2-
// Use of this source code is governed by a BSD-style license that can be
3-
// found in the LICENSE file.
4-
51
import 'dart:developer';
6-
72
import 'package:flutter/foundation.dart';
83
import 'package:flutter/material.dart';
94
import 'package:http/http.dart' as http;
105
import 'package:vector_graphics/vector_graphics.dart';
11-
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';
6+
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart' show encodeSvg;
127

138
void main() {
149
runApp(const MyApp());
1510
}
1611

12+
const String _svgDropShadow = '''
13+
<!-- Feature: Classic Drop Shadow via feGaussianBlur, feOffset, and feMerge -->
14+
<svg width="400" height="200" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
15+
<defs>
16+
<filter id="drop-shadow" x="-20%" y="-20%" width="140%" height="140%">
17+
<!-- 1. Blur the original graphic source alpha channel -->
18+
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur" />
19+
<!-- 2. Shift the blurred shadow down and right -->
20+
<feOffset in="blur" dx="5" dy="8" result="offsetBlur"/>
21+
<!-- 3. Layer the original graphic on top of the shadow -->
22+
<feMerge>
23+
<feMergeNode in="offsetBlur" />
24+
<feMergeNode in="SourceGraphic" />
25+
</feMerge>
26+
</filter>
27+
</defs>
28+
29+
<rect x="50" y="40" width="300" height="100" rx="15" fill="#4F46E5" filter="url(#drop-shadow)" />
30+
</svg>
31+
''';
32+
33+
const String _svgNeonText = '''
34+
<!-- Feature: Glowing Neon Text Effect using multiple layered feGaussianBlur outputs -->
35+
<svg width="500" height="200" viewBox="0 0 500 200" xmlns="http://www.w3.org/2000/svg">
36+
<defs>
37+
<filter id="neon-glow" x="-50%" y="-50%" width="200%" height="200%">
38+
<!-- Blur the graphic significantly to create the wide background glow -->
39+
<feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blur1" />
40+
<!-- Create a sharper, more intense core glow -->
41+
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur2" />
42+
43+
<!-- Merge the layers: wide glow, sharp glow, then crisp original text -->
44+
<feMerge>
45+
<feMergeNode in="blur1" />
46+
<feMergeNode in="blur2" />
47+
<feMergeNode in="SourceGraphic" />
48+
</feMerge>
49+
</filter>
50+
</defs>
51+
52+
<rect width="100%" height="100%" fill="#0B0F19" />
53+
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" fill="#FFF" stroke="#FF2E93" stroke-width="2" font-family="Courier New" font-size="64" font-weight="bold" filter="url(#neon-glow)">
54+
OPEN 24/7
55+
</text>
56+
</svg>
57+
''';
58+
59+
const String _svgMeshGradient = '''
60+
<!-- Feature: Smooth UI Background Gradient ("Mesh" Style) via high stdDeviation blur -->
61+
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg">
62+
<defs>
63+
<!-- High standard deviation blends the distinct shapes seamlessly -->
64+
<filter id="heavy-blur">
65+
<feGaussianBlur stdDeviation="80" />
66+
</filter>
67+
</defs>
68+
69+
<!-- Base background color -->
70+
<rect width="100%" height="100%" fill="#0F172A" />
71+
72+
<!-- Blurring container holding the colored blobs -->
73+
<g filter="url(#heavy-blur)">
74+
<circle cx="150" cy="120" r="130" fill="#38BDF8" opacity="0.6" />
75+
<circle cx="450" cy="280" r="160" fill="#EC4899" opacity="0.5" />
76+
<circle cx="300" cy="200" r="110" fill="#6366F1" opacity="0.6" />
77+
</g>
78+
</svg>
79+
''';
80+
81+
const String _svgGlassmorphism = '''
82+
<!-- Feature: Glassmorphism / Frosted Glass Panel overlay using clipped feGaussianBlur shapes -->
83+
<svg width="500" height="350" viewBox="0 0 500 350" xmlns="http://www.w3.org/2000/svg">
84+
<defs>
85+
<!-- A standard background blur filter -->
86+
<filter id="glass-blur">
87+
<feGaussianBlur stdDeviation="20" />
88+
</filter>
89+
90+
<!-- Clip path to match our UI card dimensions -->
91+
<clipPath id="card-shape">
92+
<rect x="100" y="75" width="300" height="200" rx="20" />
93+
</clipPath>
94+
</defs>
95+
96+
<!-- Colorful backdrop layer -->
97+
<rect width="100%" height="100%" fill="#1E1E2F" />
98+
<circle cx="160" cy="130" r="90" fill="#FF5E62" />
99+
<circle cx="360" cy="220" r="100" fill="#FF9966" />
100+
101+
<!-- The Frosted Glass Card Technique -->
102+
<!-- 1. Duplicate background layer clipped and heavily blurred -->
103+
<g clip-path="url(#card-shape)">
104+
<rect width="100%" height="100%" fill="#1E1E2F" />
105+
<circle cx="160" cy="130" r="90" fill="#FF5E62" filter="url(#glass-blur)" />
106+
<circle cx="360" cy="220" r="100" fill="#FF9966" filter="url(#glass-blur)" />
107+
108+
<!-- 2. Semi-transparent white tint fill for the frosted appearance -->
109+
<rect x="100" y="75" width="300" height="200" fill="#FFFFFF" opacity="0.07" />
110+
</g>
111+
112+
<!-- 3. Crisp white border stroke overlaying the card to establish depth -->
113+
<rect x="100" y="75" width="300" height="200" rx="20" fill="none" stroke="#FFFFFF" stroke-width="1.5" opacity="0.25" />
114+
</svg>
115+
''';
116+
117+
const String _svgMotionBlur = '''
118+
<!-- Feature: Directional Motion Blur using decoupled axes in stdDeviation="X Y" -->
119+
<svg width="500" height="150" viewBox="0 0 500 150" xmlns="http://www.w3.org/2000/svg">
120+
<defs>
121+
<!-- stdDeviation="Horizontal Vertical". Setting vertical to 0 creates a horizontal streak -->
122+
<filter id="motion-blur-x" x="-50%" y="0%" width="200%" height="100%">
123+
<feGaussianBlur stdDeviation="25 0" />
124+
</filter>
125+
</defs>
126+
127+
<rect width="100%" height="100%" fill="#111827" />
128+
129+
<!-- The trailing motion streak -->
130+
<circle cx="300" cy="75" r="30" fill="#F59E0B" filter="url(#motion-blur-x)" opacity="0.8" />
131+
132+
<!-- The crisp leading object -->
133+
<circle cx="360" cy="75" r="30" fill="#F59E0B" />
134+
</svg>
135+
''';
136+
17137
/// The main example app widget.
18138
class MyApp extends StatelessWidget {
19139
/// Creates a new [MyApp].
@@ -23,12 +143,70 @@ class MyApp extends StatelessWidget {
23143
Widget build(BuildContext context) {
24144
return MaterialApp(
25145
title: 'Vector Graphics Demo',
26-
theme: ThemeData(primarySwatch: Colors.blue),
27-
home: const Scaffold(
28-
body: Center(
29-
child: VectorGraphic(
30-
loader: NetworkSvgLoader(
31-
'https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg',
146+
theme: ThemeData.dark(),
147+
home: Scaffold(
148+
appBar: AppBar(title: const Text('Filter & Mask Gallery')),
149+
body: const SingleChildScrollView(
150+
child: Padding(
151+
padding: EdgeInsets.all(16.0),
152+
child: Column(
153+
crossAxisAlignment: CrossAxisAlignment.start,
154+
children: [
155+
Text('0. Original Ghostscript Tiger (Network SVG Loader)', style: TextStyle(fontWeight: FontWeight.bold)),
156+
SizedBox(height: 8),
157+
SizedBox(
158+
height: 200,
159+
width: 200,
160+
child: VectorGraphic(
161+
loader: NetworkSvgLoader(
162+
'https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg',
163+
),
164+
),
165+
),
166+
Divider(height: 32),
167+
Text('1. Classic Drop Shadow (feGaussianBlur, feOffset, feMerge)', style: TextStyle(fontWeight: FontWeight.bold)),
168+
SizedBox(height: 8),
169+
SizedBox(
170+
height: 200,
171+
width: 400,
172+
child: DecoratedBox(
173+
decoration: BoxDecoration(color: Colors.white),
174+
child: VectorGraphic(loader: _StringSvgLoader(_svgDropShadow)),
175+
),
176+
),
177+
Divider(height: 32),
178+
Text('2. Glowing Neon Text (Multiple feGaussianBlur, feMerge)', style: TextStyle(fontWeight: FontWeight.bold)),
179+
SizedBox(height: 8),
180+
SizedBox(
181+
height: 200,
182+
width: 500,
183+
child: VectorGraphic(loader: _StringSvgLoader(_svgNeonText)),
184+
),
185+
Divider(height: 32),
186+
Text('3. Smooth UI Background Gradient (Mesh style blur on Group)', style: TextStyle(fontWeight: FontWeight.bold)),
187+
SizedBox(height: 8),
188+
SizedBox(
189+
height: 400,
190+
width: 600,
191+
child: VectorGraphic(loader: _StringSvgLoader(_svgMeshGradient)),
192+
),
193+
Divider(height: 32),
194+
Text('4. Glassmorphism (Clipped group with blur)', style: TextStyle(fontWeight: FontWeight.bold)),
195+
SizedBox(height: 8),
196+
SizedBox(
197+
height: 350,
198+
width: 500,
199+
child: VectorGraphic(loader: _StringSvgLoader(_svgGlassmorphism)),
200+
),
201+
Divider(height: 32),
202+
Text('5. Directional Motion Blur (stdDeviation="25 0")', style: TextStyle(fontWeight: FontWeight.bold)),
203+
SizedBox(height: 8),
204+
SizedBox(
205+
height: 150,
206+
width: 500,
207+
child: VectorGraphic(loader: _StringSvgLoader(_svgMotionBlur)),
208+
),
209+
],
32210
),
33211
),
34212
),
@@ -37,6 +215,37 @@ class MyApp extends StatelessWidget {
37215
}
38216
}
39217

218+
class _StringSvgLoader extends BytesLoader {
219+
const _StringSvgLoader(this.svg);
220+
final String svg;
221+
222+
@override
223+
Future<ByteData> loadBytes(BuildContext? context) async {
224+
return compute(
225+
(String svgStr) {
226+
final Uint8List compiledBytes = encodeSvg(
227+
xml: svgStr,
228+
debugName: 'custom-svg',
229+
enableClippingOptimizer: false,
230+
enableMaskingOptimizer: false,
231+
enableOverdrawOptimizer: false,
232+
);
233+
return ByteData.sublistView(compiledBytes);
234+
},
235+
svg,
236+
debugLabel: 'Load String Bytes',
237+
);
238+
}
239+
240+
@override
241+
int get hashCode => svg.hashCode;
242+
243+
@override
244+
bool operator ==(Object other) {
245+
return other is _StringSvgLoader && other.svg == svg;
246+
}
247+
}
248+
40249
/// A [BytesLoader] that converts a network URL into encoded SVG data.
41250
class NetworkSvgLoader extends BytesLoader {
42251
/// Creates a [NetworkSvgLoader] that loads an SVG from [url].
@@ -60,7 +269,7 @@ class NetworkSvgLoader extends BytesLoader {
60269
);
61270
task.finish();
62271
// sendAndExit will make sure this isn't copied.
63-
return compiledBytes.buffer.asByteData();
272+
return ByteData.sublistView(compiledBytes);
64273
},
65274
url,
66275
debugLabel: 'Load Bytes',

packages/vector_graphics/lib/src/listener.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener {
393393
_paints.add(paint);
394394
}
395395

396+
@override
397+
void onPaintBlur(int paintId, double sigmaX, double sigmaY) {
398+
_paints[paintId].imageFilter = ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY);
399+
}
400+
396401
@override
397402
void onPathClose() {
398403
_currentPath!.close();

packages/vector_graphics/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: vector_graphics
22
description: A vector graphics rendering package for Flutter using a binary encoding.
33
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
5-
version: 1.2.2
5+
version: 1.2.3
66

77
environment:
88
sdk: ^3.10.0

packages/vector_graphics/test/listener_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,39 @@ void main() {
187187
await listener.waitForImageDecode();
188188
expect(() => listener.onDrawImage(2, 10, 10, 100, 100, null), throwsAssertionError);
189189
});
190+
191+
test('Paint blur is applied to paint', () async {
192+
final factory = TestPictureFactory();
193+
final listener = FlutterVectorGraphicsListener(pictureFactory: factory);
194+
195+
listener.onPaintObject(
196+
color: const ui.Color(0xff000000).toARGB32(),
197+
strokeCap: null,
198+
strokeJoin: null,
199+
blendMode: BlendMode.srcIn.index,
200+
strokeMiterLimit: null,
201+
strokeWidth: null,
202+
paintStyle: ui.PaintingStyle.fill.index,
203+
id: 0,
204+
shaderId: null,
205+
);
206+
207+
listener.onPaintBlur(0, 5.0, 10.0);
208+
209+
listener.onPathStart(0, 0);
210+
listener.onPathMoveTo(0, 0);
211+
listener.onPathLineTo(10, 10);
212+
listener.onPathFinished();
213+
214+
await listener.onDrawPath(0, 0, null);
215+
216+
final Invocation drawPath = factory.fakeCanvases.single.invocations.single;
217+
expect(drawPath.isMethod, true);
218+
expect(drawPath.memberName, #drawPath);
219+
final paint = drawPath.positionalArguments[1] as ui.Paint;
220+
expect(paint.imageFilter, isNotNull);
221+
expect(paint.imageFilter, ui.ImageFilter.blur(sigmaX: 5.0, sigmaY: 10.0));
222+
});
190223
}
191224

192225
class TestPictureFactory implements PictureFactory {

packages/vector_graphics_codec/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.1.14
22

3+
* Adds support for SVG filters (blur) and mask-type=alpha.
34
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.
45

56
## 1.1.13

packages/vector_graphics_codec/lib/vector_graphics_codec.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class VectorGraphicsCodec {
126126
static const int _textPositionTag = 50;
127127
static const int _updateTextPositionTag = 51;
128128
static const int _pathTagHalfPrecision = 52;
129+
static const int _paintBlurTag = 53;
129130

130131
static const int _version = 1;
131132
static const int _magicNumber = 0x00882d62;
@@ -230,6 +231,9 @@ class VectorGraphicsCodec {
230231
case _updateTextPositionTag:
231232
_readUpdateTextPosition(buffer, listener);
232233
continue;
234+
case _paintBlurTag:
235+
_readPaintBlur(buffer, listener);
236+
continue;
233237
default:
234238
throw StateError('Unknown type tag $type');
235239
}
@@ -959,6 +963,22 @@ class VectorGraphicsCodec {
959963
final Float64List? transform = buffer.getTransform();
960964
listener?.onPatternStart(patternId, x, y, width, height, transform!);
961965
}
966+
967+
/// Write a paint blur command to the buffer.
968+
void writePaintBlur(VectorGraphicsBuffer buffer, int paintId, double sigmaX, double sigmaY) {
969+
buffer._checkPhase(_CurrentSection.paints);
970+
buffer._putUint8(_paintBlurTag);
971+
buffer._putUint16(paintId);
972+
buffer._putFloat32(sigmaX);
973+
buffer._putFloat32(sigmaY);
974+
}
975+
976+
void _readPaintBlur(_ReadBuffer buffer, VectorGraphicsCodecListener? listener) {
977+
final int paintId = buffer.getUint16();
978+
final double sigmaX = buffer.getFloat32();
979+
final double sigmaY = buffer.getFloat32();
980+
listener?.onPaintBlur(paintId, sigmaX, sigmaY);
981+
}
962982
}
963983

964984
/// Implement this listener class to support decoding of vector_graphics binary
@@ -1028,6 +1048,9 @@ abstract class VectorGraphicsCodecListener {
10281048
/// Prepare to draw a new mask, until the next [onRestoreLayer] command.
10291049
void onMask();
10301050

1051+
/// A paint blur has been decoded.
1052+
void onPaintBlur(int paintId, double sigmaX, double sigmaY) {}
1053+
10311054
/// A radial gradient shader has been parsed.
10321055
///
10331056
/// [focalX] and [focalY] are either both `null` or `non-null`.

0 commit comments

Comments
 (0)