Upgrading to flutter_svg 2.0.0
Because of https://www.wafrat.com/updating-major-versions-in-flutter/, I found myself upgrading my project, svg_drawing_animation (https://pub.dev/packages/svg_drawing_animation), to flutter_svg 2.0.0. And it was much harder than I thought. How can a developer so smart make it so hard for others?
I upgraded my major versions and flutter_svg got upgraded to flutter_svg: ^2.0.10+1. Then my project broke:
Sure that's to be expected. After all it's a upgrade to a major version, the API has changed. Let's take a look at the changelogs at https://pub.dev/packages/flutter_svg/changelog#200:
What is this? Zero migration guide? No mention of DrawableRoot disappearing, nor the parsing being changed.
Looking at the readme, the parsing seems to have changed:
SvgPicture.asset
and SvgPicture.network
have appeared, which means I don't have to implement it myself anymore, but they return a Widget, while the previous SvgParser returned a DrawableRoot.
The lower-level parsing seems to have been replaced by this:
import 'package:flutter_svg/flutter_svg.dart';
final String rawSvg = '''<svg ...>...</svg>''';
final PictureInfo pictureInfo = await vg.loadPicture(SvgStringLoader(rawSvg), null);
// You can draw the picture to a canvas:
canvas.drawPicture(pictureInfo.picture);
This is the snippet from the README, but I have no idea where that canvas or the vg comes from.
Looking at the GitHub issues for the project, other people seem to struggle with migration as well:
- https://github.com/dnfield/flutter_svg/issues/963: Migration to 2.x.x
- https://github.com/dnfield/flutter_svg/issues/1042: Undefined class 'DrawableRoot
And the developer has left these unanswered.
I am seriously disappointed with their lack of proper migration guide.
Let's start from scratch.
loadPicture takes a loader. What loaders are available?
Future<PictureInfo> loadPicture(
BytesLoader loader,
BuildContext? context, {
bool clipViewbox = true,
VectorGraphicsErrorListener? onError,
}) async {
Looks like there are a few. That's nice, perhaps I can use those loaders directly instead of implementing my own. I can do that later. For now, let's try to make my project compile again.
What I can infer from this piece of code is that DrawableRoot might have been replaced by PictureInfo or one of its derivatives. Let's try this:
Before: SvgParser().parse(str);
After: vg.loadPicture(SvgStringLoader(str), null);
It turns out vg is exported here:
So what is the equivalent of a DrawableRoot? A DrawableRoot
had a draw
method and a viewport
property. A PictureInfo has a size and a picture. A Viewport contains x, y, width and height, so it can't be mapped exactly to a size.
When I look for the draw method, I get nothing. How am I to parse the SVG, then tweak the drawing?
So I tried a few things... But in the end, drawPicture(picture) calls drawPicture in the Canvas and does never call drawPath like before. Let's see what happened to DrawableRoot. It looks like this PR deletes it:
I can isolate it into this commit: https://github.com/dnfield/flutter_svg/pull/782/commits/71916fb5fcc6cf929636bbb78d1644d77bcaef57.
I saw this snippet of code at https://github.com/dnfield/flutter_svg/pull/782/commits/71916fb5fcc6cf929636bbb78d1644d77bcaef57#diff-827a17fbb3f1a8e67d08f7955f8127fa8dfa54ae35277e33c9d2797eec2ab159L83-L102, which makes me think that I cannot use PictureInfo to extract the actual vector graphics. By that point, it has already been converted into some non vector graphics.
Future<PictureInfo> svgPictureStringDecoder(
String raw,
bool allowDrawingOutsideOfViewBox,
ColorFilter? colorFilter,
String key, {
SvgTheme theme = const SvgTheme(),
}) async {
final DrawableRoot svgRoot = await fromSvgString(raw, key, theme: theme);
final Picture pic = svgRoot.toPicture(
clipToViewBox: allowDrawingOutsideOfViewBox == true ? false : true,
colorFilter: colorFilter,
size: svgRoot.viewport.viewBox,
);
return PictureInfo(
picture: pic,
viewport: svgRoot.viewport.viewBoxRect,
size: svgRoot.viewport.size,
compatibilityTester: svgRoot.compatibilityTester,
);
}
This is DrawableRoot: https://github.com/dnfield/flutter_svg/pull/782/commits/71916fb5fcc6cf929636bbb78d1644d77bcaef57#diff-3afdd06e5055974687959d64ce3411475afcd11ee5563d3cc7c793bc63e1e34bL862
DrawableRoot.draw is recursive and goes through each of the SVG elements: https://github.com/dnfield/flutter_svg/pull/782/commits/71916fb5fcc6cf929636bbb78d1644d77bcaef57#diff-3afdd06e5055974687959d64ce3411475afcd11ee5563d3cc7c793bc63e1e34bL949-L951
void draw(Canvas canvas, Rect bounds) {
...
for (Drawable child in children) {
child.draw(canvas, viewport.viewBoxRect);
}
...
}
Most of the code in svg has been deleted and replaced by some succint piece of code. https://github.com/dnfield/flutter_svg/pull/782/commits/71916fb5fcc6cf929636bbb78d1644d77bcaef57#diff-827a17fbb3f1a8e67d08f7955f8127fa8dfa54ae35277e33c9d2797eec2ab159
SVGPicture's build method used to be a bunch of code and has now been replaced by a simple wrapper:
@override
Widget build(BuildContext context) {
return VectorGraphic(
loader: bytesLoader,
width: width,
height: height,
fit: fit,
alignment: alignment,
semanticsLabel: semanticsLabel,
excludeFromSemantics: excludeFromSemantics,
colorFilter: colorFilter,
placeholderBuilder: placeholderBuilder,
);
}
The VectorGraphic class comes from https://pub.dev/packages/vector_graphics, https://pub.dev/documentation/vector_graphics/latest/vector_graphics/VectorGraphic-class.html. The doc says:
VectorGraphic: A widget that displays a VectorGraphicsCodec encoded asset.
Could this Codec be decoded by me to look at the whole tree? Searching for that class returns nothing though.
Search Results: 0 results for "codec"
The Vector Graphics package says:
This package is intended for use with output from thepackage:vector_graphics_compiler
and encoded via a tightly coupled version ofpackage:vector_graphics_codec
.
The vector_graphics_codec page says:
This package intentionally creates a tight coupling between package:vector_graphics_compiler and package:vector_graphics. Its format has no stability guarnatees from version to version.
This codec is not meant to have any utility outside of its usage in vector_graphics or the compiler.
VectorGraphicsCodec.decode seems promising!
Without a provided VectorGraphicsCodecListener, this method will only validate the basic structure of an object. decoders that wish to construct a dart:ui Picture object should implement VectorGraphicsCodecListener.
VectorGraphicsCodecListener has all the methods I was looking for: onDrawPath...
Let's try something like this:
final codec = VectorGraphicsCodec();
final byteData = await SvgStringLoader(svgString).loadBytes();
codec.decode(byteData, listener);
... Mmh, looks like it needs a context:
Future<ByteData> loadBytes(BuildContext? context) {
...
}
After massaging my code, and using the following listener...
import 'dart:typed_data';
import 'package:vector_graphics_codec/vector_graphics_codec.dart';
class MeasurePathLengthCodecListener implements VectorGraphicsCodecListener {
@override
void onClipPath(int pathId) {
// TODO: implement onClipPath
}
@override
void onDrawImage(int imageId, double x, double y, double width, double height,
Float64List? transform) {
// TODO: implement onDrawImage
}
@override
void onDrawPath(int pathId, int? paintId, int? patternId) {
// TODO: implement onDrawPath
print('$pathId $paintId $patternId');
}
@override
void onDrawText(int textId, int? fillId, int? strokeId, int? patternId) {
// TODO: implement onDrawText
}
@override
void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) {
// TODO: implement onDrawVertices
}
@override
void onImage(int imageId, int format, Uint8List data,
{VectorGraphicsErrorListener? onError}) {
// TODO: implement onImage
}
@override
void onLinearGradient(double fromX, double fromY, double toX, double toY,
Int32List colors, Float32List? offsets, int tileMode, int id) {
// TODO: implement onLinearGradient
}
@override
void onMask() {
// TODO: implement onMask
}
@override
void onPaintObject(
{required int color,
required int? strokeCap,
required int? strokeJoin,
required int blendMode,
required double? strokeMiterLimit,
required double? strokeWidth,
required int paintStyle,
required int id,
required int? shaderId}) {
// TODO: implement onPaintObject
}
@override
void onPathClose() {
// TODO: implement onPathClose
}
@override
void onPathCubicTo(
double x1, double y1, double x2, double y2, double x3, double y3) {
// TODO: implement onPathCubicTo
}
@override
void onPathFinished() {
// TODO: implement onPathFinished
}
@override
void onPathLineTo(double x, double y) {
// TODO: implement onPathLineTo
}
@override
void onPathMoveTo(double x, double y) {
// TODO: implement onPathMoveTo
}
@override
void onPathStart(int id, int fillType) {
// TODO: implement onPathStart
}
@override
void onPatternStart(int patternId, double x, double y, double width,
double height, Float64List transform) {
// TODO: implement onPatternStart
}
@override
void onRadialGradient(
double centerX,
double centerY,
double radius,
double? focalX,
double? focalY,
Int32List colors,
Float32List? offsets,
Float64List? transform,
int tileMode,
int id) {
// TODO: implement onRadialGradient
}
@override
void onRestoreLayer() {
// TODO: implement onRestoreLayer
}
@override
void onSaveLayer(int paintId) {
// TODO: implement onSaveLayer
}
@override
void onSize(double width, double height) {
print('size $width $height');
}
@override
void onTextConfig(
String text,
String? fontFamily,
double xAnchorMultiplier,
int fontWeight,
double fontSize,
int decoration,
int decorationStyle,
int decorationColor,
int id) {
// TODO: implement onTextConfig
}
@override
void onTextPosition(int textPositionId, double? x, double? y, double? dx,
double? dy, bool reset, Float64List? transform) {
// TODO: implement onTextPosition
}
@override
void onUpdateTextPosition(int textPositionId) {
// TODO: implement onUpdateTextPosition
}
}
I get this output:
size 109.0 109.0
0 0 null
1 0 null
2 0 null
3 0 null
4 0 null
5 0 null
6 0 null
7 0 null
8 0 null
9 0 null
10 0 null
Not bad at all!! How do I get the actual path from the path id?
By implementing a few more methods:
@override
void onPathFinished() {
print('on path finished');
}
@override
void onPathLineTo(double x, double y) {
print('on path line to $x $y');
}
@override
void onPathMoveTo(double x, double y) {
print('on path move to $x $y');
}
@override
void onPathStart(int id, int fillType) {
print('on path start $id $fillType');
}
I get more output
size 109.0 109.0
on path start 0 0
on path move to 12.050000190734863 19.299999237060547
on path finished
on path start 1 0
on path move to 19.3700008392334 21.56999969482422
on path finished
on path start 2 0
on path move to 21.209999084472656 36.90999984741211
on path finished
on path start 3 0
on path move to 20.959999084472656 52.099998474121094
on path finished
on path start 4 0
on path move to 12.25 74.93000030517578
on path finished
on path start 5 0
on path move to 36.0099983215332 20.5
on path finished
on path start 6 0
on path move to 64.88999938964844 11.75
on path finished
on path start 7 0
on path move to 65.66999816894531 14.539999961853027
on path finished
on path start 8 0
on path move to 54.53333282470703 45.64583206176758
on path finished
on path start 9 0
on path move to 49.12916564941406 57.32500076293945
on path finished
on path start 10 0
on path move to 63.80416488647461 57.952083587646484
on path finished
0 0 null
1 0 null
2 0 null
3 0 null
4 0 null
5 0 null
6 0 null
7 0 null
8 0 null
9 0 null
10 0 null
What about transforms?
size 264.6000061035156 211.6999969482422
on path start 0 0
on path move to 181.4886016845703 81.83000183105469
on path finished
on path start 1 0
on path move to 222.8885955810547 122.16000366210938
on path finished
on path start 2 0
on path move to 80.60859680175781 120.2300033569336
on path line to 222.47413635253906 52.576290130615234
on path line to 194.90892028808594 142.89073181152344
on path finished
It might be that the codec has handled all transforms already and therefore we can use the path coordinates as absolute values. As you can see, the coordinates are pretty close to the size.
When I try on the following svg, which contains a scale by 2:
<svg height="10" width="10">
<g transform="matrix(2 0 0 1 0 0)">
<line x1="0" y1="0" x2="1" y2="0" />
</g>
</svg>
I get these outputs
size 10.0 10.0
on path start 0 0
on path move to 0.0 0.0
on path line to 2.0 0.0
on path finished
0 0 null
Perfect!
The next step is to figure out what type of path they are, whether they are simply polygons or bézier curves, and if so, which type. If you look at Flutter's Path class, it has the following methods: lineTo, quadraticBezierTo, cubicTo, conicTo...
The codec package mentions bezier nowhere. https://pub.dev/documentation/vector_graphics_codec/latest/search.html?q=bezier. It does contain onPathCubicTo though.
Looking at https://pub.dev/packages/vector_graphics_compiler, their PathBuilder does indeed contain only cubic. https://pub.dev/documentation/vector_graphics_compiler/latest/vector_graphics_compiler/PathBuilder-class.html
VectorGraphicsCodec does not seem to expose any Path object. It is a shame. VectorGraphicsCodec.writePath accepts a fully fleshed Path, but when we decode it, we get the individual commands. So we need to recreate a Path object in order to draw it to a Canvas.
But it's getting late. I'll continue the migration another time.