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:

And the developer has left these unanswered.

I am seriously disappointed with their lack of proper migration guide.

Let's start from scratch.

loadPicture method - VectorGraphicUtilities class - flutter_svg library - Dart API
API docs for the loadPicture method from the VectorGraphicUtilities class, for the Dart programming language.

loadPicture takes a loader. What loaders are available?

Future<PictureInfo> loadPicture(
  BytesLoader loader,
  BuildContext? context, {
  bool clipViewbox = true,
  VectorGraphicsErrorListener? onError,
}) async {
Loaders

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:

Start using VG by dnfield · Pull Request #782 · dnfield/flutter_svg
Fixes #736Fixes #289Fixes #102

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:

flutter_svg/lib/svg.dart at 71916fb5fcc6cf929636bbb78d1644d77bcaef57 · dnfield/flutter_svg
SVG parsing, rendering, and widget library for Flutter - dnfield/flutter_svg
  @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 the package:vector_graphics_compiler and encoded via a tightly coupled version of package: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.
vector_graphics_codec | Dart package
An encoding library for `package:vector_graphics`
VectorGraphicsCodec class - vector_graphics_codec library - Dart API
API docs for the VectorGraphicsCodec class from the vector_graphics_codec library, for the Dart programming language.

VectorGraphicsCodec.decode seems promising!

decode method - VectorGraphicsCodec class - vector_graphics_codec library - Dart API
API docs for the decode method from the VectorGraphicsCodec class, for the Dart programming language.
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 class - vector_graphics_codec library - Dart API
API docs for the VectorGraphicsCodecListener class from the vector_graphics_codec library, for the Dart programming language.

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.