preface

In the previous article “[Flutter Drawing] SVG Files and Drawing (part 1)”, we introduced H, V and L SVG instructions, and analyzed them through regular expressions to generate the Path of Flutter drawing. Two instructions, C and Q, representing cubic and quadratic Bessel curves, respectively, will be introduced in this paper. Once these two instructions are parsed, the SVG icon for the nuggets should appear perfectly:


Why parse SVG?

For those who don’t understand, why would you parse SVG into a Path in a Flutter? That is to say, you do not yet understand the place of the Path object in drawing. For example, Path allows fine control over drawing, for example, drawing wireframes:


Once you have a path, it’s a matter of drawing skills, such as giving gradients:

The casing outside the gradient Fill in the gradient

For example, add a color to a drawing by shader:

Or adding color filters through maskFilter, which is essentially a drawing skill and has nothing to do with SVG itself. It is the Path object that creates the intersection of these two unrelated objects. The drawing skills of Flutter are described in detail in the Guide to Flutter Drawing.

MaskFilter.blur(BlurStyle.inner, 10)
Copy the code

MaskFilter.blur(BlurStyle.solid, 20)
Copy the code

For example, with paths, you can animate the following path drawing using computeMetrics. I’ve been asked how to do this before, but it’s essentially a path operation. But you can’t just give a word to a Flutter and get a path. Just use the software to design an SVG path corresponding to the text, just like the following rare earth nuggets:

SVG itself is a static file that records information. If it can be resolved into a Path object in a Flutter, there is more room for application. After all, once you can do logic in code, the possibilities are endless. This is one of the reasons it is necessary to parse SVG; There are two additional benefits: a deeper understanding of SVG files and the ability to practice regular parsing.


Encapsulation of SVG parsing

In the previous article, parsing SVG files directly in the sketchpad class is not very friendly either for reuse or maintenance extensions. We can encapsulate the parsing logic into a single class. Below, define SVGPathResult as the result of parsing each path. This includes the path string path, fillColor, strokeColor, and strokeWidth. Define a parser method in SVGParser that parses the SRC string and generates a list of SvgPathResults:

class SVGPathResult {
  final String? path;
  final String? fillColor;
  final String? strokeColor;
  final String? strokeWidth;

  SVGPathResult({
    required this.path,
    this.fillColor,
    this.strokeColor,
    this.strokeWidth,
  });
}

class SVGParser {
  
  List<SVGPathResult? > parser(String src) {
    List<SVGPathResult? > result = [];// TODO parses SVG files
    returnresult; }}Copy the code

1. Parse SVG files

The SVG file itself is a subset of XML, so the overall structure can be parsed by an XML parser, which introduces XML packages:

---->[pubspec.yaml]----
xml: ^ 5.3.1
Copy the code

Parsing the nodes is also very simple. The XmlDocument object is an actual XML document tree. The findAllElements method can query a subset of certain types of tags. You can use this method to obtain all path nodes, traverse the nodes, and use the getAttribute method to obtain the required attribute information. This allows you to extract the desired data from the SVG file.

List<SVGPathResult? > parser(String src) {
  List<SVGPathResult? > result = [];final XmlDocument document = XmlDocument.parse(src);
  XmlElement? root = document.getElement('svg');
  if (root == null) return result;
  List<XmlElement> pathNodes = root.findAllElements('path').toList();
  pathNodes.forEach((pathNode) {
    String? pathStr = pathNode.getAttribute('d');
    String? fillColor = pathNode.getAttribute('fill');
    String? strokeColor = pathNode.getAttribute('stroke');
    String? strokeWidth = pathNode.getAttribute('stroke-width');
    result.add(SVGPathResult(
      path: pathStr,
      fillColor: fillColor,
      strokeColor: strokeColor,
      strokeWidth: strokeWidth,
    ));
  });
  return result;
}
Copy the code

2. SVG path parsing

As you can see, parsing SVG files through XML parsing does not require much effort. Path is a string. The next problem is to parse the string into a path. Here I want this logic to be isolated, so I define a class for SvgUtils that does the job using the static method convertFromSvgPath. Analytical logic has also been introduced in the last article. In this paper, two instructions C and Q will be extended, and only the logic in this method needs to be modified:


To parse C and Q, you must first understand what they are used for. As shown below, the number of numbers after C is a multiple of 6, which represents the cubic Bezier curve, namely, the coordinates of control point 1, control point 2 and endpoint. The number after Q is a multiple of 4, representing the quadratic Bessel curve, that is, the coordinates of the control point and the endpoint.

We know that the cubicTo method in Flutter forms a cubic Bezier curve path with exactly six inputs, which are essentially parsed numbers and filled in.

if (op.startsWith("C")) {
  List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
  for (int i = 0; i < pos.length; i += 6) {
    double p0x = num.parse(pos[i]).toDouble();
    double p0y = num.parse(pos[i + 1]).toDouble();
    double p1x = num.parse(pos[i + 2]).toDouble();
    double p1y = num.parse(pos[i + 3]).toDouble();
    double p2x = num.parse(pos[i + 4]).toDouble();
    double p2y = num.parse(pos[i + 5]).toDouble(); path.cubicTo(p0x, p0y, p1x, p1y, p2x, p2y); lastX = p2x; lastY = p2y; }}Copy the code

Similarly, the quadratic bezier curve path is formed by the quadraticBezierTo method in Flutter, in which there are four input parameters, and the number is also resolved as the input parameter. This encapsulates the parsing logic in PathConvert#convertFromSvgPath, where it can be modified when additional instructions need to be extended. Parsing SVG files is handled by the SVGParser class, so it does its job.

if (op.startsWith("Q")) {
  List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
  for (int i = 0; i < pos.length; i += 4) {
    double p0x = num.parse(pos[i]).toDouble();
    double p0y = num.parse(pos[i + 1]).toDouble();
    double p1x = num.parse(pos[i + 2]).toDouble();
    double p1y = num.parse(pos[i + 3]).toDouble(); path.quadraticBezierTo(p0x, p0y, p1x, p1y); lastX = p1x; lastY = p1y; }}Copy the code

3. Brush Settings

SVG has a fill attribute under its path node for the fill and storke for the line. These are the properties of the Paint in Paint, so you need to set the brush according to these properties:

Here, we extend the SVGPathResult class with Extension to give the setPaint method. Sets properties for the brush passed in according to its own properties.

extension SetPaintBySVGPath on SVGPathResult{
  void setPaint(Paint paint){
    if (this.strokeColor ! =null) { paint.. style = PaintingStyle.stroke; Color resultColor = Color(int.parse(this.strokeColor! .substring(1), radix: 16) + 0xFF000000); paint.. color = resultColor; }if (this.strokeWidth ! =null) { paint.. strokeWidth =num.parse(this.strokeWidth!) .toDouble(); }if (this.fillColor ! =null) { paint.. style = PaintingStyle.fill; Color resultColor = Color(int.parse(this.fillColor! .substring(1), radix: 16) + 0xFF000000); paint.. color = resultColor; }}}Copy the code

One might ask, why not just write this method in SVGPathResult instead of extending it? The purpose here is to make the SVGPathResult class pure, only to collect and parse the path information function, based on its function can be extended by the user. Paint itself is a class in Flutter that needs to be run on the device to debug, which is not convenient. Without Paint, SVGParser can be made to exist without a Flutter. The classes used are the Classes of the DART language itself and run without a Flutter.


3. Rendering of analytic results in Flutter

After the above parsing and processing of Path and Paint, the rest of the drawing is pretty simple. The following code, after parsing, traverses the SVGPathResult list, generates the path, and draws it. Extra_02_svg /02

---->[paint]----
List<SVGPathResult? > parserResults = svgParser.parser(src); parserResults.forEach((SVGPathResult? result) {if (result == null) return;
  if(result.path ! =null) {
    Path path = SvgUtils.convertFromSvgPath(result.path!);
    result.setPaint(mainPaint);
    canvas.drawPath(path, mainPaint);
  }
});
Copy the code

The effect manipulation of the display is essentially set by reading the maskFilter and shader of the brush. For example, the following is an example of using a shader to apply a color to an image: extra_02_SVG /03

Matrix4 matrix4 = Matrix4.diagonal3Values(0.1.0.1.1)
    .multiplied(Matrix4.translationValues(70.10.0));

mainPaint.shader = ImageShader(
  img,
  TileMode.repeated,
  TileMode.repeated,
  matrix4.storage,
);
Copy the code

The extra_02_SVG /05 is an animation controller with computeMetrics to measure the path.

parserResults.forEach((SVGPathResult? result) {
  if (result == null) return;
  if(result.path ! =null) { Path path = SvgUtils.convertFromSvgPath(result.path!) ; result.setPaint(mainPaint); PathMetrics pms = path.computeMetrics(); mainPaint.style = PaintingStyle.stroke; pms.forEach((pm) { canvas.drawPath(pm.extractPath(0, pm.length * progress.value), mainPaint); }); }});Copy the code

Nuggets SVG uses only these commands, which may seem perfect, but SVG has more than that. There are other instructions that need to be parsed, such as A, Q, T, and so on, as well as lowercase letters relative to uppercase letters that represent relative paths, which require an extension of the parsing logic. That’s it. See you next time. Thanks for watching