blob: 347b299e8842b272a6883f95580d6aefb4cf4de3 [file] [log] [blame]
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' show Picture;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show AssetBundle;
import 'package:flutter/widgets.dart';
import 'package:xml/xml.dart' hide parse;
import 'package:xml/xml.dart' as xml show parse;
import 'src/picture_provider.dart';
import 'src/picture_stream.dart';
import 'src/render_picture.dart';
import 'src/svg/xml_parsers.dart';
import 'src/svg_parser.dart';
import 'src/vector_drawable.dart';
/// Instance for [Svg]'s utility methods, which can produce a [DrawableRoot]
/// or [PictureInfo] from [String] or [Uint8List].
final Svg svg = new Svg._();
/// A utility class for decoding SVG data to a [DrawableRoot] or a [PictureInfo].
///
/// These methods are used by [SvgPicture], but can also be directly used e.g.
/// to create a [DrawableRoot] you manipulate or render to your own [Canvas].
/// Access to this class is provided by the exported [svg] member.
class Svg {
Svg._();
/// Produces a [PictureInfo] from a [Uint8List] of SVG byte data (assumes UTF8 encoding).
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// The `colorFilter` property will be applied to any [Paint] objects used during drawing.
///
/// The [key] will be used for debugging purposes.
FutureOr<PictureInfo> svgPictureDecoder(
Uint8List raw,
bool allowDrawingOutsideOfViewBox,
ColorFilter colorFilter,
String key) async {
final DrawableRoot svgRoot = await fromSvgBytes(raw, key);
final Picture pic = svgRoot.toPicture(
clipToViewBox: allowDrawingOutsideOfViewBox == true ? false : true,
colorFilter: colorFilter);
return new PictureInfo(picture: pic, viewBox: svgRoot.viewport.rect);
}
/// Produces a [PictureInfo] from a [String] of SVG data.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// The `colorFilter` property will be applied to any [Paint] objects used during drawing.
///
/// The [key] will be used for debugging purposes.
FutureOr<PictureInfo> svgPictureStringDecoder(String raw,
bool allowDrawingOutsideOfViewBox, ColorFilter colorFilter, String key) {
final DrawableRoot svg = fromSvgString(raw, key);
return new PictureInfo(
picture: svg.toPicture(
clipToViewBox: allowDrawingOutsideOfViewBox == true ? false : true,
colorFilter: colorFilter),
viewBox: svg.viewport.rect);
}
/// Produces a [Drawableroot] from a [Uint8List] of SVG byte data (assumes UTF8 encoding).
///
/// The [key] will be used for debugging purposes.
FutureOr<DrawableRoot> fromSvgBytes(Uint8List raw, String key) async {
// TODO - do utf decoding in another thread?
// Might just have to live with potentially slow(ish) decoding, this is causing errors.
// See: https://github.com/dart-lang/sdk/issues/31954
// See: https://github.com/flutter/flutter/blob/bf3bd7667f07709d0b817ebfcb6972782cfef637/packages/flutter/lib/src/services/asset_bundle.dart#L66
// if (raw.lengthInBytes < 20 * 1024) {
return fromSvgString(utf8.decode(raw), key);
// } else {
// final String str =
// await compute(_utf8Decode, raw, debugLabel: 'UTF8 decode for SVG');
// return fromSvgString(str);
// }
}
// String _utf8Decode(Uint8List data) {
// return utf8.decode(data);
// }
/// Creates a [DrawableRoot] from a string of SVG data.
///
/// The `key` is used for debugging purposes.
DrawableRoot fromSvgString(String rawSvg, String key) {
final XmlElement svg = xml.parse(rawSvg).rootElement;
final DrawableViewport viewBox = parseViewBox(svg);
//final Map<String, PaintServer> paintServers = <String, PaintServer>{};
final DrawableDefinitionServer definitions = new DrawableDefinitionServer();
final DrawableStyle style =
parseStyle(svg, definitions, viewBox.rect, null);
final List<Drawable> children = svg.children
.where((XmlNode child) => child is XmlElement)
.map(
(XmlNode child) => parseSvgElement(
child,
definitions,
viewBox.rect,
style,
key,
),
)
.toList();
return new DrawableRoot(
viewBox,
children,
definitions,
parseStyle(svg, definitions, viewBox.rect, null),
);
}
}
/// A widget that will parse SVG data into a [Picture] using a [PictureProvider].
///
/// The picture will be cached using the [PictureCache], incorporating any color
/// filtering used into the key (meaning the same SVG with two different `color`
/// arguments applied would be two cache entries).
class SvgPicture extends StatefulWidget {
/// Instantiates a widget that renders an SVG picture using the `pictureProvider`.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time, e.g. for a network picture.
const SvgPicture(this.pictureProvider,
{Key key,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder})
: super(key: key);
/// Instantiates a widget that renders an SVG picture from an [AssetBundle].
///
/// The key will be derived from the `assetName`, `package`, and `bundle`
/// arguments. The `package` argument must be non-null when displaying an SVG
/// from a package and null otherwise. See the `Assets in packages` section for
/// details.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time.
///
/// The `color` and `colorBlendMode` arguments, if specified, will be used to set a
/// [ColorFilter] on any [Paint]s created for this drawing.
///
/// ## Assets in packages
///
/// To create the widget with an asset from a package, the [package] argument
/// must be provided. For instance, suppose a package called `my_icons` has
/// `icons/heart.svg` .
///
/// Then to display the image, use:
///
/// ```dart
/// new SvgPicture.asset('icons/heart.svg', package: 'my_icons')
/// ```
///
/// Assets used by the package itself should also be displayed using the
/// [package] argument as above.
///
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its `pubspec.yaml`.
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its `pubspec.yaml`. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.svg
/// lib/backgrounds/background2.svg
/// lib/backgrounds/background3.svg
///```
///
/// To include, say the first image, the `pubspec.yaml` of the app should
/// specify it in the assets section:
///
/// ```yaml
/// assets:
/// - packages/fancy_backgrounds/backgrounds/background1.svg
/// ```
///
/// The `lib/` is implied, so it should not be included in the asset path.
///
///
/// See also:
///
/// * [AssetPicture], which is used to implement the behavior when the scale is
/// omitted.
/// * [ExactAssetPicture], which is used to implement the behavior when the
/// scale is present.
/// * <https://flutter.io/assets-and-images/>, an introduction to assets in
/// Flutter.
SvgPicture.asset(String assetName,
{Key key,
this.matchTextDirection = false,
AssetBundle bundle,
String package,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
Color color,
BlendMode colorBlendMode = BlendMode.srcIn})
: pictureProvider = new ExactAssetPicture(
allowDrawingOutsideViewBox == true
? svgByteDecoderOutsideViewBox
: svgByteDecoder,
assetName,
bundle: bundle,
package: package,
colorFilter: _getColorFilter(color, colorBlendMode)),
super(key: key);
/// Creates a widget that displays a [PictureStream] obtained from the network.
///
/// The [url] argument must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time, such as high latency scenarios.
///
/// The `color` and `colorBlendMode` arguments, if specified, will be used to set a
/// [ColorFilter] on any [Paint]s created for this drawing.
///
/// All network images are cached regardless of HTTP headers.
///
/// An optional `headers` argument can be used to send custom HTTP headers
/// with the image request.
SvgPicture.network(String url,
{Key key,
Map<String, String> headers,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
Color color,
BlendMode colorBlendMode = BlendMode.srcIn})
: pictureProvider = new NetworkPicture(
allowDrawingOutsideViewBox == true
? svgByteDecoderOutsideViewBox
: svgByteDecoder,
url,
headers: headers,
colorFilter: _getColorFilter(color, colorBlendMode)),
super(key: key);
/// Creates a widget that displays a [PictureStream] obtained from a [File].
///
/// The [file] argument must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time.
///
/// The `color` and `colorBlendMode` arguments, if specified, will be used to set a
/// [ColorFilter] on any [Paint]s created for this drawing.
///
/// On Android, this may require the
/// `android.permission.READ_EXTERNAL_STORAGE` permission.
SvgPicture.file(File file,
{Key key,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
Color color,
BlendMode colorBlendMode = BlendMode.srcIn})
: pictureProvider = new FilePicture(
allowDrawingOutsideViewBox == true
? svgByteDecoderOutsideViewBox
: svgByteDecoder,
file,
colorFilter: _getColorFilter(color, colorBlendMode)),
super(key: key);
/// Creates a widget that displays a [PictureStream] obtained from a [Uint8List].
///
/// The [bytes] argument must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time.
///
/// The `color` and `colorBlendMode` arguments, if specified, will be used to set a
/// [ColorFilter] on any [Paint]s created for this drawing.
SvgPicture.memory(Uint8List bytes,
{Key key,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
Color color,
BlendMode colorBlendMode = BlendMode.srcIn})
: pictureProvider = new MemoryPicture(
allowDrawingOutsideViewBox == true
? svgByteDecoderOutsideViewBox
: svgByteDecoder,
bytes,
colorFilter: _getColorFilter(color, colorBlendMode)),
super(key: key);
/// Creates a widget that displays a [PictureStream] obtained from a [String].
///
/// The [bytes] argument must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// If `matchTextDirection` is set to true, the picture will be flipped
/// horizontally in [TextDirection.rtl] contexts.
///
/// The `allowDrawingOutsideOfViewBox` parameter should be used with caution -
/// if set to true, it will not clip the canvas used internally to the view box,
/// meaning the picture may draw beyond the intended area and lead to undefined
/// behavior or additional memory overhead.
///
/// A custom `placeholderBuilder` can be specified for cases where decoding or
/// acquiring data may take a noticeably long time.
///
/// The `color` and `colorBlendMode` arguments, if specified, will be used to set a
/// [ColorFilter] on any [Paint]s created for this drawing.
SvgPicture.string(String bytes,
{Key key,
this.width,
this.height,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.matchTextDirection = false,
this.allowDrawingOutsideViewBox = false,
this.placeholderBuilder,
Color color,
BlendMode colorBlendMode = BlendMode.srcIn})
: pictureProvider = new StringPicture(
allowDrawingOutsideViewBox == true
? svgStringDecoderOutsideViewBox
: svgStringDecoder,
bytes,
colorFilter: _getColorFilter(color, colorBlendMode)),
super(key: key);
/// The default placeholder for a SVG that may take time to parse or
/// retrieve, e.g. from a network location.
static WidgetBuilder defaultPlaceholderBuilder =
(BuildContext ctx) => const LimitedBox();
static ColorFilter _getColorFilter(Color color, BlendMode colorBlendMode) =>
color == null
? null
: new ColorFilter.mode(color, colorBlendMode ?? BlendMode.srcIn);
/// A [PictureInfoDecoder] for [Uint8List]s that will clip to the viewBox.
static final PictureInfoDecoder<Uint8List> svgByteDecoder =
(Uint8List bytes, ColorFilter colorFilter, String key) =>
svg.svgPictureDecoder(bytes, false, colorFilter, key);
/// A [PictureInfoDecoder] for strings that will clip to the viewBox.
static final PictureInfoDecoder<String> svgStringDecoder =
(String data, ColorFilter colorFilter, String key) =>
svg.svgPictureStringDecoder(data, false, colorFilter, key);
/// A [PictureInfoDecoder] for [Uint8List]s that will not clip to the viewBox.
static final PictureInfoDecoder<Uint8List> svgByteDecoderOutsideViewBox =
(Uint8List bytes, ColorFilter colorFilter, String key) =>
svg.svgPictureDecoder(bytes, true, colorFilter, key);
/// A [PictureInfoDecoder] for [String]s that will not clip to the viewBox.
static final PictureInfoDecoder<String> svgStringDecoderOutsideViewBox =
(String data, ColorFilter colorFilter, String key) =>
svg.svgPictureStringDecoder(data, true, colorFilter, key);
/// If specified, the width to use for the SVG. If unspecified, the SVG
/// will take the width of its parent.
final double width;
/// If specified, the height to use for the SVG. If unspecified, the SVG
/// will take the height of its parent.
final double height;
/// How to inscribe the picture into the space allocated during layout.
/// The default is [BoxFit.contain].
final BoxFit fit;
/// How to align the picture within its parent widget.
///
/// The alignment aligns the given position in the picture to the given position
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
/// -1.0) aligns the image to the top-left corner of its layout bounds, while a
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
/// picture with the bottom right corner of its layout bounds. Similarly, an
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
/// middle of the bottom edge of its layout bounds.
///
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
/// [AlignmentDirectional]), then a [TextDirection] must be available
/// when the picture is painted.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final Alignment alignment;
/// The [PictureProvider] used to resolve the SVG.
final PictureProvider pictureProvider;
/// The placeholder to use while fetching, decoding, and parsing the SVG data.
final WidgetBuilder placeholderBuilder;
/// If true, will horizontally flip the picture in [TextDirection.rtl] contexts.
final bool matchTextDirection;
/// If true, will allow the SVG to be drawn outside of the clip boundary of its
/// viewBox.
final bool allowDrawingOutsideViewBox;
@override
State<SvgPicture> createState() => new _SvgPictureState();
}
class _SvgPictureState extends State<SvgPicture> {
PictureInfo _picture;
PictureStream _pictureStream;
bool _isListeningToStream = false;
@override
void didChangeDependencies() {
_resolveImage();
if (TickerMode.of(context)) {
_listenToStream();
} else {
_stopListeningToStream();
}
super.didChangeDependencies();
}
@override
void didUpdateWidget(SvgPicture oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.pictureProvider != oldWidget.pictureProvider) {
_resolveImage();
}
}
@override
void reassemble() {
_resolveImage(); // in case the image cache was flushed
super.reassemble();
}
void _resolveImage() {
final PictureStream newStream = widget.pictureProvider
.resolve(createLocalPictureConfiguration(context));
assert(newStream != null);
_updateSourceStream(newStream);
}
void _handleImageChanged(PictureInfo imageInfo, bool synchronousCall) {
setState(() {
_picture = imageInfo;
});
}
// Update _pictureStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
void _updateSourceStream(PictureStream newStream) {
if (_pictureStream?.key == newStream?.key) {
return;
}
if (_isListeningToStream)
_pictureStream.removeListener(_handleImageChanged);
_pictureStream = newStream;
if (_isListeningToStream) {
_pictureStream.addListener(_handleImageChanged);
}
}
void _listenToStream() {
if (_isListeningToStream) {
return;
}
_pictureStream.addListener(_handleImageChanged);
_isListeningToStream = true;
}
void _stopListeningToStream() {
if (!_isListeningToStream) {
return;
}
_pictureStream.removeListener(_handleImageChanged);
_isListeningToStream = false;
}
@override
void dispose() {
assert(_pictureStream != null);
_stopListeningToStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_picture != null) {
Widget picture = new RawPicture(
_picture,
matchTextDirection: widget.matchTextDirection,
allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox,
);
picture =
new SizedBox.fromSize(size: _picture.viewBox.size, child: picture);
picture = new FittedBox(
fit: widget.fit, alignment: widget.alignment, child: picture);
double width = widget.width;
double height = widget.height;
if (width == null && height == null) {
width = _picture.viewBox.width;
height = _picture.viewBox.height;
} else if (height != null) {
width = height / _picture.viewBox.height * _picture.viewBox.width;
} else if (width != null) {
height = width / _picture.viewBox.width * _picture.viewBox.height;
}
return new SizedBox(width: width, height: height, child: picture);
}
return widget.placeholderBuilder == null
? _getDefaultPlaceholder(context, widget.width, widget.height)
: widget.placeholderBuilder(context);
}
Widget _getDefaultPlaceholder(
BuildContext context, double width, double height) {
if (width != null || height != null) {
return new SizedBox(width: width, height: height);
}
return SvgPicture.defaultPlaceholderBuilder(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description
.add(new DiagnosticsProperty<PictureStream>('stream', _pictureStream));
}
}