Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flutter_svg] Error handling enhancement #8062

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions third_party/packages/flutter_svg/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.0.15
* Adds error handling when parsing an invalid svg string.
* Adds error handling when downloading svg string from network.
* Adds error handling when reading svg file from asset.
* Expose ErrorWidgetBuilder.

## 2.0.14

* Makes the package WASM compatible.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 103 additions & 55 deletions third_party/packages/flutter_svg/example/lib/grid.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

const List<String> _assetNames = <String>[
// 'assets/notfound.svg', // uncomment to test an asset that doesn't exist.
'assets/invalid.svg',
'assets/notfound.svg', // uncomment to test an asset that doesn't exist.
'assets/flutter_logo.svg',
'assets/dart.svg',
'assets/simple/clip_path_3.svg',
Expand Down Expand Up @@ -35,7 +38,7 @@ const List<String> _assetNames = <String>[
];

/// Assets treated as "icons" - using a color filter to render differently.
const List<String> iconNames = <String>[
const List<String> _iconNames = <String>[
'assets/deborah_ufw/new-action-expander.svg',
'assets/deborah_ufw/new-camera.svg',
'assets/deborah_ufw/new-gif-button.svg',
Expand All @@ -49,12 +52,27 @@ const List<String> iconNames = <String>[
];

/// Assets to test network access.
const List<String> uriNames = <String>[
const List<String> _uriNames = <String>[
'http://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg',
'https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/410.svg',
'https://upload.wikimedia.org/wikipedia/commons/b/b4/Chess_ndd45.svg',
];

const List<String> _uriFailedNames = <String>[
'an error image url.svg', // invalid url.
'https: /sadf.svg', // invalid url.
'http://www.google.com/404', // 404 url.
'https://picsum.photos/200', // wrong format image url.
];

const List<String> _stringNames = <String>[
'''<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" height="200" width="200"/></svg>''', // Shows an example of an SVG image that will fetch a raster image from a URL.
'''<svg height="100" width="100" xmlns="http://www.w3.org/2000/svg"> <circle r="45" cx="50" cy="50" fill="red" /> </svg> ''', // valid svg
'''<svg></svg>''', // empty svg.
'sdf sdf ', // invalid svg.
'', // empty string.
];

void main() {
runApp(_MyApp());
}
Expand All @@ -81,59 +99,10 @@ class _MyHomePage extends StatefulWidget {
}

class _MyHomePageState extends State<_MyHomePage> {
final List<Widget> _painters = <Widget>[];
late double _dimension;

@override
void initState() {
super.initState();
_dimension = 203.0;
for (final String assetName in _assetNames) {
_painters.add(
SvgPicture.asset(assetName),
);
}

for (int i = 0; i < iconNames.length; i++) {
_painters.add(
Directionality(
textDirection: TextDirection.ltr,
child: SvgPicture.asset(
iconNames[i],
colorFilter: ColorFilter.mode(
Colors.blueGrey[(i + 1) * 100] ?? Colors.blueGrey,
BlendMode.srcIn,
),
matchTextDirection: true,
),
),
);
}

for (final String uriName in uriNames) {
_painters.add(
SvgPicture.network(
uriName,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
),
);
}
// Shows an example of an SVG image that will fetch a raster image from a URL.
_painters.add(SvgPicture.string('''
<svg viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" height="200" width="200"/>
</svg>'''));
}
double _dimension = 60;

@override
Widget build(BuildContext context) {
if (_dimension > MediaQuery.of(context).size.width - 10.0) {
_dimension = MediaQuery.of(context).size.width - 10.0;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
Expand All @@ -144,7 +113,7 @@ class _MyHomePageState extends State<_MyHomePage> {
max: MediaQuery.of(context).size.width - 10.0,
value: _dimension,
onChanged: (double val) {
setState(() => _dimension = val);
setState(() => _dimension = min(MediaQuery.of(context).size.width - 10.0, val));
},
),
Expanded(
Expand All @@ -154,7 +123,86 @@ class _MyHomePageState extends State<_MyHomePage> {
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
children: _painters.toList(),
children: <Widget>[
..._assetNames.map(
(String e) => SvgPicture.asset(
e,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) => Container(
color: Colors.brown,
width: 10,
height: 10,
),
),
),
..._iconNames.map(
(String e) => Directionality(
textDirection: TextDirection.ltr,
child: SvgPicture.asset(
e,
colorFilter: ColorFilter.mode(
Colors.blueGrey[(_iconNames.indexOf(e) + 1) * 100] ?? Colors.blueGrey,
BlendMode.srcIn,
),
matchTextDirection: true,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) => Container(
color: Colors.yellow,
width: 10,
height: 10,
),
),
),
),
..._uriNames.map(
(String e) => SvgPicture.network(
e,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) => Container(
color: Colors.red,
width: 10,
height: 10,
),
),
),
..._uriFailedNames.map(
(String e) => SvgPicture.network(
e,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) => Container(
color: Colors.deepPurple,
width: 10,
height: 10,
),
),
),
..._stringNames.map(
(String e) => SvgPicture.string(
e,
placeholderBuilder: (BuildContext context) => Container(
padding: const EdgeInsets.all(30.0),
child: const CircularProgressIndicator(),
),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) => Container(
color: Colors.pinkAccent,
width: 10,
height: 10,
),
),
),
],
),
),
]),
Expand Down
66 changes: 42 additions & 24 deletions third_party/packages/flutter_svg/lib/src/loaders.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,29 @@ abstract class SvgLoader<T> extends BytesLoader {
final SvgTheme theme = getTheme(context);
return prepareMessage(context).then((T? message) {
return compute((T? message) {
return vg
.encodeSvg(
xml: provideSvg(message),
theme: theme.toVgTheme(),
colorMapper: colorMapper == null
? null
: _DelegateVgColorMapper(colorMapper!),
debugName: 'Svg loader',
enableClippingOptimizer: false,
enableMaskingOptimizer: false,
enableOverdrawOptimizer: false,
)
.buffer
.asByteData();
try {
debugPrint('SvgLoader._load.provideSvg: empty');
final String xml = provideSvg(message);
if (xml.isEmpty) {
return Future<ByteData>.value(ByteData(0));
} else {
return vg
.encodeSvg(
xml: xml,
theme: theme.toVgTheme(),
colorMapper: colorMapper == null ? null : _DelegateVgColorMapper(colorMapper!),
debugName: 'Svg loader',
enableClippingOptimizer: false,
enableMaskingOptimizer: false,
enableOverdrawOptimizer: false,
)
.buffer
.asByteData();
}
} catch (e) {
debugPrint('SvgLoader._load.error: $e');
return Future<ByteData>.value(ByteData(0));
}
}, message, debugLabel: 'Load Bytes');
});
}
Expand Down Expand Up @@ -373,15 +382,19 @@ class SvgAssetLoader extends SvgLoader<ByteData> {
}

@override
Future<ByteData?> prepareMessage(BuildContext? context) {
return _resolveBundle(context).load(
packageName == null ? assetName : 'packages/$packageName/$assetName',
);
Future<ByteData?> prepareMessage(BuildContext? context) async {
try {
return await _resolveBundle(context).load(
packageName == null ? assetName : 'packages/$packageName/$assetName',
);
} catch (e) {
debugPrint('SvgAssetLoader.prepareMessage.error: $e');
return Future<ByteData?>.value();
}
}

@override
String provideSvg(ByteData? message) =>
utf8.decode(message!.buffer.asUint8List(), allowMalformed: true);
String provideSvg(ByteData? message) => utf8.decode(message!.buffer.asUint8List(), allowMalformed: true);

@override
SvgCacheKey cacheKey(BuildContext? context) {
Expand Down Expand Up @@ -437,13 +450,18 @@ class SvgNetworkLoader extends SvgLoader<Uint8List> {

@override
Future<Uint8List?> prepareMessage(BuildContext? context) async {
final http.Client client = _httpClient ?? http.Client();
return (await client.get(Uri.parse(url), headers: headers)).bodyBytes;
try {
final http.Client client = _httpClient ?? http.Client();
final http.Response res = await client.get(Uri.parse(url), headers: headers);
return res.bodyBytes;
} catch (e) {
debugPrint('SvgNetworkLoader.prepareMessage.error: $e');
return null;
}
}

@override
String provideSvg(Uint8List? message) =>
utf8.decode(message!, allowMalformed: true);
String provideSvg(Uint8List? message) => message == null ? '' : utf8.decode(message, allowMalformed: true);

@override
int get hashCode => Object.hash(url, headers, theme, colorMapper);
Expand Down
18 changes: 18 additions & 0 deletions third_party/packages/flutter_svg/lib/svg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ class SvgPicture extends StatelessWidget {
this.semanticsLabel,
this.excludeFromSemantics = false,
this.clipBehavior = Clip.hardEdge,
this.errorBuilder,
@Deprecated(
'No code should use this parameter. It never was implemented properly. '
'The SVG theme must be set on the bytesLoader.')
SvgTheme? theme,
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
});


/// Instantiates a widget that renders an SVG picture from an [AssetBundle].
///
/// The key will be derived from the `assetName`, `package`, and `bundle`
Expand Down Expand Up @@ -190,6 +192,7 @@ class SvgPicture extends StatelessWidget {
@Deprecated('Use colorFilter instead.')
ui.BlendMode colorBlendMode = ui.BlendMode.srcIn,
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
this.errorBuilder,
}) : bytesLoader = SvgAssetLoader(
assetName,
packageName: package,
Expand Down Expand Up @@ -251,6 +254,7 @@ class SvgPicture extends StatelessWidget {
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
SvgTheme? theme,
http.Client? httpClient,
this.errorBuilder,
}) : bytesLoader = SvgNetworkLoader(
url,
headers: headers,
Expand Down Expand Up @@ -308,6 +312,7 @@ class SvgPicture extends StatelessWidget {
this.clipBehavior = Clip.hardEdge,
SvgTheme? theme,
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
this.errorBuilder,
}) : bytesLoader = SvgFileLoader(file, theme: theme),
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);

Expand Down Expand Up @@ -357,6 +362,7 @@ class SvgPicture extends StatelessWidget {
this.clipBehavior = Clip.hardEdge,
SvgTheme? theme,
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
this.errorBuilder,
}) : bytesLoader = SvgBytesLoader(bytes, theme: theme),
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);

Expand Down Expand Up @@ -406,6 +412,7 @@ class SvgPicture extends StatelessWidget {
this.clipBehavior = Clip.hardEdge,
SvgTheme? theme,
@Deprecated('This no longer does anything.') bool cacheColorFilter = false,
this.errorBuilder,
}) : bytesLoader = SvgStringLoader(string, theme: theme),
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);

Expand Down Expand Up @@ -490,6 +497,9 @@ class SvgPicture extends StatelessWidget {
/// The color filter, if any, to apply to this widget.
final ColorFilter? colorFilter;

/// The widget to show when failed to fetch, decode, and parse the SVG data.
final SvgPictureErrorWidgetBuilder? errorBuilder;

@override
Widget build(BuildContext context) {
return createCompatVectorGraphic(
Expand All @@ -505,6 +515,7 @@ class SvgPicture extends StatelessWidget {
placeholderBuilder: placeholderBuilder,
clipViewbox: !allowDrawingOutsideViewBox,
matchTextDirection: matchTextDirection,
errorBuilder: errorBuilder,
);
}

Expand Down Expand Up @@ -567,3 +578,10 @@ class SvgPicture extends StatelessWidget {
));
}
}

/// The signature that [VectorGraphic.errorBuilder] uses to report exceptions.
typedef SvgPictureErrorWidgetBuilder = Widget Function(
BuildContext context,
Object error,
StackTrace stackTrace,
);
Loading