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

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

## 1.1.19
Expand Down
144 changes: 144 additions & 0 deletions packages/vector_graphics_compiler/lib/src/svg/colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,147 @@ const Map<String, Color> namedColors = <String, Color>{
'yellow': Color.fromARGB(255, 255, 255, 0),
'yellowgreen': Color.fromARGB(255, 154, 205, 50),
};

/// Parses a CSS `rgb()` or `rgba()` function color string and returns a Color.
///
/// The [colorString] should be the full color string including the function
/// name (`rgb` or `rgba`) and parentheses.
///
/// Both `rgb()` and `rgba()` accept the same syntax variations:
/// - `rgb(R G B)` or `rgba(R G B)` - modern space-separated
/// - `rgb(R G B / A)` or `rgba(R G B / A)` - modern with slash before alpha
/// - `rgb(R,G,B)` or `rgba(R,G,B)` - legacy comma-separated
/// - `rgb(R,G,B,A)` or `rgba(R,G,B,A)` - legacy with alpha
/// - `rgb(R G,B,A)` or `rgba(R G,B,A)` - mixed: spaces before first comma
///
/// Throws [StateError] if the color string is invalid.
Color parseRgbFunction(String colorString) {
final String content = colorString.substring(
colorString.indexOf('(') + 1,
colorString.indexOf(')'),
);

if (content.isEmpty) {
throw StateError('Invalid color "$colorString": empty content');
}
final List<String> stringValues;

final List<String> commaSplit = content
.split(',')
.map((String value) => value.trim())
.toList();

if (commaSplit.length > 1) {
// We are dealing with comma-separated syntax

// First handle the weird case where "R G, B" and "R G, B, A" are valid
final List<String> firstValueSpaceSplit = commaSplit.first
.split(' ')
.map((String value) => value.trim())
.where((String value) => value.isNotEmpty)
.toList();
if (firstValueSpaceSplit.length > 2) {
throw StateError(
'Invalid color "$colorString": expected at most 2 space-separated values in first value',
);
}
stringValues = [...firstValueSpaceSplit, ...commaSplit.skip(1)];
} else {
final List<String> slashSplit = content
.split('/')
.map((String value) => value.trim())
.toList();
if (slashSplit.length > 2) {
throw StateError(
'Invalid color "$colorString": multiple slashes not allowed',
);
}
final List<String> rgbSpaceSplit = slashSplit.first
.split(' ')
.map((String value) => value.trim())
.where((String value) => value.isNotEmpty)
.toList();
if (rgbSpaceSplit.length != 3) {
throw StateError(
'Invalid color "$colorString": expected 3 space-separated RGB values',
);
}
stringValues = [...rgbSpaceSplit, ...slashSplit.skip(1)];
}

if (stringValues.length < 3 || stringValues.length > 4) {
throw StateError(
'Invalid color "$colorString": expected 3-4 values, got ${stringValues.length}',
);
}

final int r = _parseRgbFunctionComponent(
componentIndex: 0,
rawComponentValue: stringValues[0],
originalColorString: colorString,
);
final int g = _parseRgbFunctionComponent(
componentIndex: 1,
rawComponentValue: stringValues[1],
originalColorString: colorString,
);
final int b = _parseRgbFunctionComponent(
componentIndex: 2,
rawComponentValue: stringValues[2],
originalColorString: colorString,
);
final int a = stringValues.length == 4
? _parseRgbFunctionComponent(
componentIndex: 3,
rawComponentValue: stringValues[3],
originalColorString: colorString,
)
: 255;

return Color.fromARGB(a, r, g, b);
}

/// Parses a single RGB/RGBA component value and returns an integer 0-255.
///
/// The [componentIndex] indicates which component is being parsed:
/// - 0, 1, 2: RGB values (red, green, blue)
/// - 3: Alpha value
///
/// The [rawComponentValue] can be:
/// - A percentage (e.g., "50%") - converted to 0-255 range
/// - A decimal number (e.g., "128" or "128.5") - clamped to 0-255 for RGB
/// - For alpha (index 3): decimal 0-1 range, converted to 0-255
///
/// Out-of-bounds values are clamped rather than rejected.
///
/// Throws [StateError] if the value cannot be parsed as a number.
int _parseRgbFunctionComponent({
required int componentIndex,
required String rawComponentValue,
required String originalColorString,
}) {
final isAlpha = componentIndex == 3;
if (rawComponentValue.endsWith('%')) {
final String numPart = rawComponentValue.substring(
0,
rawComponentValue.length - 1,
);
final double? percent = double.tryParse(numPart);
if (percent == null) {
throw StateError(
'Invalid color "$originalColorString": invalid percentage "$rawComponentValue"',
);
}
return (percent.clamp(0, 100) * 2.55).round();
}
final double? value = double.tryParse(rawComponentValue);
if (value == null) {
throw StateError(
'Invalid color "$originalColorString": invalid value "$rawComponentValue"',
);
}
if (isAlpha) {
return (value.clamp(0, 1) * 255).round();
}
return value.clamp(0, 255).round();
}
39 changes: 4 additions & 35 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,10 @@ class SvgParser {
}
}

// handle rgba() colors e.g. rgba(255, 255, 255, 1.0)
if (colorString.toLowerCase().startsWith('rgba')) {
final List<String> rawColorElements = colorString
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
.split(',')
.map((String rawColor) => rawColor.trim())
.toList();

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

final List<int> rgb = rawColorElements
.map((String rawColor) => int.parse(rawColor))
.toList();

return Color.fromRGBO(rgb[0], rgb[1], rgb[2], opacity);
// 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('rgb')) {
return parseRgbFunction(colorString);
}

// Conversion code from: https://github.com/MichaelFenwick/Color, thanks :)
Expand Down Expand Up @@ -1456,26 +1445,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
Loading