diff --git a/AUTHORS b/AUTHORS index be6c57cfb16..b60279e3f60 100644 --- a/AUTHORS +++ b/AUTHORS @@ -78,3 +78,4 @@ Taskulu LDA Alexander Rabin LinXunFeng Hashir Shoaib +Ricardo Dalarme diff --git a/third_party/packages/flutter_svg/CHANGELOG.md b/third_party/packages/flutter_svg/CHANGELOG.md index 0f9dd9ee901..3e58b61b30f 100644 --- a/third_party/packages/flutter_svg/CHANGELOG.md +++ b/third_party/packages/flutter_svg/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Exposes `colorMapper` in `SvgPicture` constructors. + ## 2.0.17 * Implement errorBuilder callback diff --git a/third_party/packages/flutter_svg/README.md b/third_party/packages/flutter_svg/README.md index d4d205c3eb0..e25ca450bfa 100644 --- a/third_party/packages/flutter_svg/README.md +++ b/third_party/packages/flutter_svg/README.md @@ -33,6 +33,53 @@ final Widget svgIcon = SvgPicture.asset( ); ``` +For more advanced color manipulation, you can use the `colorMapper` property. +This allows you to define a custom mapping function that will be called for +every color encountered during SVG parsing, enabling you to substitute colors +based on various criteria like the color value itself, the element name, or the +attribute name. + +To use this feature, you need to create a class that extends `ColorMapper` and +override the `substitute` method. + +Here's an example of how to implement a `ColorMapper` to replace specific colors in an SVG: + + +```dart +class _MyColorMapper extends ColorMapper { + const _MyColorMapper(); + + @override + Color substitute( + String? id, + String elementName, + String attributeName, + Color color, + ) { + if (color == const Color(0xFFFF0000)) { + return Colors.blue; + } + if (color == const Color(0xFF00FF00)) { + return Colors.yellow; + } + return color; + } +} +// ··· + const String svgString = ''' + + + + +'''; + final Widget svgIcon = SvgPicture.string( + svgString, + colorMapper: const _MyColorMapper(), + ); +``` + +In this example, all red colors in the SVG will be rendered as blue, and all green colors will be rendered as yellow. You can customize the `substitute` method to implement more complex color mapping logic based on your requirements. + The default placeholder is an empty box (`LimitedBox`) - although if a `height` or `width` is specified on the `SvgPicture`, a `SizedBox` will be used instead (which ensures better layout experience). There is currently no way to show an @@ -67,6 +114,7 @@ If you'd like to render the SVG to some other canvas, you can do something like: ```dart import 'dart:ui' as ui; + // ··· const String rawSvg = '''...'''; final PictureInfo pictureInfo = diff --git a/third_party/packages/flutter_svg/example/lib/readme_excerpts.dart b/third_party/packages/flutter_svg/example/lib/readme_excerpts.dart index fc17e62abe3..7fd7f9132ee 100644 --- a/third_party/packages/flutter_svg/example/lib/readme_excerpts.dart +++ b/third_party/packages/flutter_svg/example/lib/readme_excerpts.dart @@ -7,6 +7,7 @@ // #docregion OutputConversion import 'dart:ui' as ui; + // #enddocregion OutputConversion import 'package:flutter/material.dart'; @@ -101,3 +102,42 @@ Future convertSvgOutput() async { // #enddocregion OutputConversion return image; } + +// #docregion ColorMapper +class _MyColorMapper extends ColorMapper { + const _MyColorMapper(); + + @override + Color substitute( + String? id, + String elementName, + String attributeName, + Color color, + ) { + if (color == const Color(0xFFFF0000)) { + return Colors.blue; + } + if (color == const Color(0xFF00FF00)) { + return Colors.yellow; + } + return color; + } +} +// #enddocregion ColorMapper + +/// Demonstrates loading an SVG asset with a color mapping. +Widget loadWithColorMapper() { + // #docregion ColorMapper + const String svgString = ''' + + + + +'''; + final Widget svgIcon = SvgPicture.string( + svgString, + colorMapper: const _MyColorMapper(), + ); + // #enddocregion ColorMapper + return svgIcon; +} diff --git a/third_party/packages/flutter_svg/lib/svg.dart b/third_party/packages/flutter_svg/lib/svg.dart index 0cb5bb071ee..b1343f47b8e 100644 --- a/third_party/packages/flutter_svg/lib/svg.dart +++ b/third_party/packages/flutter_svg/lib/svg.dart @@ -195,6 +195,7 @@ class SvgPicture extends StatelessWidget { this.clipBehavior = Clip.hardEdge, this.errorBuilder, SvgTheme? theme, + ColorMapper? colorMapper, ui.ColorFilter? colorFilter, @Deprecated('Use colorFilter instead.') ui.Color? color, @Deprecated('Use colorFilter instead.') @@ -205,6 +206,7 @@ class SvgPicture extends StatelessWidget { packageName: package, assetBundle: bundle, theme: theme, + colorMapper: colorMapper, ), colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode); @@ -261,11 +263,13 @@ class SvgPicture extends StatelessWidget { this.errorBuilder, @Deprecated('This no longer does anything.') bool cacheColorFilter = false, SvgTheme? theme, + ColorMapper? colorMapper, http.Client? httpClient, }) : bytesLoader = SvgNetworkLoader( url, headers: headers, theme: theme, + colorMapper: colorMapper, httpClient: httpClient, ), colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode); @@ -319,8 +323,13 @@ class SvgPicture extends StatelessWidget { this.clipBehavior = Clip.hardEdge, this.errorBuilder, SvgTheme? theme, + ColorMapper? colorMapper, @Deprecated('This no longer does anything.') bool cacheColorFilter = false, - }) : bytesLoader = SvgFileLoader(file, theme: theme), + }) : bytesLoader = SvgFileLoader( + file, + theme: theme, + colorMapper: colorMapper, + ), colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode); /// Creates a widget that displays an SVG obtained from a [Uint8List]. @@ -369,8 +378,13 @@ class SvgPicture extends StatelessWidget { this.clipBehavior = Clip.hardEdge, this.errorBuilder, SvgTheme? theme, + ColorMapper? colorMapper, @Deprecated('This no longer does anything.') bool cacheColorFilter = false, - }) : bytesLoader = SvgBytesLoader(bytes, theme: theme), + }) : bytesLoader = SvgBytesLoader( + bytes, + theme: theme, + colorMapper: colorMapper, + ), colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode); /// Creates a widget that displays an SVG obtained from a [String]. @@ -419,8 +433,13 @@ class SvgPicture extends StatelessWidget { this.clipBehavior = Clip.hardEdge, this.errorBuilder, SvgTheme? theme, + ColorMapper? colorMapper, @Deprecated('This no longer does anything.') bool cacheColorFilter = false, - }) : bytesLoader = SvgStringLoader(string, theme: theme), + }) : bytesLoader = SvgStringLoader( + string, + theme: theme, + colorMapper: colorMapper, + ), colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode); static ColorFilter? _getColorFilter( diff --git a/third_party/packages/flutter_svg/pubspec.yaml b/third_party/packages/flutter_svg/pubspec.yaml index ca33c7e36d1..b72740d0f68 100644 --- a/third_party/packages/flutter_svg/pubspec.yaml +++ b/third_party/packages/flutter_svg/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_svg description: An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. repository: https://github.com/flutter/packages/tree/main/third_party/packages/flutter_svg issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_svg%22 -version: 2.0.17 +version: 2.1.0 environment: sdk: ^3.4.0 diff --git a/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.asset.color_mapper.png b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.asset.color_mapper.png new file mode 100644 index 00000000000..8af44b8b9f1 Binary files /dev/null and b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.asset.color_mapper.png differ diff --git a/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.memory.color_mapper.png b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.memory.color_mapper.png new file mode 100644 index 00000000000..8af44b8b9f1 Binary files /dev/null and b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.memory.color_mapper.png differ diff --git a/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.network.color_mapper.png b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.network.color_mapper.png new file mode 100644 index 00000000000..8af44b8b9f1 Binary files /dev/null and b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.network.color_mapper.png differ diff --git a/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.string.color_mapper.png b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.string.color_mapper.png new file mode 100644 index 00000000000..8af44b8b9f1 Binary files /dev/null and b/third_party/packages/flutter_svg/test/golden_widget/flutter_logo.string.color_mapper.png differ diff --git a/third_party/packages/flutter_svg/test/widget_svg_test.dart b/third_party/packages/flutter_svg/test/widget_svg_test.dart index 94edb83a1f3..d0f60ad65c7 100644 --- a/third_party/packages/flutter_svg/test/widget_svg_test.dart +++ b/third_party/packages/flutter_svg/test/widget_svg_test.dart @@ -39,6 +39,29 @@ Future _checkWidgetAndGolden(Key key, String filename) async { await expectLater(widgetFinder, matchesGoldenFile('golden_widget/$filename')); } +class _TestColorMapper extends ColorMapper { + const _TestColorMapper(); + + /// Substitutes specific colors for testing the SVG rendering. + @override + Color substitute( + String? id, String elementName, String attributeName, Color color) { + if (color == const Color(0xFF42A5F5)) { + return const Color(0xFF00FF00); // Green + } + if (color == const Color(0xFF0D47A1)) { + return const Color(0xFFFF0000); // Red + } + if (color == const Color(0xFF616161)) { + return const Color(0xFF0000FF); // Blue + } + if (color == const Color(0xFF000000)) { + return const Color(0xFFFFFF00); // Yellow + } + return color; + } +} + void main() { final MediaQueryData mediaQueryData = MediaQueryData.fromView(PlatformDispatcher.instance.implicitView!); @@ -116,6 +139,28 @@ void main() { await _checkWidgetAndGolden(key, 'flutter_logo.string.png'); }); + testWidgets('SvgPicture.string with colorMapper', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MediaQuery( + data: mediaQueryData, + child: RepaintBoundary( + key: key, + child: SvgPicture.string( + svgStr, + width: 100.0, + height: 100.0, + colorMapper: const _TestColorMapper(), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + await _checkWidgetAndGolden(key, 'flutter_logo.string.color_mapper.png'); + }); + testWidgets('SvgPicture natural size', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -250,6 +295,26 @@ void main() { await _checkWidgetAndGolden(key, 'flutter_logo.memory.png'); }); + testWidgets('SvgPicture.memory with colorMapper', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MediaQuery( + data: mediaQueryData, + child: RepaintBoundary( + key: key, + child: SvgPicture.memory( + svgBytes, + colorMapper: const _TestColorMapper(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await _checkWidgetAndGolden(key, 'flutter_logo.memory.color_mapper.png'); + }); + testWidgets('SvgPicture.asset', (WidgetTester tester) async { final FakeAssetBundle fakeAsset = FakeAssetBundle(); final GlobalKey key = GlobalKey(); @@ -269,6 +334,26 @@ void main() { await _checkWidgetAndGolden(key, 'flutter_logo.asset.png'); }); + testWidgets('SvgPicture.asset with colorMapper', (WidgetTester tester) async { + final FakeAssetBundle fakeAsset = FakeAssetBundle(); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MediaQuery( + data: mediaQueryData, + child: RepaintBoundary( + key: key, + child: SvgPicture.asset( + 'test.svg', + bundle: fakeAsset, + colorMapper: const _TestColorMapper(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await _checkWidgetAndGolden(key, 'flutter_logo.asset.color_mapper.png'); + }); + testWidgets('SvgPicture.asset DefaultAssetBundle', (WidgetTester tester) async { final FakeAssetBundle fakeAsset = FakeAssetBundle(); @@ -295,6 +380,33 @@ void main() { await _checkWidgetAndGolden(key, 'flutter_logo.asset.png'); }); + testWidgets('SvgPicture.asset DefaultAssetBundle with colorMapper', + (WidgetTester tester) async { + final FakeAssetBundle fakeAsset = FakeAssetBundle(); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: mediaQueryData, + child: DefaultAssetBundle( + bundle: fakeAsset, + child: RepaintBoundary( + key: key, + child: SvgPicture.asset( + 'test.svg', + semanticsLabel: 'Test SVG', + colorMapper: const _TestColorMapper(), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await _checkWidgetAndGolden(key, 'flutter_logo.asset.color_mapper.png'); + }); + testWidgets('SvgPicture.network', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget( @@ -313,6 +425,26 @@ void main() { await _checkWidgetAndGolden(key, 'flutter_logo.network.png'); }); + testWidgets('SvgPicture.network with colorMapper', + (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + MediaQuery( + data: mediaQueryData, + child: RepaintBoundary( + key: key, + child: SvgPicture.network( + 'test.svg', + httpClient: FakeHttpClient(), + colorMapper: const _TestColorMapper(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await _checkWidgetAndGolden(key, 'flutter_logo.network.color_mapper.png'); + }); + testWidgets('SvgPicture.network with headers', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); final FakeHttpClient client = FakeHttpClient();