Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/vector_graphics_compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 1.1.20

* Fix rgb and rgba color parsing to handle modern CSS syntax
* Updates minimum supported SDK version to Flutter 3.35/Dart 3.9.

## 1.1.19
Expand Down
59 changes: 29 additions & 30 deletions packages/vector_graphics_compiler/lib/src/svg/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1372,21 +1372,40 @@ class SvgParser {
}
}

// handle rgba() colors e.g. rgba(255, 255, 255, 1.0)
if (colorString.toLowerCase().startsWith('rgba')) {
final List<String> rawColorElements = colorString
// handle rgba() colors e.g. rgb(255, 255, 255) and rgba(255, 255, 255, 1.0)
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/rgb
if (colorString.toLowerCase().startsWith('rgba') ||
colorString.toLowerCase().startsWith('rgb')) {
final List<int> rgba = colorString
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
.split(',')
.split(RegExp(r'[,/\s]'))
Copy link
Author

Choose a reason for hiding this comment

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

valid delimiters are: ,, (empty space) and / (used to delimit the opacity)

.map((String rawColor) => rawColor.trim())
.where((e) => e.isNotEmpty)
.indexed
.map((indexedColor) {
var (index, rawColor) = indexedColor;
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
return (parseDouble(rawColor)! * 2.55).round();
}
if (index == 3) {
// if alpha is not percentage, it means it's a double between 0 and 1
final double opacity = parseDouble(rawColor)!;
if (opacity < 0 || opacity > 1) {
throw StateError('Invalid "opacity": $opacity');
}
return (opacity * 255).round();
}
// If rgb is not percentage, it means it's an integer between 0 and 255
return int.parse(rawColor);
})
Comment on lines +1385 to +1401

Choose a reason for hiding this comment

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

medium

The use of the custom parseDouble from numbers.dart is not ideal for parsing color components. This function is designed to strip units like px or em, but according to the CSS specification, color components in rgb() or rgba() functions should not have units. This could lead to incorrect parsing of invalid color strings (e.g., rgb(50px, 0, 0) would be parsed as rgb(50, 0, 0)).

To ensure stricter adherence to the CSS standard, you should use double.parse from dart:core instead, which will correctly throw an error for values with units.

Suggested change
.map((indexedColor) {
var (index, rawColor) = indexedColor;
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
return (parseDouble(rawColor)! * 2.55).round();
}
if (index == 3) {
// if alpha is not percentage, it means it's a double between 0 and 1
final double opacity = parseDouble(rawColor)!;
if (opacity < 0 || opacity > 1) {
throw StateError('Invalid "opacity": $opacity');
}
return (opacity * 255).round();
}
// If rgb is not percentage, it means it's an integer between 0 and 255
return int.parse(rawColor);
})
.map((indexedColor) {
var (index, rawColor) = indexedColor;
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
return (double.parse(rawColor) * 2.55).round();
}
if (index == 3) {
// if alpha is not percentage, it means it's a double between 0 and 1
final double opacity = double.parse(rawColor);
if (opacity < 0 || opacity > 1) {
throw StateError('Invalid "opacity": $opacity');
}
return (opacity * 255).round();
}
// If rgb is not percentage, it means it's an integer between 0 and 255
return int.parse(rawColor);
})

Copy link
Author

Choose a reason for hiding this comment

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

This parseDouble usage was retained from the existing rbg parsing implementation.

.toList();

final double opacity = parseDouble(rawColorElements.removeLast())!;

final List<int> rgb = rawColorElements
.map((String rawColor) => int.parse(rawColor))
.toList();
if (rgba.length == 3) {
rgba.add(255);
}

return Color.fromRGBO(rgb[0], rgb[1], rgb[2], opacity);
return Color.fromARGB(rgba[3], rgba[0], rgba[1], rgba[2]);
}

// Conversion code from: https://github.com/MichaelFenwick/Color, thanks :)
Expand Down Expand Up @@ -1456,26 +1475,6 @@ class SvgParser {
);
}

// handle rgb() colors e.g. rgb(255, 255, 255)
if (colorString.toLowerCase().startsWith('rgb')) {
final List<int> rgb = colorString
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
.split(',')
.map((String rawColor) {
rawColor = rawColor.trim();
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
return (parseDouble(rawColor)! * 2.55).round();
}
return int.parse(rawColor);
})
.toList();

// rgba() isn't really in the spec, but Firefox supported it at one point so why not.
final int a = rgb.length > 3 ? rgb[3] : 255;
return Color.fromARGB(a, rgb[0], rgb[1], rgb[2]);
}

// handle named colors ('red', 'green', etc.).
final Color? namedColor = namedColors[colorString];
if (namedColor != null) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vector_graphics_compiler/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: vector_graphics_compiler
description: A compiler to convert SVGs to the binary format used by `package:vector_graphics`.
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_compiler
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
version: 1.1.19
version: 1.1.20

executables:
vector_graphics_compiler:
Expand Down
70 changes: 65 additions & 5 deletions packages/vector_graphics_compiler/test/parsers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,71 @@ void main() {
parser.parseColor('#ABCDEF', attributeName: 'foo', id: null),
const Color.fromARGB(255, 0xAB, 0xCD, 0xEF),
);
// RGBA in svg/css, ARGB in this library.
expect(
parser.parseColor('#ABCDEF88', attributeName: 'foo', id: null),
const Color.fromARGB(0x88, 0xAB, 0xCD, 0xEF),
);
});

group('Colors - svg/css', () {
final parser = SvgParser('', const SvgTheme(), 'test_key', true, null);

group('with no opacity', () {
const rgbContentVariations = [
'171, 205, 239',
'171,205,239',
'171 205 239',
'171 205 239',
'171 205 239',
'171 205 239',
'171 205 239',
'67% 80.5% 93.7%',
];

final List<String> rgbaVariations = [
'#ABCDEF',
...rgbContentVariations.map((rgb) => 'rgba($rgb)'),
...rgbContentVariations.map((rgb) => 'rgb($rgb)'),
];

for (final rgba in rgbaVariations) {
test(rgba, () {
expect(
parser.parseColor(rgba, attributeName: 'foo', id: null),
const Color.fromARGB(0xFF, 0xAB, 0xCD, 0xEF),
);
});
}
});
group('with opacity', () {
const rgbContentVariations = [
'171, 205, 239, 0.53',
'171,205,239,0.53',
'171 205 239 0.53',
'171 205 239 0.53',
'171 205 239 / 53%',
'171 205 239 / 0.53',
'171 205 239 / 53%',
'67% 80.5% 93.7% / 53%',
];
final List<String> rgbaVariations = [
'#ABCDEF87',
...rgbContentVariations.map((rgb) => 'rgba($rgb)'),
...rgbContentVariations.map((rgb) => 'rgb($rgb)'),
];
for (final rgba in rgbaVariations) {
test(rgba, () {
expect(
parser.parseColor(rgba, attributeName: 'foo', id: null),
const Color.fromARGB(0x87, 0xAB, 0xCD, 0xEF),
);
});
}
});

test('rgba with no opacity', () {
// RGBA is now an alias for RGB, so opacity can be missing entirely
expect(
parser.parseColor('rgba(171 205 239)', attributeName: 'foo', id: null),
const Color.fromARGB(0xFF, 0xAB, 0xCD, 0xEF),
);
});
});

test('Colors - mapped', () async {
Expand Down