Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions packages/vector_graphics_compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.2.2

* Adds support for modern space-separated HSL and HSLA color syntax.

## 1.2.1

* Fixes HSL/HSLA color parsing for decimal percentage components (e.g. `hsl(270, 100%, 76.27%)`).
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 @@ -193,9 +193,40 @@ final RegExp _cssRgbColorMatcher = RegExp(
caseSensitive: false,
);

/// Legacy (comma-separated) HSL syntax pattern with named capture groups.
/// Matches: hsl(H, S, L) or hsla(H, S, L, A)
final String _legacyHslSyntax =
'' // string alignment
r'(?<commaHue>%DIGIT%)\s*,\s*'
r'(?<commaSaturation>%DIGIT%)\s*,\s*'
r'(?<commaLightness>%DIGIT%)'
r'(?:\s*,\s*(?<commaHslAlpha>%DIGIT%))?'
.replaceAll('%DIGIT%', _cssDigit);

/// Modern (space-separated) HSL syntax pattern with named capture groups.
/// Matches: hsl(H S L) or hsl(H S L / A)
final String _modernHslSyntax =
'' // string alignment
r'(?<spaceHue>%DIGIT%)\s+'
r'(?<spaceSaturation>%DIGIT%)\s+'
r'(?<spaceLightness>%DIGIT%)'
r'(?:\s*\/\s*(?<spaceHslAlpha>%DIGIT%))?'
.replaceAll('%DIGIT%', _cssDigit);

/// Combined regex for matching CSS hsl/hsla color functions.
/// Supports both legacy (comma) and modern (space) syntax.
/// https://www.w3.org/TR/css-color-4/#the-hsl-notation
final RegExp _cssHslColorMatcher = RegExp(
'hsla?\\(\\s*(?:$_legacyHslSyntax|$_modernHslSyntax)\\s*\\)',
caseSensitive: false,
);

/// Record type representing parsed CSS RGB values as strings.
typedef CssRgbRecord = ({String r, String g, String b, String a});

/// Record type representing parsed CSS HSL values as strings.
typedef CssHslRecord = ({String h, String s, String l, String a});

/// Parses a CSS `rgb()` or `rgba()` function string into a record of string values.
///
/// Returns a record with r, g, b, and a string values if the input matches
Expand All @@ -222,6 +253,35 @@ CssRgbRecord? parseCssRgb(String input) {
return (r: r!, g: g!, b: b!, a: a);
}

/// Parses a CSS `hsl()` or `hsla()` function string into a record of string values.
///
/// Returns a record with h, s, l, and a string values if the input matches
/// valid CSS hsl/hsla syntax, or null if the syntax is invalid.
///
/// Both legacy (comma-separated) and modern (space-separated) syntax are supported:
/// - Legacy: `hsl(270, 100%, 76%)` or `hsla(270, 100%, 76%, 0.5)`
/// - Modern: `hsl(270 100% 76%)` or `hsl(270 100% 76% / 0.5)`
@visibleForTesting
CssHslRecord? parseCssHsl(String input) {
final RegExpMatch? match = _cssHslColorMatcher.firstMatch(input);
if (match == null) {
return null;
}
final String? h =
match.namedGroup('commaHue') ?? match.namedGroup('spaceHue');
final String? s =
match.namedGroup('commaSaturation') ??
match.namedGroup('spaceSaturation');
final String? l =
match.namedGroup('commaLightness') ?? match.namedGroup('spaceLightness');
final String a =
match.namedGroup('commaHslAlpha') ??
match.namedGroup('spaceHslAlpha') ??
'1';

return (h: h!, s: s!, l: l!, a: a);
}

/// Parses a CSS `rgb()` or `rgba()` function color string and returns a Color.
///
/// The [colorString] should be the full color string including the function
Expand All @@ -246,6 +306,30 @@ Color parseRgbFunction(String colorString) {
return _cssRgbRecordToColor(parsed);
}

/// Parses a CSS `hsl()` or `hsla()` function color string and returns a Color.
///
/// The [colorString] should be the full color string including the function
/// name (`hsl` or `hsla`) and parentheses.
///
/// Both `hsl()` and `hsla()` accept the same syntax variations:
/// - `hsl(H S L)` or `hsla(H S L)` - modern space-separated
/// - `hsl(H S L / A)` or `hsla(H S L / A)` - modern with slash before alpha
/// - `hsl(H,S,L)` or `hsla(H,S,L)` - legacy comma-separated
/// - `hsl(H,S,L,A)` or `hsla(H,S,L,A)` - legacy with alpha
///
/// Throws [ArgumentError] if the color string is invalid.
Color parseHslFunction(String colorString) {
final CssHslRecord? parsed = parseCssHsl(colorString);
if (parsed == null) {
throw ArgumentError.value(
colorString,
'colorString',
'Invalid CSS hsl/hsla color syntax',
);
}
return _cssHslRecordToColor(parsed);
}

/// Converts a [CssRgbRecord] to a [Color].
///
/// Each component string can be:
Expand All @@ -263,6 +347,49 @@ Color _cssRgbRecordToColor(CssRgbRecord record) {
return Color.fromARGB(a, r, g, b);
}

/// Converts a [CssHslRecord] to a [Color].
Color _cssHslRecordToColor(CssHslRecord record) {
final double hue = _parseHslValue(record.h) / 360 % 1;
final double saturation = _parseHslValue(record.s) / 100;
final double luminance = _parseHslValue(record.l) / 100;
final int alpha = _parseHslAlpha(record.a);
var rgb = <double>[0, 0, 0];

if (hue < 1 / 6) {
rgb[0] = 1;
rgb[1] = hue * 6;
} else if (hue < 2 / 6) {
rgb[0] = 2 - hue * 6;
rgb[1] = 1;
} else if (hue < 3 / 6) {
rgb[1] = 1;
rgb[2] = hue * 6 - 2;
} else if (hue < 4 / 6) {
rgb[1] = 4 - hue * 6;
rgb[2] = 1;
} else if (hue < 5 / 6) {
rgb[0] = hue * 6 - 4;
rgb[2] = 1;
} else {
rgb[0] = 1;
rgb[2] = 6 - hue * 6;
}

rgb = rgb.map((double val) => val + (1 - saturation) * (0.5 - val)).toList();

if (luminance < 0.5) {
rgb = rgb.map((double val) => luminance * 2 * val).toList();
} else {
rgb = rgb
.map((double val) => luminance * 2 * (1 - val) + 2 * val - 1)
.toList();
}

rgb = rgb.map((double val) => val * 255).toList();

return Color.fromARGB(alpha, rgb[0].round(), rgb[1].round(), rgb[2].round());
}

/// Parses a single color component value and returns an integer 0-255.
int _parseColorComponent(String value, {required bool isAlpha}) {
if (value.endsWith('%')) {
Expand All @@ -276,3 +403,20 @@ int _parseColorComponent(String value, {required bool isAlpha}) {
}
return numValue.clamp(0, 255).round();
}

/// Parses a single HSL component value and returns its numeric value.
double _parseHslValue(String value) {
if (value.endsWith('%')) {
value = value.substring(0, value.length - 1);
}
return double.parse(value);
}

/// Parses a single HSL alpha value and returns an integer 0-255.
int _parseHslAlpha(String value) {
if (value.endsWith('%')) {
// Avoid * 2.55 because floating-point rounding makes 50% produce 127.
return (_parseHslValue(value).clamp(0, 100) / 100 * 255).round();
}
return (double.parse(value).clamp(0, 1) * 255).round();
}
69 changes: 3 additions & 66 deletions packages/vector_graphics_compiler/lib/src/svg/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1432,73 +1432,10 @@ class SvgParser {
return parseRgbFunction(colorString);
}

// Conversion code from: https://github.com/MichaelFenwick/Color, thanks :)
// handle hsla() colors e.g. hsl(270, 100%, 76%) and hsla(270, 100%, 76%, 1.0)
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/hsl
if (colorString.toLowerCase().startsWith('hsl')) {
final List<String> values = colorString
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
.split(',')
.map((String rawColor) => rawColor.trim())
.toList();
double parseHslValue(String rawColor) {
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
}
return parseDouble(rawColor)!;
}

int parseHslAlpha(String rawAlpha) {
if (rawAlpha.endsWith('%')) {
return (parseHslValue(rawAlpha).clamp(0, 100) * 2.55).round();
}
return (parseDouble(rawAlpha)!.clamp(0, 1) * 255).round();
}

final double hue = parseHslValue(values[0]) / 360 % 1;
final double saturation = parseHslValue(values[1]) / 100;
final double luminance = parseHslValue(values[2]) / 100;
final int alpha = values.length > 3 ? parseHslAlpha(values[3]) : 255;
var rgb = <double>[0, 0, 0];

if (hue < 1 / 6) {
rgb[0] = 1;
rgb[1] = hue * 6;
} else if (hue < 2 / 6) {
rgb[0] = 2 - hue * 6;
rgb[1] = 1;
} else if (hue < 3 / 6) {
rgb[1] = 1;
rgb[2] = hue * 6 - 2;
} else if (hue < 4 / 6) {
rgb[1] = 4 - hue * 6;
rgb[2] = 1;
} else if (hue < 5 / 6) {
rgb[0] = hue * 6 - 4;
rgb[2] = 1;
} else {
rgb[0] = 1;
rgb[2] = 6 - hue * 6;
}

rgb = rgb
.map((double val) => val + (1 - saturation) * (0.5 - val))
.toList();

if (luminance < 0.5) {
rgb = rgb.map((double val) => luminance * 2 * val).toList();
} else {
rgb = rgb
.map((double val) => luminance * 2 * (1 - val) + 2 * val - 1)
.toList();
}

rgb = rgb.map((double val) => val * 255).toList();

return Color.fromARGB(
alpha,
rgb[0].round(),
rgb[1].round(),
rgb[2].round(),
);
return parseHslFunction(colorString);
}

// handle named colors ('red', 'green', etc.).
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.2.1
version: 1.2.2

executables:
vector_graphics_compiler:
Expand Down
Loading