Skip to content

[flutter_svg] feat: Expose the colorMapper property in SvgPicture #9043

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

Merged
merged 3 commits into from
Apr 18, 2025
Merged
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ Taskulu LDA <contributions@taskulu.com>
Alexander Rabin <alex.rabin@sentracam.com>
LinXunFeng <linxunfeng@yeah.net>
Hashir Shoaib <hashirshoaeb@gmail.com>
Ricardo Dalarme <ricardodalarme@outlook.com>
4 changes: 4 additions & 0 deletions third_party/packages/flutter_svg/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.1.0

* Exposes `colorMapper` in `SvgPicture` constructors.

## 2.0.17

* Implement errorBuilder callback
Expand Down
48 changes: 48 additions & 0 deletions third_party/packages/flutter_svg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<?code-excerpt "example/lib/readme_excerpts.dart (ColorMapper)"?>
```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 = '''
<svg viewBox="0 0 100 100">
<rect width="50" height="50" fill="#FF0000" />
<circle cx="75" cy="75" r="25" fill="#00FF00" />
</svg>
''';
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
Expand Down Expand Up @@ -67,6 +114,7 @@ If you'd like to render the SVG to some other canvas, you can do something like:
<?code-excerpt "example/lib/readme_excerpts.dart (OutputConversion)"?>
```dart
import 'dart:ui' as ui;

// ···
const String rawSvg = '''<svg ...>...</svg>''';
final PictureInfo pictureInfo =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

// #docregion OutputConversion
import 'dart:ui' as ui;

// #enddocregion OutputConversion

import 'package:flutter/material.dart';
Expand Down Expand Up @@ -101,3 +102,42 @@ Future<ui.Image> 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 = '''
<svg viewBox="0 0 100 100">
<rect width="50" height="50" fill="#FF0000" />
<circle cx="75" cy="75" r="25" fill="#00FF00" />
</svg>
''';
final Widget svgIcon = SvgPicture.string(
svgString,
colorMapper: const _MyColorMapper(),
);
// #enddocregion ColorMapper
return svgIcon;
}
25 changes: 22 additions & 3 deletions third_party/packages/flutter_svg/lib/svg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand All @@ -205,6 +206,7 @@ class SvgPicture extends StatelessWidget {
packageName: package,
assetBundle: bundle,
theme: theme,
colorMapper: colorMapper,
),
colorFilter = colorFilter ?? _getColorFilter(color, colorBlendMode);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion third_party/packages/flutter_svg/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
132 changes: 132 additions & 0 deletions third_party/packages/flutter_svg/test/widget_svg_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ Future<void> _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!);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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(
Expand All @@ -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();
Expand Down