blob: e9a4a5e604727d3fb64c0b23254760b77ff9bd05 [file] [log] [blame]
import 'dart:collection';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:path_drawing/path_drawing.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:xml/xml_events.dart' hide parseEvents;
import '../svg/theme.dart';
import '../utilities/errors.dart';
import '../utilities/numbers.dart';
import '../utilities/xml.dart';
import '../vector_drawable.dart';
import 'parsers.dart';
final Set<String> _unhandledElements = <String>{'title', 'desc'};
typedef _ParseFunc = Future<void>? Function(
SvgParserState parserState, bool warningsAsErrors);
typedef _PathFunc = Path? Function(SvgParserState parserState);
final RegExp _trimPattern = RegExp(r'[\r|\n|\t]');
const Map<String, _ParseFunc> _svgElementParsers = <String, _ParseFunc>{
'svg': _Elements.svg,
'g': _Elements.g,
'a': _Elements.g, // treat as group
'use': _Elements.use,
'symbol': _Elements.symbol,
'mask': _Elements.symbol, // treat as symbol
'radialGradient': _Elements.radialGradient,
'linearGradient': _Elements.linearGradient,
'clipPath': _Elements.clipPath,
'image': _Elements.image,
'text': _Elements.text,
};
const Map<String, _PathFunc> _svgPathFuncs = <String, _PathFunc>{
'circle': _Paths.circle,
'path': _Paths.path,
'rect': _Paths.rect,
'polygon': _Paths.polygon,
'polyline': _Paths.polyline,
'ellipse': _Paths.ellipse,
'line': _Paths.line,
};
Offset _parseCurrentOffset(SvgParserState parserState, Offset? lastOffset) {
final String? x = parserState.attribute('x', def: null);
final String? y = parserState.attribute('y', def: null);
return Offset(
x != null
? parserState.parseDoubleWithUnits(x)!
: parserState.parseDoubleWithUnits(
parserState.attribute('dx', def: '0'),
)! +
(lastOffset?.dx ?? 0),
y != null
? parserState.parseDoubleWithUnits(y)!
: parserState.parseDoubleWithUnits(
parserState.attribute('dy', def: '0'),
)! +
(lastOffset?.dy ?? 0),
);
}
class _TextInfo {
const _TextInfo(
this.style,
this.offset,
this.transform,
);
final DrawableStyle style;
final Offset offset;
final Matrix4? transform;
@override
String toString() => '$runtimeType{$offset, $style, $transform}';
}
// ignore: avoid_classes_with_only_static_members
class _Elements {
static Future<void>? svg(SvgParserState parserState, bool warningsAsErrors) {
final DrawableViewport? viewBox = parserState.parseViewBox();
final String? id = parserState.attribute('id', def: '');
final Color? color = parserState.parseColor(
parserState.attribute('color', def: null),
currentColor: parserState.theme.currentColor,
);
// TODO(dnfield): Support nested SVG elements. https://github.com/dnfield/flutter_svg/issues/132
if (parserState._root != null) {
const String errorMessage = 'Unsupported nested <svg> element.';
if (warningsAsErrors) {
throw UnsupportedError(errorMessage);
}
FlutterError.reportError(FlutterErrorDetails(
exception: UnsupportedError(errorMessage),
informationCollector: () => <DiagnosticsNode>[
ErrorDescription(
'The root <svg> element contained an unsupported nested SVG element.'),
if (parserState._key != null) ErrorDescription(''),
if (parserState._key != null)
DiagnosticsProperty<String>('Picture key', parserState._key),
],
library: 'SVG',
context: ErrorDescription('in _Element.svg'),
));
parserState._parentDrawables.addLast(
_SvgGroupTuple(
'svg',
DrawableGroup(
id,
<Drawable>[],
parserState.parseStyle(viewBox!.viewBoxRect, null,
currentColor: color),
color: color,
),
),
);
return null;
}
parserState._root = DrawableRoot(
id,
viewBox!,
<Drawable>[],
parserState._definitions,
parserState.parseStyle(viewBox.viewBoxRect, null, currentColor: color),
color: color,
compatibilityTester: parserState._compatibilityTester,
);
parserState.addGroup(parserState._currentStartElement!, parserState._root);
return null;
}
static Future<void>? g(SvgParserState parserState, bool warningsAsErrors) {
if (parserState._currentStartElement?.isSelfClosing == true) {
return null;
}
final DrawableParent parent = parserState.currentGroup!;
final Color? color = parserState.parseColor(
parserState.attribute('color', def: null),
currentColor: parent.color ?? parserState.theme.currentColor) ??
parent.color;
final DrawableGroup group = DrawableGroup(
parserState.attribute('id', def: ''),
<Drawable>[],
parserState.parseStyle(parserState.rootBounds, parent.style,
currentColor: color),
transform: parseTransform(parserState.attribute('transform'))?.storage,
color: color,
);
parent.children!.add(group);
parserState.addGroup(parserState._currentStartElement!, group);
return null;
}
static Future<void>? symbol(
SvgParserState parserState, bool warningsAsErrors) {
final DrawableParent parent = parserState.currentGroup!;
final Color? color = parserState.parseColor(
parserState.attribute('color', def: null),
currentColor: parent.color ?? parserState.theme.currentColor) ??
parent.color;
final DrawableGroup group = DrawableGroup(
parserState.attribute('id', def: ''),
<Drawable>[],
parserState.parseStyle(
parserState.rootBounds,
parent.style,
currentColor: color,
),
transform: parseTransform(parserState.attribute('transform'))?.storage,
color: color,
);
parserState.addGroup(parserState._currentStartElement!, group);
return null;
}
static Future<void>? use(SvgParserState parserState, bool warningsAsErrors) {
final DrawableParent? parent = parserState.currentGroup;
final String xlinkHref = getHrefAttribute(parserState.attributes)!;
if (xlinkHref.isEmpty) {
return null;
}
final DrawableStyle style = parserState.parseStyle(
parserState.rootBounds,
parent!.style,
currentColor: parent.color,
);
final Matrix4 transform =
parseTransform(parserState.attribute('transform')) ??
Matrix4.identity();
transform.translate(
parserState.parseDoubleWithUnits(
parserState.attribute('x', def: '0'),
),
parserState.parseDoubleWithUnits(
parserState.attribute('y', def: '0'),
)!,
);
final DrawableStyleable ref =
parserState._definitions.getDrawable('url($xlinkHref)')!;
final DrawableGroup group = DrawableGroup(
parserState.attribute('id', def: ''),
<Drawable>[ref.mergeStyle(style)],
style,
transform: transform.storage,
);
parserState.checkForIri(group);
parent.children!.add(group);
return null;
}
static Future<void>? parseStops(
SvgParserState parserState,
List<Color> colors,
List<double> offsets,
) {
final DrawableParent parent = parserState.currentGroup!;
for (XmlEvent event in parserState._readSubtree()) {
if (event is XmlEndElementEvent) {
continue;
}
if (event is XmlStartElementEvent) {
final String rawOpacity = getAttribute(
parserState.attributes,
'stop-opacity',
def: '1',
)!;
final Color stopColor = parserState.parseColor(
getAttribute(parserState.attributes, 'stop-color'),
currentColor: parent.color ?? parserState.theme.currentColor) ??
parent.color ??
colorBlack;
colors.add(stopColor.withOpacity(parseDouble(rawOpacity)!));
final String rawOffset = getAttribute(
parserState.attributes,
'offset',
def: '0%',
)!;
offsets.add(parseDecimalOrPercentage(rawOffset));
}
}
return null;
}
static Future<void>? radialGradient(
SvgParserState parserState,
bool warningsAsErrors,
) {
final String? gradientUnits = getAttribute(
parserState.attributes,
'gradientUnits',
def: null,
);
bool isObjectBoundingBox = gradientUnits != 'userSpaceOnUse';
final String? rawCx = parserState.attribute('cx', def: '50%');
final String? rawCy = parserState.attribute('cy', def: '50%');
final String? rawR = parserState.attribute('r', def: '50%');
final String? rawFx = parserState.attribute('fx', def: rawCx);
final String? rawFy = parserState.attribute('fy', def: rawCy);
final TileMode spreadMethod = parserState.parseTileMode();
final String id = parserState.buildUrlIri();
final Matrix4? originalTransform = parseTransform(
parserState.attribute('gradientTransform', def: null),
);
final List<double> offsets = <double>[];
final List<Color> colors = <Color>[];
if (parserState._currentStartElement!.isSelfClosing) {
final String? href = getHrefAttribute(parserState.attributes);
final DrawableGradient? ref =
parserState._definitions.getGradient<DrawableGradient>('url($href)');
if (ref == null) {
reportMissingDef(parserState._key, href, 'radialGradient');
} else {
if (gradientUnits == null) {
isObjectBoundingBox =
ref.unitMode == GradientUnitMode.objectBoundingBox;
}
colors.addAll(ref.colors!);
offsets.addAll(ref.offsets!);
}
} else {
parseStops(parserState, colors, offsets);
}
late double cx, cy, r, fx, fy;
if (isObjectBoundingBox) {
cx = parseDecimalOrPercentage(rawCx!);
cy = parseDecimalOrPercentage(rawCy!);
r = parseDecimalOrPercentage(rawR!);
fx = parseDecimalOrPercentage(rawFx!);
fy = parseDecimalOrPercentage(rawFy!);
} else {
cx = isPercentage(rawCx!)
? parsePercentage(rawCx) * parserState.rootBounds.width +
parserState.rootBounds.left
: parserState.parseDoubleWithUnits(rawCx)!;
cy = isPercentage(rawCy!)
? parsePercentage(rawCy) * parserState.rootBounds.height +
parserState.rootBounds.top
: parserState.parseDoubleWithUnits(rawCy)!;
r = isPercentage(rawR!)
? parsePercentage(rawR) *
((parserState.rootBounds.height + parserState.rootBounds.width) /
2)
: parserState.parseDoubleWithUnits(rawR)!;
fx = isPercentage(rawFx!)
? parsePercentage(rawFx) * parserState.rootBounds.width +
parserState.rootBounds.left
: parserState.parseDoubleWithUnits(rawFx)!;
fy = isPercentage(rawFy!)
? parsePercentage(rawFy) * parserState.rootBounds.height +
parserState.rootBounds.top
: parserState.parseDoubleWithUnits(rawFy)!;
}
parserState._definitions.addGradient(
id,
DrawableRadialGradient(
center: Offset(cx, cy),
radius: r,
focal: (fx != cx || fy != cy) ? Offset(fx, fy) : Offset(cx, cy),
focalRadius: 0.0,
colors: colors,
offsets: offsets,
unitMode: isObjectBoundingBox
? GradientUnitMode.objectBoundingBox
: GradientUnitMode.userSpaceOnUse,
spreadMethod: spreadMethod,
transform: originalTransform?.storage,
),
);
return null;
}
static Future<void>? linearGradient(
SvgParserState parserState,
bool warningsAsErrors,
) {
final String? gradientUnits = parserState.attribute('gradientUnits');
bool isObjectBoundingBox = gradientUnits != 'userSpaceOnUse';
final String x1 = parserState.attribute('x1', def: '0%')!;
final String x2 = parserState.attribute('x2', def: '100%')!;
final String y1 = parserState.attribute('y1', def: '0%')!;
final String y2 = parserState.attribute('y2', def: '0%')!;
final String id = parserState.buildUrlIri();
final Matrix4? originalTransform = parseTransform(
parserState.attribute('gradientTransform'),
);
final TileMode spreadMethod = parserState.parseTileMode();
final List<Color> colors = <Color>[];
final List<double> offsets = <double>[];
if (parserState._currentStartElement!.isSelfClosing) {
final String? href = getHrefAttribute(parserState.attributes);
final DrawableGradient? ref =
parserState._definitions.getGradient<DrawableGradient>('url($href)');
if (ref == null) {
reportMissingDef(parserState._key, href, 'linearGradient');
} else {
if (gradientUnits == null) {
isObjectBoundingBox =
ref.unitMode == GradientUnitMode.objectBoundingBox;
}
colors.addAll(ref.colors!);
offsets.addAll(ref.offsets!);
}
} else {
parseStops(parserState, colors, offsets);
}
Offset fromOffset, toOffset;
if (isObjectBoundingBox) {
fromOffset = Offset(
parseDecimalOrPercentage(x1),
parseDecimalOrPercentage(y1),
);
toOffset = Offset(
parseDecimalOrPercentage(x2),
parseDecimalOrPercentage(y2),
);
} else {
fromOffset = Offset(
isPercentage(x1)
? parsePercentage(x1) * parserState.rootBounds.width +
parserState.rootBounds.left
: parserState.parseDoubleWithUnits(x1)!,
isPercentage(y1)
? parsePercentage(y1) * parserState.rootBounds.height +
parserState.rootBounds.top
: parserState.parseDoubleWithUnits(y1)!,
);
toOffset = Offset(
isPercentage(x2)
? parsePercentage(x2) * parserState.rootBounds.width +
parserState.rootBounds.left
: parserState.parseDoubleWithUnits(x2)!,
isPercentage(y2)
? parsePercentage(y2) * parserState.rootBounds.height +
parserState.rootBounds.top
: parserState.parseDoubleWithUnits(y2)!,
);
}
parserState._definitions.addGradient(
id,
DrawableLinearGradient(
from: fromOffset,
to: toOffset,
colors: colors,
offsets: offsets,
spreadMethod: spreadMethod,
unitMode: isObjectBoundingBox
? GradientUnitMode.objectBoundingBox
: GradientUnitMode.userSpaceOnUse,
transform: originalTransform?.storage,
),
);
return null;
}
static Future<void>? clipPath(
SvgParserState parserState, bool warningsAsErrors) {
final String id = parserState.buildUrlIri();
final List<Path> paths = <Path>[];
Path? currentPath;
for (XmlEvent event in parserState._readSubtree()) {
if (event is XmlEndElementEvent) {
continue;
}
if (event is XmlStartElementEvent) {
final _PathFunc? pathFn = _svgPathFuncs[event.name];
if (pathFn != null) {
final Path nextPath = parserState.applyTransformIfNeeded(
pathFn(parserState),
)!;
nextPath.fillType = parserState.parseFillRule('clip-rule')!;
if (currentPath != null &&
nextPath.fillType != currentPath.fillType) {
currentPath = nextPath;
paths.add(currentPath);
} else if (currentPath == null) {
currentPath = nextPath;
paths.add(currentPath);
} else {
currentPath.addPath(nextPath, Offset.zero);
}
} else if (event.name == 'use') {
final String? xlinkHref = getHrefAttribute(parserState.attributes);
final DrawableStyleable? definitionDrawable =
parserState._definitions.getDrawable('url($xlinkHref)');
void extractPathsFromDrawable(Drawable? target) {
if (target is DrawableShape) {
paths.add(target.path);
} else if (target is DrawableGroup) {
target.children!.forEach(extractPathsFromDrawable);
}
}
extractPathsFromDrawable(definitionDrawable);
} else {
final String errorMessage =
'Unsupported clipPath child ${event.name}';
if (warningsAsErrors) {
throw UnsupportedError(errorMessage);
}
FlutterError.reportError(FlutterErrorDetails(
exception: UnsupportedError(errorMessage),
informationCollector: () => <DiagnosticsNode>[
ErrorDescription(
'The <clipPath> element contained an unsupported child ${event.name}'),
if (parserState._key != null) ErrorDescription(''),
if (parserState._key != null)
DiagnosticsProperty<String>('Picture key', parserState._key),
],
library: 'SVG',
context: ErrorDescription('in _Element.clipPath'),
));
}
}
}
parserState._definitions.addClipPath(id, paths);
return null;
}
static Future<void> image(
SvgParserState parserState, bool warningsAsErrors) async {
final String? href = getHrefAttribute(parserState.attributes);
if (href == null) {
return;
}
final Offset offset = Offset(
parserState.parseDoubleWithUnits(
parserState.attribute('x', def: '0'),
)!,
parserState.parseDoubleWithUnits(
parserState.attribute('y', def: '0'),
)!,
);
final Image image = await resolveImage(href);
final Size size = Size(
parserState.parseDoubleWithUnits(parserState.attribute('width')) ??
image.width.toDouble(),
parserState.parseDoubleWithUnits(parserState.attribute('height')) ??
image.height.toDouble(),
);
final DrawableParent parent = parserState._parentDrawables.last.drawable!;
final DrawableStyle? parentStyle = parent.style;
final DrawableRasterImage drawable = DrawableRasterImage(
parserState.attribute('id', def: ''),
image,
offset,
parserState.parseStyle(parserState.rootBounds, parentStyle,
currentColor: parent.color),
size: size,
transform: parseTransform(parserState.attribute('transform'))?.storage,
);
parserState.checkForIri(drawable);
parserState.currentGroup!.children!.add(drawable);
}
static Future<void> text(
SvgParserState parserState,
bool warningsAsErrors,
) async {
assert(parserState != null); // ignore: unnecessary_null_comparison
assert(parserState.currentGroup != null);
if (parserState._currentStartElement!.isSelfClosing) {
return;
}
// <text>, <tspan> -> Collect styles
// <tref> TBD - looks like Inkscape supports it, but no browser does.
// XmlNodeType.TEXT/CDATA -> DrawableText
// Track the style(s) and offset(s) for <text> and <tspan> elements
final Queue<_TextInfo> textInfos = ListQueue<_TextInfo>();
double lastTextWidth = 0;
void _processText(String value) {
if (value.isEmpty) {
return;
}
assert(textInfos.isNotEmpty);
final _TextInfo lastTextInfo = textInfos.last;
final Paragraph fill = createParagraph(
value,
lastTextInfo.style,
lastTextInfo.style.fill,
);
final Paragraph stroke = createParagraph(
value,
lastTextInfo.style,
DrawablePaint.isEmpty(lastTextInfo.style.stroke)
? transparentStroke
: lastTextInfo.style.stroke,
);
parserState.currentGroup!.children!.add(
DrawableText(
parserState.attribute('id', def: ''),
fill,
stroke,
lastTextInfo.offset,
lastTextInfo.style.textStyle!.anchor ??
DrawableTextAnchorPosition.start,
transform: lastTextInfo.transform?.storage,
),
);
lastTextWidth = fill.maxIntrinsicWidth;
}
void _processStartElement(XmlStartElementEvent event) {
_TextInfo? lastTextInfo;
if (textInfos.isNotEmpty) {
lastTextInfo = textInfos.last;
}
final Offset currentOffset = _parseCurrentOffset(
parserState,
lastTextInfo?.offset.translate(lastTextWidth, 0),
);
Matrix4? transform = parseTransform(parserState.attribute('transform'));
if (lastTextInfo?.transform != null) {
if (transform == null) {
transform = lastTextInfo!.transform;
} else {
transform = lastTextInfo!.transform!.multiplied(transform);
}
}
final DrawableStyle? parentStyle =
lastTextInfo?.style ?? parserState.currentGroup!.style;
textInfos.add(_TextInfo(
parserState.parseStyle(
parserState.rootBounds,
parentStyle,
),
currentOffset,
transform,
));
if (event.isSelfClosing) {
textInfos.removeLast();
}
}
_processStartElement(parserState._currentStartElement!);
for (XmlEvent event in parserState._readSubtree()) {
if (event is XmlCDATAEvent) {
_processText(event.text.trim());
} else if (event is XmlTextEvent) {
final String? space =
getAttribute(parserState.attributes, 'space', def: null);
if (space != 'preserve') {
_processText(event.text.trim());
} else {
_processText(event.text.replaceAll(_trimPattern, ''));
}
}
if (event is XmlStartElementEvent) {
_processStartElement(event);
} else if (event is XmlEndElementEvent) {
textInfos.removeLast();
}
}
}
}
// ignore: avoid_classes_with_only_static_members
class _Paths {
static Path circle(SvgParserState parserState) {
final double cx = parserState.parseDoubleWithUnits(
parserState.attribute('cx', def: '0'),
)!;
final double cy = parserState.parseDoubleWithUnits(
parserState.attribute('cy', def: '0'),
)!;
final double r = parserState.parseDoubleWithUnits(
parserState.attribute('r', def: '0'),
)!;
final Rect oval = Rect.fromCircle(center: Offset(cx, cy), radius: r);
return Path()..addOval(oval);
}
static Path path(SvgParserState parserState) {
final String d = parserState.attribute('d', def: '')!;
return parseSvgPathData(d);
}
static Path rect(SvgParserState parserState) {
final double x = parserState.parseDoubleWithUnits(
parserState.attribute('x', def: '0'),
)!;
final double y = parserState.parseDoubleWithUnits(
parserState.attribute('y', def: '0'),
)!;
final double w = parserState.parseDoubleWithUnits(
parserState.attribute('width', def: '0'),
)!;
final double h = parserState.parseDoubleWithUnits(
parserState.attribute('height', def: '0'),
)!;
final Rect rect = Rect.fromLTWH(x, y, w, h);
String? rxRaw = parserState.attribute('rx');
String? ryRaw = parserState.attribute('ry');
rxRaw ??= ryRaw;
ryRaw ??= rxRaw;
if (rxRaw != null && rxRaw != '') {
final double rx = parserState.parseDoubleWithUnits(rxRaw)!;
final double ry = parserState.parseDoubleWithUnits(ryRaw)!;
return Path()..addRRect(RRect.fromRectXY(rect, rx, ry));
}
return Path()..addRect(rect);
}
static Path? polygon(SvgParserState parserState) {
return parsePathFromPoints(parserState, true);
}
static Path? polyline(SvgParserState parserState) {
return parsePathFromPoints(parserState, false);
}
static Path? parsePathFromPoints(SvgParserState parserState, bool close) {
final String points = parserState.attribute('points', def: '')!;
if (points == '') {
return null;
}
final String path = 'M$points${close ? 'z' : ''}';
return parseSvgPathData(path);
}
static Path ellipse(SvgParserState parserState) {
final double cx = parserState.parseDoubleWithUnits(
parserState.attribute('cx', def: '0'),
)!;
final double cy = parserState.parseDoubleWithUnits(
parserState.attribute('cy', def: '0'),
)!;
final double rx = parserState.parseDoubleWithUnits(
parserState.attribute('rx', def: '0'),
)!;
final double ry = parserState.parseDoubleWithUnits(
parserState.attribute('ry', def: '0'),
)!;
final Rect r = Rect.fromLTWH(cx - rx, cy - ry, rx * 2, ry * 2);
return Path()..addOval(r);
}
static Path line(SvgParserState parserState) {
final double x1 = parserState.parseDoubleWithUnits(
parserState.attribute('x1', def: '0'),
)!;
final double x2 = parserState.parseDoubleWithUnits(
parserState.attribute('x2', def: '0'),
)!;
final double y1 = parserState.parseDoubleWithUnits(
parserState.attribute('y1', def: '0'),
)!;
final double y2 = parserState.parseDoubleWithUnits(
parserState.attribute('y2', def: '0'),
)!;
return Path()
..moveTo(x1, y1)
..lineTo(x2, y2);
}
}
class _SvgGroupTuple {
_SvgGroupTuple(this.name, this.drawable);
final String name;
final DrawableParent? drawable;
}
class _SvgCompatibilityTester extends CacheCompatibilityTester {
bool usesCurrentColor = false;
bool usesFontSize = false;
@override
bool isCompatible(Object oldData, Object newData) {
if (oldData is! SvgTheme || newData is! SvgTheme) {
return true;
}
if (usesCurrentColor && oldData.currentColor != newData.currentColor) {
return false;
}
if (usesFontSize && oldData.fontSize != newData.fontSize) {
return false;
}
return true;
}
}
/// The implementation of [SvgParser].
///
/// Maintains state while pushing an [XmlPushReader] through the SVG tree.
class SvgParserState {
/// Creates a new [SvgParserState].
SvgParserState(
Iterable<XmlEvent> events,
this.theme,
this._key,
this._warningsAsErrors,
)
// ignore: unnecessary_null_comparison
: assert(events != null),
_eventIterator = events.iterator;
_SvgCompatibilityTester _compatibilityTester = _SvgCompatibilityTester();
/// The theme used when parsing SVG elements.
final SvgTheme theme;
final Iterator<XmlEvent> _eventIterator;
final String? _key;
final bool _warningsAsErrors;
final DrawableDefinitionServer _definitions = DrawableDefinitionServer();
final Queue<_SvgGroupTuple> _parentDrawables = ListQueue<_SvgGroupTuple>(10);
DrawableRoot? _root;
late Map<String, String> _currentAttributes;
XmlStartElementEvent? _currentStartElement;
/// The current depth of the reader in the XML hierarchy.
int depth = 0;
void _discardSubtree() {
final int subtreeStartDepth = depth;
while (_eventIterator.moveNext()) {
final XmlEvent event = _eventIterator.current;
if (event is XmlStartElementEvent && !event.isSelfClosing) {
depth += 1;
} else if (event is XmlEndElementEvent) {
depth -= 1;
assert(depth >= 0);
}
_currentAttributes = <String, String>{};
_currentStartElement = null;
if (depth < subtreeStartDepth) {
return;
}
}
}
Iterable<XmlEvent> _readSubtree() sync* {
final int subtreeStartDepth = depth;
while (_eventIterator.moveNext()) {
final XmlEvent event = _eventIterator.current;
bool isSelfClosing = false;
if (event is XmlStartElementEvent) {
final Map<String, String> attributeMap =
event.attributes.toAttributeMap();
if (getAttribute(attributeMap, 'display') == 'none' ||
getAttribute(attributeMap, 'visibility') == 'hidden') {
print('SVG Warning: Discarding:\n\n $event\n\n'
'and any children it has since it is not visible.\n'
'If that element is meant to be visible, the `display` or '
'`visibility` attributes should be removed.\n'
'If that element is not meant to be visible, it would be better '
'to remove it from the SVG file.');
if (!event.isSelfClosing) {
depth += 1;
_discardSubtree();
}
continue;
}
_currentAttributes = attributeMap;
_currentStartElement = event;
depth += 1;
isSelfClosing = event.isSelfClosing;
}
yield event;
if (isSelfClosing || event is XmlEndElementEvent) {
depth -= 1;
assert(depth >= 0);
_currentAttributes = <String, String>{};
_currentStartElement = null;
}
if (depth < subtreeStartDepth) {
return;
}
}
}
/// Drive the [XmlTextReader] to EOF and produce a [DrawableRoot].
Future<DrawableRoot> parse() async {
_compatibilityTester = _SvgCompatibilityTester();
for (XmlEvent event in _readSubtree()) {
if (event is XmlStartElementEvent) {
if (startElement(event)) {
continue;
}
final _ParseFunc? parseFunc = _svgElementParsers[event.name];
await parseFunc?.call(this, _warningsAsErrors);
if (parseFunc == null) {
if (!event.isSelfClosing) {
_discardSubtree();
}
assert(() {
unhandledElement(event);
return true;
}());
}
} else if (event is XmlEndElementEvent) {
endElement(event);
}
}
if (_root == null) {
throw StateError('Invalid SVG data');
}
return _root!;
}
/// The XML Attributes of the current node in the tree.
Map<String, String> get attributes => _currentAttributes;
/// Gets the attribute for the current position of the parser.
String? attribute(String name, {String? def}) =>
getAttribute(attributes, name, def: def);
/// The current group, if any, in the [Drawable] heirarchy.
DrawableParent? get currentGroup {
assert(_parentDrawables != null); // ignore: unnecessary_null_comparison
assert(_parentDrawables.isNotEmpty);
return _parentDrawables.last.drawable;
}
/// The root bounds of the drawable.
Rect get rootBounds {
assert(_root != null, 'Cannot get rootBounds with null root');
assert(_root!.viewport != null); // ignore: unnecessary_null_comparison
return _root!.viewport.viewBoxRect;
}
/// Whether this [DrawableStyleable] belongs in the [DrawableDefinitions] or not.
bool checkForIri(DrawableStyleable? drawable) {
final String iri = buildUrlIri();
if (iri != emptyUrlIri) {
_definitions.addDrawable(iri, drawable!);
return true;
}
return false;
}
/// Appends a group to the collection.
void addGroup(XmlStartElementEvent event, DrawableParent? drawable) {
_parentDrawables.addLast(_SvgGroupTuple(event.name, drawable));
checkForIri(drawable);
}
/// Appends a [DrawableShape] to the [currentGroup].
bool addShape(XmlStartElementEvent event) {
final _PathFunc? pathFunc = _svgPathFuncs[event.name];
if (pathFunc == null) {
return false;
}
final DrawableParent parent = _parentDrawables.last.drawable!;
final DrawableStyle? parentStyle = parent.style;
final Path path = pathFunc(this)!;
final DrawableStyleable drawable = DrawableShape(
getAttribute(attributes, 'id', def: ''),
path,
parseStyle(
path.getBounds(),
parentStyle,
defaultFillColor: colorBlack,
currentColor: parent.color,
),
transform: parseTransform(getAttribute(attributes, 'transform'))?.storage,
);
checkForIri(drawable);
parent.children!.add(drawable);
return true;
}
/// Potentially handles a starting element.
bool startElement(XmlStartElementEvent event) {
if (event.name == 'defs') {
if (!event.isSelfClosing) {
addGroup(
event,
DrawableGroup(
'__defs__${event.hashCode}',
<Drawable>[],
null,
color: currentGroup?.color,
transform: currentGroup?.transform,
),
);
return true;
}
}
return addShape(event);
}
/// Handles the end of an XML element.
void endElement(XmlEndElementEvent event) {
if (event.name == _parentDrawables.last.name) {
_parentDrawables.removeLast();
}
}
/// Prints an error for unhandled elements.
///
/// Will only print an error once for unhandled/unexpected elements, except for
/// `<style/>`, `<title/>`, and `<desc/>` elements.
void unhandledElement(XmlStartElementEvent event) {
final String errorMessage =
'unhandled element ${event.name}; Picture key: $_key';
if (_warningsAsErrors) {
// Throw error instead of log warning.
throw UnimplementedError(errorMessage);
}
if (event.name == 'style') {
FlutterError.reportError(FlutterErrorDetails(
exception: UnimplementedError(
'The <style> element is not implemented in this library.'),
informationCollector: () => <DiagnosticsNode>[
ErrorDescription(
'Style elements are not supported by this library and the requested SVG may not '
'render as intended.'),
ErrorHint(
'If possible, ensure the SVG uses inline styles and/or attributes (which are '
'supported), or use a preprocessing utility such as svgcleaner to inline the '
'styles for you.'),
ErrorDescription(''),
DiagnosticsProperty<String>('Picture key', _key),
],
library: 'SVG',
context: ErrorDescription('in parseSvgElement'),
));
} else if (_unhandledElements.add(event.name)) {
print(errorMessage);
}
}
/// The number of pixels per CSS inch.
static const int kCssPixelsPerInch = 96;
/// The number of points per CSS inch.
static const int kCssPointsPerInch = 72;
/// The multiplicand to convert from CSS points to pixels.
static const double kPointsToPixelFactor =
kCssPixelsPerInch / kCssPointsPerInch;
/// Parses a `rawDouble` `String` to a `double`
/// taking into account absolute and relative units
/// (`px`, `em` or `ex`).
///
/// Passing an `em` value will calculate the result
/// relative to the provided [fontSize]:
/// 1 em = 1 * `fontSize`.
///
/// Passing an `ex` value will calculate the result
/// relative to the provided [xHeight]:
/// 1 ex = 1 * `xHeight`.
///
/// The `rawDouble` might include a unit which is
/// stripped off when parsed to a `double`.
///
/// Passing `null` will return `null`.
double? parseDoubleWithUnits(
String? rawDouble, {
bool tryParse = false,
}) {
if (rawDouble == null) {
return null;
}
double unit = 1.0;
// 1 rem unit is equal to the root font size.
// 1 em unit is equal to the current font size.
// 1 ex unit is equal to the current x-height.
if (rawDouble.contains('pt')) {
unit = kPointsToPixelFactor;
} else if (rawDouble.contains('rem')) {
_compatibilityTester.usesFontSize = true;
unit = theme.fontSize;
} else if (rawDouble.contains('em')) {
_compatibilityTester.usesFontSize = true;
unit = theme.fontSize;
} else if (rawDouble.contains('ex')) {
_compatibilityTester.usesFontSize = true;
unit = theme.xHeight;
}
final double? value = parseDouble(
rawDouble,
tryParse: tryParse,
);
return value != null ? value * unit : null;
}
static final Map<String, double> _kTextSizeMap = <String, double>{
'xx-small': 10,
'x-small': 12,
'small': 14,
'medium': 18,
'large': 22,
'x-large': 26,
'xx-large': 32,
};
/// Parses a `font-size` attribute.
double? parseFontSize(
String? raw, {
double? parentValue,
}) {
if (raw == null || raw == '') {
return null;
}
double? ret = parseDoubleWithUnits(
raw,
tryParse: true,
);
if (ret != null) {
return ret;
}
raw = raw.toLowerCase().trim();
ret = _kTextSizeMap[raw];
if (ret != null) {
return ret;
}
if (raw == 'larger') {
if (parentValue == null) {
return _kTextSizeMap['large'];
}
return parentValue * 1.2;
}
if (raw == 'smaller') {
if (parentValue == null) {
return _kTextSizeMap['small'];
}
return parentValue / 1.2;
}
throw StateError('Could not parse font-size: $raw');
}
double _parseRawWidthHeight(String raw) {
if (raw == '100%' || raw == '') {
return double.infinity;
}
assert(() {
final RegExp notDigits = RegExp(r'[^\d\.]');
if (!raw.endsWith('px') &&
!raw.endsWith('em') &&
!raw.endsWith('ex') &&
raw.contains(notDigits)) {
print(
'Warning: Flutter SVG only supports the following formats for `width` and `height` on the SVG root:\n'
' width="100%"\n'
' width="100em"\n'
' width="100ex"\n'
' width="100px"\n'
' width="100" (where the number will be treated as pixels).\n'
'The supplied value ($raw) will be discarded and treated as if it had not been specified.');
}
return true;
}());
return parseDoubleWithUnits(raw, tryParse: true) ?? double.infinity;
}
/// Parses an SVG @viewBox attribute (e.g. 0 0 100 100) to a [Rect].
///
/// The [nullOk] parameter controls whether this function should throw if there is no
/// viewBox or width/height parameters.
///
/// The [respectWidthHeight] parameter specifies whether `width` and `height` attributes
/// on the root SVG element should be treated in accordance with the specification.
DrawableViewport? parseViewBox({bool nullOk = false}) {
final String viewBox = getAttribute(attributes, 'viewBox')!;
final String rawWidth = getAttribute(attributes, 'width')!;
final String rawHeight = getAttribute(attributes, 'height')!;
if (viewBox == '' && rawWidth == '' && rawHeight == '') {
if (nullOk) {
return null;
}
throw StateError('SVG did not specify dimensions\n\n'
'The SVG library looks for a `viewBox` or `width` and `height` attribute '
'to determine the viewport boundary of the SVG. Note that these attributes, '
'as with all SVG attributes, are case sensitive.\n'
'During processing, the following attributes were found:\n'
' $attributes');
}
final double width = _parseRawWidthHeight(rawWidth);
final double height = _parseRawWidthHeight(rawHeight);
if (viewBox == '') {
return DrawableViewport(
Size(width, height),
Size(width, height),
);
}
final List<String> parts = viewBox.split(RegExp(r'[ ,]+'));
if (parts.length < 4) {
throw StateError('viewBox element must be 4 elements long');
}
return DrawableViewport(
Size(width, height),
Size(
parseDouble(parts[2])!,
parseDouble(parts[3])!,
),
viewBoxOffset: Offset(
-parseDouble(parts[0])!,
-parseDouble(parts[1])!,
),
);
}
/// Builds an IRI in the form of `'url(#id)'`.
String buildUrlIri() => 'url(#${getAttribute(attributes, 'id')})';
/// An empty IRI.
static const String emptyUrlIri = DrawableDefinitionServer.emptyUrlIri;
/// Parses an @stroke-dasharray attribute into a [CircularIntervalList].
///
/// Does not currently support percentages.
CircularIntervalList<double>? parseDashArray() {
final String? rawDashArray = getAttribute(attributes, 'stroke-dasharray');
if (rawDashArray == '') {
return null;
} else if (rawDashArray == 'none') {
return DrawableStyle.emptyDashArray;
}
final List<String> parts = rawDashArray!.split(RegExp(r'[ ,]+'));
final List<double> doubles = <double>[];
bool atLeastOneNonZeroDash = false;
for (final String part in parts) {
final double dashOffset = parseDoubleWithUnits(part)!;
if (dashOffset != 0) {
atLeastOneNonZeroDash = true;
}
doubles.add(dashOffset);
}
if (doubles.isEmpty || !atLeastOneNonZeroDash) {
return null;
}
return CircularIntervalList<double>(doubles);
}
/// Parses a @stroke-dashoffset into a [DashOffset].
DashOffset? parseDashOffset() {
final String? rawDashOffset = getAttribute(attributes, 'stroke-dashoffset');
if (rawDashOffset == '') {
return null;
}
if (rawDashOffset!.endsWith('%')) {
return DashOffset.percentage(parsePercentage(rawDashOffset));
} else {
return DashOffset.absolute(parseDoubleWithUnits(rawDashOffset)!);
}
}
/// Parses a `spreadMethod` attribute into a [TileMode].
TileMode parseTileMode() {
final String? spreadMethod = attribute('spreadMethod', def: 'pad');
switch (spreadMethod) {
case 'pad':
return TileMode.clamp;
case 'repeat':
return TileMode.repeated;
case 'reflect':
return TileMode.mirror;
default:
return TileMode.clamp;
}
}
/// Parses an @opacity value into a [double], clamped between 0..1.
double? parseOpacity() {
final String? rawOpacity = getAttribute(attributes, 'opacity', def: null);
if (rawOpacity != null) {
return parseDouble(rawOpacity)!.clamp(0.0, 1.0).toDouble();
}
return null;
}
DrawablePaint _getDefinitionPaint(
String? key,
PaintingStyle paintingStyle,
String iri,
DrawableDefinitionServer definitions,
Rect bounds, {
double? opacity,
}) {
final Shader? shader = definitions.getShader(iri, bounds);
if (shader == null) {
reportMissingDef(key, iri, '_getDefinitionPaint');
}
return DrawablePaint(
paintingStyle,
shader: shader,
color: opacity != null ? Color.fromRGBO(255, 255, 255, opacity) : null,
);
}
/// Parses a @stroke attribute into a [Paint].
DrawablePaint? parseStroke(
Rect bounds,
DrawablePaint? parentStroke,
Color? currentColor,
) {
final String? rawStroke = getAttribute(attributes, 'stroke', def: null);
final String? rawStrokeOpacity = getAttribute(
attributes,
'stroke-opacity',
def: '1.0',
);
final String? rawOpacity = getAttribute(attributes, 'opacity');
double opacity = parseDouble(rawStrokeOpacity)!.clamp(0.0, 1.0).toDouble();
if (rawOpacity != '') {
opacity *= parseDouble(rawOpacity)!.clamp(0.0, 1.0);
}
final String? rawStrokeCap =
getAttribute(attributes, 'stroke-linecap', def: null);
final String? rawLineJoin =
getAttribute(attributes, 'stroke-linejoin', def: null);
final String? rawMiterLimit =
getAttribute(attributes, 'stroke-miterlimit', def: null);
final String? rawStrokeWidth =
getAttribute(attributes, 'stroke-width', def: null);
final String? anyStrokeAttribute = rawStroke ??
rawStrokeCap ??
rawLineJoin ??
rawMiterLimit ??
rawStrokeWidth;
if (anyStrokeAttribute == null && DrawablePaint.isEmpty(parentStroke)) {
return null;
} else if (rawStroke == 'none') {
return DrawablePaint.empty;
}
DrawablePaint? definitionPaint;
Color? strokeColor;
if (rawStroke?.startsWith('url') == true) {
definitionPaint = _getDefinitionPaint(
_key,
PaintingStyle.stroke,
rawStroke!,
_definitions,
bounds,
opacity: opacity,
);
strokeColor = definitionPaint.color;
} else {
strokeColor = parseColor(rawStroke, currentColor: currentColor);
}
final DrawablePaint paint = DrawablePaint(
PaintingStyle.stroke,
color: (strokeColor ??
currentColor ??
parentStroke?.color ??
definitionPaint?.color)
?.withOpacity(opacity),
strokeCap: StrokeCap.values.firstWhere(
(StrokeCap sc) => sc.toString() == 'StrokeCap.$rawStrokeCap',
orElse: () =>
parentStroke?.strokeCap ??
definitionPaint?.strokeCap ??
StrokeCap.butt,
),
strokeJoin: StrokeJoin.values.firstWhere(
(StrokeJoin sj) => sj.toString() == 'StrokeJoin.$rawLineJoin',
orElse: () =>
parentStroke?.strokeJoin ??
definitionPaint?.strokeJoin ??
StrokeJoin.miter,
),
strokeMiterLimit: parseDouble(rawMiterLimit) ??
parentStroke?.strokeMiterLimit ??
definitionPaint?.strokeMiterLimit ??
4.0,
strokeWidth: parseDoubleWithUnits(rawStrokeWidth) ??
parentStroke?.strokeWidth ??
definitionPaint?.strokeWidth ??
1.0,
);
return DrawablePaint.merge(definitionPaint, paint);
}
/// Parses a `fill` attribute.
DrawablePaint? parseFill(
Rect bounds,
DrawablePaint? parentFill,
Color? defaultFillColor,
Color? currentColor,
) {
final String rawFill = attribute('fill', def: '')!;
final String? rawFillOpacity = attribute('fill-opacity', def: '1.0');
final String? rawOpacity = attribute('opacity', def: '');
double opacity = parseDouble(rawFillOpacity)!.clamp(0.0, 1.0).toDouble();
if (rawOpacity != '') {
opacity *= parseDouble(rawOpacity)!.clamp(0.0, 1.0);
}
if (rawFill.startsWith('url')) {
return _getDefinitionPaint(
_key,
PaintingStyle.fill,
rawFill,
_definitions,
bounds,
opacity: opacity,
);
}
final Color? fillColor = _determineFillColor(
parentFill?.color,
rawFill,
opacity,
rawOpacity != '' || rawFillOpacity != '',
defaultFillColor,
currentColor,
);
if (rawFill == '' &&
(fillColor == null || parentFill == DrawablePaint.empty)) {
return null;
}
if (rawFill == 'none') {
return DrawablePaint.empty;
}
return DrawablePaint(
PaintingStyle.fill,
color: fillColor,
);
}
Color? _determineFillColor(
Color? parentFillColor,
String rawFill,
double opacity,
bool explicitOpacity,
Color? defaultFillColor,
Color? currentColor,
) {
final Color? color = parseColor(rawFill, currentColor: currentColor) ??
parentFillColor ??
defaultFillColor;
if (explicitOpacity && color != null) {
return color.withOpacity(opacity);
}
return color;
}
/// Parses a `fill-rule` attribute into a [PathFillType].
PathFillType? parseFillRule([
String attr = 'fill-rule',
String? def = 'nonzero',
]) {
final String? rawFillRule = getAttribute(attributes, attr, def: def);
return parseRawFillRule(rawFillRule);
}
/// Applies a transform to a path if the [attributes] contain a `transform`.
Path? applyTransformIfNeeded(Path? path) {
final Matrix4? transform =
parseTransform(getAttribute(attributes, 'transform', def: null));
if (transform != null) {
return path!.transform(transform.storage);
} else {
return path;
}
}
/// Parses a `clipPath` element into a list of [Path]s.
List<Path>? parseClipPath() {
final String? rawClipAttribute = getAttribute(attributes, 'clip-path');
if (rawClipAttribute != '') {
return _definitions.getClipPath(rawClipAttribute!);
}
return null;
}
static const Map<String, BlendMode> _blendModes = <String, BlendMode>{
'multiply': BlendMode.multiply,
'screen': BlendMode.screen,
'overlay': BlendMode.overlay,
'darken': BlendMode.darken,
'lighten': BlendMode.lighten,
'color-dodge': BlendMode.colorDodge,
'color-burn': BlendMode.colorBurn,
'hard-light': BlendMode.hardLight,
'soft-light': BlendMode.softLight,
'difference': BlendMode.difference,
'exclusion': BlendMode.exclusion,
'hue': BlendMode.hue,
'saturation': BlendMode.saturation,
'color': BlendMode.color,
'luminosity': BlendMode.luminosity,
};
/// Lookup the mask if the attribute is present.
DrawableStyleable? parseMask() {
final String? rawMaskAttribute = getAttribute(attributes, 'mask');
if (rawMaskAttribute != '') {
return _definitions.getDrawable(rawMaskAttribute!);
}
return null;
}
/// Parses a `font-weight` attribute value into a [FontWeight].
FontWeight? parseFontWeight(String? fontWeight) {
if (fontWeight == null) {
return null;
}
switch (fontWeight) {
case '100':
return FontWeight.w100;
case '200':
return FontWeight.w200;
case '300':
return FontWeight.w300;
case 'normal':
case '400':
return FontWeight.w400;
case '500':
return FontWeight.w500;
case '600':
return FontWeight.w600;
case 'bold':
case '700':
return FontWeight.w700;
case '800':
return FontWeight.w800;
case '900':
return FontWeight.w900;
}
throw UnsupportedError('Attribute value for font-weight="$fontWeight"'
' is not supported');
}
/// Parses a `font-style` attribute value into a [FontStyle].
FontStyle? parseFontStyle(String? fontStyle) {
if (fontStyle == null) {
return null;
}
switch (fontStyle) {
case 'normal':
return FontStyle.normal;
case 'italic':
case 'oblique':
return FontStyle.italic;
}
throw UnsupportedError('Attribute value for font-style="$fontStyle"'
' is not supported');
}
/// Parses a `text-decoration` attribute value into a [TextDecoration].
TextDecoration? parseTextDecoration(String? textDecoration) {
if (textDecoration == null) {
return null;
}
switch (textDecoration) {
case 'none':
return TextDecoration.none;
case 'underline':
return TextDecoration.underline;
case 'overline':
return TextDecoration.overline;
case 'line-through':
return TextDecoration.lineThrough;
}
throw UnsupportedError(
'Attribute value for text-decoration="$textDecoration"'
' is not supported');
}
/// Parses a `text-decoration-style` attribute value into a [TextDecorationStyle].
TextDecorationStyle? parseTextDecorationStyle(String? textDecorationStyle) {
if (textDecorationStyle == null) {
return null;
}
switch (textDecorationStyle) {
case 'solid':
return TextDecorationStyle.solid;
case 'dashed':
return TextDecorationStyle.dashed;
case 'dotted':
return TextDecorationStyle.dotted;
case 'double':
return TextDecorationStyle.double;
case 'wavy':
return TextDecorationStyle.wavy;
}
throw UnsupportedError(
'Attribute value for text-decoration-style="$textDecorationStyle"'
' is not supported');
}
/// Parses style attributes or @style attribute.
///
/// Remember that @style attribute takes precedence.
DrawableStyle parseStyle(
Rect bounds,
DrawableStyle? parentStyle, {
Color? defaultFillColor,
Color? currentColor,
}) {
return DrawableStyle.mergeAndBlend(
parentStyle,
stroke: parseStroke(bounds, parentStyle?.stroke, currentColor),
dashArray: parseDashArray(),
dashOffset: parseDashOffset(),
fill:
parseFill(bounds, parentStyle?.fill, defaultFillColor, currentColor),
pathFillType: parseFillRule(
'fill-rule',
parentStyle != null ? null : 'nonzero',
),
groupOpacity: parseOpacity(),
mask: parseMask(),
clipPath: parseClipPath(),
textStyle: DrawableTextStyle(
fontFamily: getAttribute(attributes, 'font-family'),
fontSize: parseFontSize(getAttribute(attributes, 'font-size'),
parentValue: parentStyle?.textStyle?.fontSize),
fontWeight: parseFontWeight(
getAttribute(attributes, 'font-weight', def: null),
),
fontStyle: parseFontStyle(
getAttribute(attributes, 'font-style', def: null),
),
anchor: parseTextAnchor(
getAttribute(attributes, 'text-anchor', def: 'inherit'),
),
decoration: parseTextDecoration(
getAttribute(attributes, 'text-decoration', def: null),
),
decorationColor: parseColor(
getAttribute(attributes, 'text-decoration-color', def: null),
currentColor: currentColor,
),
decorationStyle: parseTextDecorationStyle(
getAttribute(attributes, 'text-decoration-style', def: null),
),
),
blendMode: _blendModes[getAttribute(attributes, 'mix-blend-mode')!],
);
}
/// Converts a SVG Color String (either a # prefixed color string or a named color) to a [Color].
Color? parseColor(String? colorString, {Color? currentColor}) {
if (colorString == null || colorString.isEmpty) {
return null;
}
if (colorString == 'none') {
return null;
}
if (colorString.toLowerCase() == 'currentcolor') {
_compatibilityTester.usesCurrentColor = true;
return currentColor ?? theme.currentColor;
}
// handle hex colors e.g. #fff or #ffffff. This supports #RRGGBBAA
if (colorString[0] == '#') {
if (colorString.length == 4) {
final String r = colorString[1];
final String g = colorString[2];
final String b = colorString[3];
colorString = '#$r$r$g$g$b$b';
}
int color = int.parse(colorString.substring(1), radix: 16);
if (colorString.length == 7) {
return Color(color |= 0xFF000000);
}
if (colorString.length == 9) {
return Color(color);
}
}
// 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);
}
// Conversion code from: https://github.com/MichaelFenwick/Color, thanks :)
if (colorString.toLowerCase().startsWith('hsl')) {
final List<int> values = colorString
.substring(colorString.indexOf('(') + 1, colorString.indexOf(')'))
.split(',')
.map((String rawColor) {
rawColor = rawColor.trim();
if (rawColor.endsWith('%')) {
rawColor = rawColor.substring(0, rawColor.length - 1);
}
if (rawColor.contains('.')) {
return (parseDouble(rawColor)! * 2.55).round();
}
return int.parse(rawColor);
}).toList();
final double hue = values[0] / 360 % 1;
final double saturation = values[1] / 100;
final double luminance = values[2] / 100;
final int alpha = values.length > 3 ? values[3] : 255;
List<double> 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());
}
// 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) {
return namedColor;
}
// This is an error, but browsers are permissive here, so we can be too.
// See for example https://github.com/dnfield/flutter_svg/issues/764 - a
// user may be working with a network based SVG that uses the string "null"
// which is not part of the specification.
return null;
}
}
/// The color black, with full opacity.
const Color colorBlack = Color(0xFF000000);
// https://www.w3.org/TR/SVG11/types.html#ColorKeywords
const Map<String, Color> _namedColors = <String, Color>{
'aliceblue': Color.fromARGB(255, 240, 248, 255),
'antiquewhite': Color.fromARGB(255, 250, 235, 215),
'aqua': Color.fromARGB(255, 0, 255, 255),
'aquamarine': Color.fromARGB(255, 127, 255, 212),
'azure': Color.fromARGB(255, 240, 255, 255),
'beige': Color.fromARGB(255, 245, 245, 220),
'bisque': Color.fromARGB(255, 255, 228, 196),
'black': Color.fromARGB(255, 0, 0, 0),
'blanchedalmond': Color.fromARGB(255, 255, 235, 205),
'blue': Color.fromARGB(255, 0, 0, 255),
'blueviolet': Color.fromARGB(255, 138, 43, 226),
'brown': Color.fromARGB(255, 165, 42, 42),
'burlywood': Color.fromARGB(255, 222, 184, 135),
'cadetblue': Color.fromARGB(255, 95, 158, 160),
'chartreuse': Color.fromARGB(255, 127, 255, 0),
'chocolate': Color.fromARGB(255, 210, 105, 30),
'coral': Color.fromARGB(255, 255, 127, 80),
'cornflowerblue': Color.fromARGB(255, 100, 149, 237),
'cornsilk': Color.fromARGB(255, 255, 248, 220),
'crimson': Color.fromARGB(255, 220, 20, 60),
'cyan': Color.fromARGB(255, 0, 255, 255),
'darkblue': Color.fromARGB(255, 0, 0, 139),
'darkcyan': Color.fromARGB(255, 0, 139, 139),
'darkgoldenrod': Color.fromARGB(255, 184, 134, 11),
'darkgray': Color.fromARGB(255, 169, 169, 169),
'darkgreen': Color.fromARGB(255, 0, 100, 0),
'darkgrey': Color.fromARGB(255, 169, 169, 169),
'darkkhaki': Color.fromARGB(255, 189, 183, 107),
'darkmagenta': Color.fromARGB(255, 139, 0, 139),
'darkolivegreen': Color.fromARGB(255, 85, 107, 47),
'darkorange': Color.fromARGB(255, 255, 140, 0),
'darkorchid': Color.fromARGB(255, 153, 50, 204),
'darkred': Color.fromARGB(255, 139, 0, 0),
'darksalmon': Color.fromARGB(255, 233, 150, 122),
'darkseagreen': Color.fromARGB(255, 143, 188, 143),
'darkslateblue': Color.fromARGB(255, 72, 61, 139),
'darkslategray': Color.fromARGB(255, 47, 79, 79),
'darkslategrey': Color.fromARGB(255, 47, 79, 79),
'darkturquoise': Color.fromARGB(255, 0, 206, 209),
'darkviolet': Color.fromARGB(255, 148, 0, 211),
'deeppink': Color.fromARGB(255, 255, 20, 147),
'deepskyblue': Color.fromARGB(255, 0, 191, 255),
'dimgray': Color.fromARGB(255, 105, 105, 105),
'dimgrey': Color.fromARGB(255, 105, 105, 105),
'dodgerblue': Color.fromARGB(255, 30, 144, 255),
'firebrick': Color.fromARGB(255, 178, 34, 34),
'floralwhite': Color.fromARGB(255, 255, 250, 240),
'forestgreen': Color.fromARGB(255, 34, 139, 34),
'fuchsia': Color.fromARGB(255, 255, 0, 255),
'gainsboro': Color.fromARGB(255, 220, 220, 220),
'ghostwhite': Color.fromARGB(255, 248, 248, 255),
'gold': Color.fromARGB(255, 255, 215, 0),
'goldenrod': Color.fromARGB(255, 218, 165, 32),
'gray': Color.fromARGB(255, 128, 128, 128),
'grey': Color.fromARGB(255, 128, 128, 128),
'green': Color.fromARGB(255, 0, 128, 0),
'greenyellow': Color.fromARGB(255, 173, 255, 47),
'honeydew': Color.fromARGB(255, 240, 255, 240),
'hotpink': Color.fromARGB(255, 255, 105, 180),
'indianred': Color.fromARGB(255, 205, 92, 92),
'indigo': Color.fromARGB(255, 75, 0, 130),
'ivory': Color.fromARGB(255, 255, 255, 240),
'khaki': Color.fromARGB(255, 240, 230, 140),
'lavender': Color.fromARGB(255, 230, 230, 250),
'lavenderblush': Color.fromARGB(255, 255, 240, 245),
'lawngreen': Color.fromARGB(255, 124, 252, 0),
'lemonchiffon': Color.fromARGB(255, 255, 250, 205),
'lightblue': Color.fromARGB(255, 173, 216, 230),
'lightcoral': Color.fromARGB(255, 240, 128, 128),
'lightcyan': Color.fromARGB(255, 224, 255, 255),
'lightgoldenrodyellow': Color.fromARGB(255, 250, 250, 210),
'lightgray': Color.fromARGB(255, 211, 211, 211),
'lightgreen': Color.fromARGB(255, 144, 238, 144),
'lightgrey': Color.fromARGB(255, 211, 211, 211),
'lightpink': Color.fromARGB(255, 255, 182, 193),
'lightsalmon': Color.fromARGB(255, 255, 160, 122),
'lightseagreen': Color.fromARGB(255, 32, 178, 170),
'lightskyblue': Color.fromARGB(255, 135, 206, 250),
'lightslategray': Color.fromARGB(255, 119, 136, 153),
'lightslategrey': Color.fromARGB(255, 119, 136, 153),
'lightsteelblue': Color.fromARGB(255, 176, 196, 222),
'lightyellow': Color.fromARGB(255, 255, 255, 224),
'lime': Color.fromARGB(255, 0, 255, 0),
'limegreen': Color.fromARGB(255, 50, 205, 50),
'linen': Color.fromARGB(255, 250, 240, 230),
'magenta': Color.fromARGB(255, 255, 0, 255),
'maroon': Color.fromARGB(255, 128, 0, 0),
'mediumaquamarine': Color.fromARGB(255, 102, 205, 170),
'mediumblue': Color.fromARGB(255, 0, 0, 205),
'mediumorchid': Color.fromARGB(255, 186, 85, 211),
'mediumpurple': Color.fromARGB(255, 147, 112, 219),
'mediumseagreen': Color.fromARGB(255, 60, 179, 113),
'mediumslateblue': Color.fromARGB(255, 123, 104, 238),
'mediumspringgreen': Color.fromARGB(255, 0, 250, 154),
'mediumturquoise': Color.fromARGB(255, 72, 209, 204),
'mediumvioletred': Color.fromARGB(255, 199, 21, 133),
'midnightblue': Color.fromARGB(255, 25, 25, 112),
'mintcream': Color.fromARGB(255, 245, 255, 250),
'mistyrose': Color.fromARGB(255, 255, 228, 225),
'moccasin': Color.fromARGB(255, 255, 228, 181),
'navajowhite': Color.fromARGB(255, 255, 222, 173),
'navy': Color.fromARGB(255, 0, 0, 128),
'oldlace': Color.fromARGB(255, 253, 245, 230),
'olive': Color.fromARGB(255, 128, 128, 0),
'olivedrab': Color.fromARGB(255, 107, 142, 35),
'orange': Color.fromARGB(255, 255, 165, 0),
'orangered': Color.fromARGB(255, 255, 69, 0),
'orchid': Color.fromARGB(255, 218, 112, 214),
'palegoldenrod': Color.fromARGB(255, 238, 232, 170),
'palegreen': Color.fromARGB(255, 152, 251, 152),
'paleturquoise': Color.fromARGB(255, 175, 238, 238),
'palevioletred': Color.fromARGB(255, 219, 112, 147),
'papayawhip': Color.fromARGB(255, 255, 239, 213),
'peachpuff': Color.fromARGB(255, 255, 218, 185),
'peru': Color.fromARGB(255, 205, 133, 63),
'pink': Color.fromARGB(255, 255, 192, 203),
'plum': Color.fromARGB(255, 221, 160, 221),
'powderblue': Color.fromARGB(255, 176, 224, 230),
'purple': Color.fromARGB(255, 128, 0, 128),
'red': Color.fromARGB(255, 255, 0, 0),
'rosybrown': Color.fromARGB(255, 188, 143, 143),
'royalblue': Color.fromARGB(255, 65, 105, 225),
'saddlebrown': Color.fromARGB(255, 139, 69, 19),
'salmon': Color.fromARGB(255, 250, 128, 114),
'sandybrown': Color.fromARGB(255, 244, 164, 96),
'seagreen': Color.fromARGB(255, 46, 139, 87),
'seashell': Color.fromARGB(255, 255, 245, 238),
'sienna': Color.fromARGB(255, 160, 82, 45),
'silver': Color.fromARGB(255, 192, 192, 192),
'skyblue': Color.fromARGB(255, 135, 206, 235),
'slateblue': Color.fromARGB(255, 106, 90, 205),
'slategray': Color.fromARGB(255, 112, 128, 144),
'slategrey': Color.fromARGB(255, 112, 128, 144),
'snow': Color.fromARGB(255, 255, 250, 250),
'springgreen': Color.fromARGB(255, 0, 255, 127),
'steelblue': Color.fromARGB(255, 70, 130, 180),
'tan': Color.fromARGB(255, 210, 180, 140),
'teal': Color.fromARGB(255, 0, 128, 128),
'thistle': Color.fromARGB(255, 216, 191, 216),
'tomato': Color.fromARGB(255, 255, 99, 71),
'transparent': Color.fromARGB(0, 255, 255, 255),
'turquoise': Color.fromARGB(255, 64, 224, 208),
'violet': Color.fromARGB(255, 238, 130, 238),
'wheat': Color.fromARGB(255, 245, 222, 179),
'white': Color.fromARGB(255, 255, 255, 255),
'whitesmoke': Color.fromARGB(255, 245, 245, 245),
'yellow': Color.fromARGB(255, 255, 255, 0),
'yellowgreen': Color.fromARGB(255, 154, 205, 50),
};