Skip to content

Commit 5551028

Browse files
committed
[vector_graphics_compiler] Fix rgb/rgba color parsing to support modern CSS syntax
- Consolidate rgb() and rgba() parsing into single implementation - Add support for space-separated color values - Add support for percentage-based RGB values - Add support for slash (/) separator before alpha channel - Handle both comma and space delimiters - Add comprehensive test coverage for various color formats
1 parent 0e20a04 commit 5551028

File tree

2 files changed

+94
-35
lines changed

2 files changed

+94
-35
lines changed

packages/vector_graphics_compiler/lib/src/svg/parser.dart

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,21 +1372,40 @@ class SvgParser {
13721372
}
13731373
}
13741374

1375-
// handle rgba() colors e.g. rgba(255, 255, 255, 1.0)
1376-
if (colorString.toLowerCase().startsWith('rgba')) {
1377-
final List<String> rawColorElements = colorString
1375+
// handle rgba() colors e.g. rgb(255, 255, 255) and rgba(255, 255, 255, 1.0)
1376+
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/rgb
1377+
if (colorString.toLowerCase().startsWith('rgba') ||
1378+
colorString.toLowerCase().startsWith('rgb')) {
1379+
final List<int> rgba = colorString
13781380
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
1379-
.split(',')
1381+
.split(RegExp(r'[,/\s]'))
13801382
.map((String rawColor) => rawColor.trim())
1383+
.where((e) => e.isNotEmpty)
1384+
.indexed
1385+
.map((indexedColor) {
1386+
var (index, rawColor) = indexedColor;
1387+
if (rawColor.endsWith('%')) {
1388+
rawColor = rawColor.substring(0, rawColor.length - 1);
1389+
return (parseDouble(rawColor)! * 2.55).round();
1390+
}
1391+
if (index == 3) {
1392+
// if alpha is not percentage, it means it's a double between 0 and 1
1393+
final double opacity = parseDouble(rawColor)!;
1394+
if (opacity < 0 || opacity > 1) {
1395+
throw StateError('Invalid "opacity": $opacity');
1396+
}
1397+
return (opacity * 255).round();
1398+
}
1399+
// If rgb is not percentage, it means it's an integer between 0 and 255
1400+
return int.parse(rawColor);
1401+
})
13811402
.toList();
13821403

1383-
final double opacity = parseDouble(rawColorElements.removeLast())!;
1384-
1385-
final List<int> rgb = rawColorElements
1386-
.map((String rawColor) => int.parse(rawColor))
1387-
.toList();
1404+
if (rgba.length == 3) {
1405+
rgba.add(255);
1406+
}
13881407

1389-
return Color.fromRGBO(rgb[0], rgb[1], rgb[2], opacity);
1408+
return Color.fromARGB(rgba[3], rgba[0], rgba[1], rgba[2]);
13901409
}
13911410

13921411
// Conversion code from: https://github.com/MichaelFenwick/Color, thanks :)
@@ -1456,26 +1475,6 @@ class SvgParser {
14561475
);
14571476
}
14581477

1459-
// handle rgb() colors e.g. rgb(255, 255, 255)
1460-
if (colorString.toLowerCase().startsWith('rgb')) {
1461-
final List<int> rgb = colorString
1462-
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
1463-
.split(',')
1464-
.map((String rawColor) {
1465-
rawColor = rawColor.trim();
1466-
if (rawColor.endsWith('%')) {
1467-
rawColor = rawColor.substring(0, rawColor.length - 1);
1468-
return (parseDouble(rawColor)! * 2.55).round();
1469-
}
1470-
return int.parse(rawColor);
1471-
})
1472-
.toList();
1473-
1474-
// rgba() isn't really in the spec, but Firefox supported it at one point so why not.
1475-
final int a = rgb.length > 3 ? rgb[3] : 255;
1476-
return Color.fromARGB(a, rgb[0], rgb[1], rgb[2]);
1477-
}
1478-
14791478
// handle named colors ('red', 'green', etc.).
14801479
final Color? namedColor = namedColors[colorString];
14811480
if (namedColor != null) {

packages/vector_graphics_compiler/test/parsers_test.dart

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,71 @@ void main() {
2121
parser.parseColor('#ABCDEF', attributeName: 'foo', id: null),
2222
const Color.fromARGB(255, 0xAB, 0xCD, 0xEF),
2323
);
24-
// RGBA in svg/css, ARGB in this library.
25-
expect(
26-
parser.parseColor('#ABCDEF88', attributeName: 'foo', id: null),
27-
const Color.fromARGB(0x88, 0xAB, 0xCD, 0xEF),
28-
);
24+
});
25+
26+
group('Colors - svg/css', () {
27+
final parser = SvgParser('', const SvgTheme(), 'test_key', true, null);
28+
29+
group('with no opacity', () {
30+
const rgbContentVariations = [
31+
'171, 205, 239',
32+
'171,205,239',
33+
'171 205 239',
34+
'171 205 239',
35+
'171 205 239',
36+
'171 205 239',
37+
'171 205 239',
38+
'67% 80.5% 93.7%',
39+
];
40+
41+
final List<String> rgbaVariations = [
42+
'#ABCDEF',
43+
...rgbContentVariations.map((rgb) => 'rgba($rgb)'),
44+
...rgbContentVariations.map((rgb) => 'rgb($rgb)'),
45+
];
46+
47+
for (final rgba in rgbaVariations) {
48+
test(rgba, () {
49+
expect(
50+
parser.parseColor(rgba, attributeName: 'foo', id: null),
51+
const Color.fromARGB(0xFF, 0xAB, 0xCD, 0xEF),
52+
);
53+
});
54+
}
55+
});
56+
group('with opacity', () {
57+
const rgbContentVariations = [
58+
'171, 205, 239, 0.53',
59+
'171,205,239,0.53',
60+
'171 205 239 0.53',
61+
'171 205 239 0.53',
62+
'171 205 239 / 53%',
63+
'171 205 239 / 0.53',
64+
'171 205 239 / 53%',
65+
'67% 80.5% 93.7% / 53%',
66+
];
67+
final List<String> rgbaVariations = [
68+
'#ABCDEF87',
69+
...rgbContentVariations.map((rgb) => 'rgba($rgb)'),
70+
...rgbContentVariations.map((rgb) => 'rgb($rgb)'),
71+
];
72+
for (final rgba in rgbaVariations) {
73+
test(rgba, () {
74+
expect(
75+
parser.parseColor(rgba, attributeName: 'foo', id: null),
76+
const Color.fromARGB(0x87, 0xAB, 0xCD, 0xEF),
77+
);
78+
});
79+
}
80+
});
81+
82+
test('rgba with no opacity', () {
83+
// RGBA is now an alias for RGB, so opacity can be missing entirely
84+
expect(
85+
parser.parseColor('rgba(171 205 239)', attributeName: 'foo', id: null),
86+
const Color.fromARGB(0xFF, 0xAB, 0xCD, 0xEF),
87+
);
88+
});
2989
});
3090

3191
test('Colors - mapped', () async {

0 commit comments

Comments
 (0)