blob: 9a275e3cf4cd0e08bcbe01e7671b527c69a67fd5 [file] [log] [blame]
import 'dart:convert' hide Codec;
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:vector_math/vector_math_64.dart';
import '../utilities/http.dart';
import '../utilities/numbers.dart';
import '../vector_drawable.dart';
/// Parses a `text-anchor` attribute.
DrawableTextAnchorPosition? parseTextAnchor(String? raw) {
switch (raw) {
case 'inherit':
return null;
case 'middle':
return DrawableTextAnchorPosition.middle;
case 'end':
return DrawableTextAnchorPosition.end;
case 'start':
default:
return DrawableTextAnchorPosition.start;
}
}
const String _transformCommandAtom = ' *,?([^(]+)\\(([^)]*)\\)';
final RegExp _transformValidator = RegExp('^($_transformCommandAtom)*\$');
final RegExp _transformCommand = RegExp(_transformCommandAtom);
typedef _MatrixParser = Matrix4 Function(String? paramsStr, Matrix4 current);
const Map<String, _MatrixParser> _matrixParsers = <String, _MatrixParser>{
'matrix': _parseSvgMatrix,
'translate': _parseSvgTranslate,
'scale': _parseSvgScale,
'rotate': _parseSvgRotate,
'skewX': _parseSvgSkewX,
'skewY': _parseSvgSkewY,
};
/// Parses a SVG transform attribute into a [Matrix4].
///
/// Based on work in the "vi-tool" by @amirh, but extended to support additional
/// transforms and use a Matrix4 rather than Matrix3 for the affine matrices.
///
/// Also adds [x] and [y] to append as a final translation, e.g. for `<use>`.
Matrix4? parseTransform(String? transform) {
if (transform == null || transform == '') {
return null;
}
if (!_transformValidator.hasMatch(transform))
throw StateError('illegal or unsupported transform: $transform');
final Iterable<Match> matches =
_transformCommand.allMatches(transform).toList().reversed;
Matrix4 result = Matrix4.identity();
for (Match m in matches) {
final String command = m.group(1)!.trim();
final String? params = m.group(2);
final _MatrixParser? transformer = _matrixParsers[command];
if (transformer == null) {
throw StateError('Unsupported transform: $command');
}
result = transformer(params, result);
}
return result;
}
final RegExp _valueSeparator = RegExp('( *, *| +)');
Matrix4 _parseSvgMatrix(String? paramsStr, Matrix4 current) {
final List<String> params = paramsStr!.trim().split(_valueSeparator);
assert(params.isNotEmpty);
assert(params.length == 6);
final double a = parseDouble(params[0])!;
final double b = parseDouble(params[1])!;
final double c = parseDouble(params[2])!;
final double d = parseDouble(params[3])!;
final double e = parseDouble(params[4])!;
final double f = parseDouble(params[5])!;
return affineMatrix(a, b, c, d, e, f).multiplied(current);
}
Matrix4 _parseSvgSkewX(String? paramsStr, Matrix4 current) {
final double x = parseDouble(paramsStr)!;
return affineMatrix(1.0, 0.0, tan(x), 1.0, 0.0, 0.0).multiplied(current);
}
Matrix4 _parseSvgSkewY(String? paramsStr, Matrix4 current) {
final double y = parseDouble(paramsStr)!;
return affineMatrix(1.0, tan(y), 0.0, 1.0, 0.0, 0.0).multiplied(current);
}
Matrix4 _parseSvgTranslate(String? paramsStr, Matrix4 current) {
final List<String> params = paramsStr!.split(_valueSeparator);
assert(params.isNotEmpty);
assert(params.length <= 2);
final double x = parseDouble(params[0])!;
final double y = params.length < 2 ? 0.0 : parseDouble(params[1])!;
return affineMatrix(1.0, 0.0, 0.0, 1.0, x, y).multiplied(current);
}
Matrix4 _parseSvgScale(String? paramsStr, Matrix4 current) {
final List<String> params = paramsStr!.split(_valueSeparator);
assert(params.isNotEmpty);
assert(params.length <= 2);
final double x = parseDouble(params[0])!;
final double y = params.length < 2 ? x : parseDouble(params[1])!;
return affineMatrix(x, 0.0, 0.0, y, 0.0, 0.0).multiplied(current);
}
Matrix4 _parseSvgRotate(String? paramsStr, Matrix4 current) {
final List<String> params = paramsStr!.split(_valueSeparator);
assert(params.length <= 3);
final double a = radians(parseDouble(params[0])!);
final Matrix4 rotate =
affineMatrix(cos(a), sin(a), -sin(a), cos(a), 0.0, 0.0);
if (params.length > 1) {
final double x = parseDouble(params[1])!;
final double y = params.length == 3 ? parseDouble(params[2])! : x;
return affineMatrix(1.0, 0.0, 0.0, 1.0, x, y)
.multiplied(current)
.multiplied(rotate)
.multiplied(affineMatrix(1.0, 0.0, 0.0, 1.0, -x, -y));
} else {
return rotate.multiplied(current);
}
}
/// Creates a [Matrix4] affine matrix.
Matrix4 affineMatrix(
double a, double b, double c, double d, double e, double f) {
return Matrix4(
a, b, 0.0, 0.0, c, d, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, e, f, 0.0, 1.0);
}
/// Parses a `fill-rule` attribute.
PathFillType? parseRawFillRule(String? rawFillRule) {
if (rawFillRule == 'inherit' || rawFillRule == null) {
return null;
}
return rawFillRule != 'evenodd' ? PathFillType.nonZero : PathFillType.evenOdd;
}
final RegExp _whitespacePattern = RegExp(r'\s');
/// Resolves an image reference, potentially downloading it via HTTP.
Future<Image> resolveImage(String href) async {
assert(href != '');
final Future<Image> Function(Uint8List) decodeImage =
(Uint8List bytes) async {
final Codec codec = await instantiateImageCodec(bytes);
final FrameInfo frame = await codec.getNextFrame();
return frame.image;
};
if (href.startsWith('http')) {
final Uint8List bytes = await httpGet(href);
return decodeImage(bytes);
}
if (href.startsWith('data:')) {
final int commaLocation = href.indexOf(',') + 1;
final Uint8List bytes = base64.decode(
href.substring(commaLocation).replaceAll(_whitespacePattern, ''));
return decodeImage(bytes);
}
throw UnsupportedError('Could not resolve image href: $href');
}
const ParagraphConstraints _infiniteParagraphConstraints = ParagraphConstraints(
width: double.infinity,
);
/// A [DrawablePaint] with a transparent stroke.
const DrawablePaint transparentStroke =
DrawablePaint(PaintingStyle.stroke, color: Color(0x0));
/// Creates a [Paragraph] object using the specified [text], [style], and
/// [foregroundOverride].
Paragraph createParagraph(
String text,
DrawableStyle style,
DrawablePaint? foregroundOverride,
) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle())
..pushStyle(
style.textStyle!.toFlutterTextStyle(
foregroundOverride: foregroundOverride,
),
)
..addText(text);
return builder.build()..layout(_infiniteParagraphConstraints);
}
/// Parses strings in the form of '1.0' or '100%'.
double parseDecimalOrPercentage(String val, {double multiplier = 1.0}) {
if (isPercentage(val)) {
return parsePercentage(val, multiplier: multiplier);
} else {
return parseDouble(val)!;
}
}
/// Parses values in the form of '100%'.
double parsePercentage(String val, {double multiplier = 1.0}) {
return parseDouble(val.substring(0, val.length - 1))! / 100 * multiplier;
}
/// Whether a string should be treated as a percentage (i.e. if it ends with a `'%'`).
bool isPercentage(String val) => val.endsWith('%');