Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
143 changes: 143 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,146 @@ 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())
.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