@@ -157,3 +157,184 @@ const Map<String, Color> namedColors = <String, Color>{
157157 'yellow' : Color .fromARGB (255 , 255 , 255 , 0 ),
158158 'yellowgreen' : Color .fromARGB (255 , 154 , 205 , 50 ),
159159};
160+
161+ /// Parses a CSS `rgb()` or `rgba()` function color string and returns a Color.
162+ ///
163+ /// The [colorString] should be the full color string including the function
164+ /// name (`rgb` or `rgba` ) and parentheses.
165+ ///
166+ /// Both `rgb()` and `rgba()` accept the same syntax variations:
167+ /// - `rgb(R G B)` or `rgba(R G B)` - modern space-separated
168+ /// - `rgb(R G B / A)` or `rgba(R G B / A)` - modern with slash before alpha
169+ /// - `rgb(R,G,B)` or `rgba(R,G,B)` - legacy comma-separated
170+ /// - `rgb(R,G,B,A)` or `rgba(R,G,B,A)` - legacy with alpha
171+ /// - `rgb(R G,B,A)` or `rgba(R G,B,A)` - mixed: spaces before first comma
172+ ///
173+ /// Throws [StateError] if the color string is invalid.
174+ Color parseRgbFunction (String colorString) {
175+ final String content = colorString.substring (
176+ colorString.indexOf ('(' ) + 1 ,
177+ colorString.indexOf (')' ),
178+ );
179+ final currentValue = StringBuffer ();
180+ final values = < String > [];
181+ var hasSlash = false ;
182+ var commaCount = 0 ;
183+ var pendingSpaceSeparator = false ;
184+ var justSawComma = false ;
185+
186+ void finalizeCurrentValue () {
187+ final String trimmed = currentValue.toString ().trim ();
188+ if (trimmed.isNotEmpty) {
189+ values.add (trimmed);
190+ }
191+ currentValue.clear ();
192+ pendingSpaceSeparator = false ;
193+ }
194+
195+ // Parse character by character
196+ for (var i = 0 ; i < content.length; i++ ) {
197+ final String char = content[i];
198+ final bool isWhitespace =
199+ char == ' ' || char == '\t ' || char == '\n ' || char == '\r ' ;
200+
201+ if (isWhitespace) {
202+ if (currentValue.isNotEmpty) {
203+ pendingSpaceSeparator = true ;
204+ }
205+ justSawComma = false ;
206+ continue ;
207+ }
208+
209+ if (char == '/' ) {
210+ if (commaCount > 0 ) {
211+ throw StateError (
212+ 'Invalid color "$colorString ": cannot mix comma and slash separators' ,
213+ );
214+ }
215+ if (hasSlash) {
216+ throw StateError (
217+ 'Invalid color "$colorString ": multiple slashes not allowed' ,
218+ );
219+ }
220+ finalizeCurrentValue ();
221+ if (values.length != 3 ) {
222+ throw StateError (
223+ 'Invalid color "$colorString ": expected 3 RGB values before slash, '
224+ 'got ${values .length }' ,
225+ );
226+ }
227+ hasSlash = true ;
228+ justSawComma = false ;
229+ continue ;
230+ }
231+
232+ if (char == ',' ) {
233+ if (hasSlash) {
234+ throw StateError (
235+ 'Invalid color "$colorString ": cannot mix comma and slash separators' ,
236+ );
237+ }
238+ final bool hasContent = currentValue.isNotEmpty || pendingSpaceSeparator;
239+ if (justSawComma || (commaCount == 0 && ! hasContent && values.isEmpty)) {
240+ throw StateError (
241+ 'Invalid color "$colorString ": empty value in comma-separated list' ,
242+ );
243+ }
244+ finalizeCurrentValue ();
245+ commaCount++ ;
246+ justSawComma = true ;
247+ continue ;
248+ }
249+
250+ // Regular character
251+ justSawComma = false ;
252+ if (pendingSpaceSeparator && currentValue.isNotEmpty) {
253+ if (commaCount > 0 ) {
254+ throw StateError (
255+ 'Invalid color "$colorString ": space-separated values not allowed '
256+ 'after comma' ,
257+ );
258+ }
259+ finalizeCurrentValue ();
260+ }
261+ currentValue.write (char);
262+ pendingSpaceSeparator = false ;
263+ }
264+
265+ // Finalize last value
266+ final bool hadContent = currentValue.isNotEmpty;
267+ finalizeCurrentValue ();
268+
269+ if (justSawComma) {
270+ throw StateError (
271+ 'Invalid color "$colorString ": empty value in comma-separated list' ,
272+ );
273+ }
274+ if (hasSlash && values.length == 3 && ! hadContent) {
275+ throw StateError (
276+ 'Invalid color "$colorString ": missing alpha value after slash' ,
277+ );
278+ }
279+
280+ // Validate value count
281+ if (values.length < 3 ) {
282+ throw StateError (
283+ 'Invalid color "$colorString ": expected at least 3 values, '
284+ 'got ${values .length }' ,
285+ );
286+ }
287+ if (values.length > 4 ) {
288+ throw StateError (
289+ 'Invalid color "$colorString ": expected at most 4 values, '
290+ 'got ${values .length }' ,
291+ );
292+ }
293+
294+ // Validate 4-value syntax rules
295+ if (values.length == 4 && ! hasSlash && commaCount < 2 ) {
296+ if (commaCount == 0 ) {
297+ throw StateError (
298+ 'Invalid color "$colorString ": modern syntax requires "/" '
299+ 'before alpha value' ,
300+ );
301+ } else {
302+ throw StateError (
303+ 'Invalid color "$colorString ": legacy syntax with alpha '
304+ 'requires at least 2 commas' ,
305+ );
306+ }
307+ }
308+
309+ // Convert a single value to an integer color component
310+ int parseComponent (int index, String rawValue) {
311+ final isAlpha = index == 3 ;
312+ if (rawValue.endsWith ('%' )) {
313+ final String numPart = rawValue.substring (0 , rawValue.length - 1 );
314+ final double ? percent = double .tryParse (numPart);
315+ if (percent == null ) {
316+ throw StateError (
317+ 'Invalid color "$colorString ": invalid percentage "$rawValue "' ,
318+ );
319+ }
320+ return (percent.clamp (0 , 100 ) * 2.55 ).round ();
321+ }
322+ final double ? value = double .tryParse (rawValue);
323+ if (value == null ) {
324+ throw StateError (
325+ 'Invalid color "$colorString ": invalid value "$rawValue "' ,
326+ );
327+ }
328+ if (isAlpha) {
329+ return (value.clamp (0 , 1 ) * 255 ).round ();
330+ }
331+ return value.clamp (0 , 255 ).round ();
332+ }
333+
334+ final int r = parseComponent (0 , values[0 ]);
335+ final int g = parseComponent (1 , values[1 ]);
336+ final int b = parseComponent (2 , values[2 ]);
337+ final int a = values.length == 4 ? parseComponent (3 , values[3 ]) : 255 ;
338+
339+ return Color .fromARGB (a, r, g, b);
340+ }
0 commit comments