diff --git a/CODEOWNERS b/CODEOWNERS index 0785e52ba2c..a22fc4448a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -40,6 +40,9 @@ packages/shared_preferences/** @tarrinneal packages/standard_message_codec/** @jonahwilliams packages/two_dimensional_scrollables/** @Piinks packages/url_launcher/** @stuartmorgan +packages/vector_graphics/** @jonahwilliams +packages/vector_graphics_codec/** @jonahwilliams +packages/vector_graphics_compiler/** @jonahwilliams packages/video_player/** @tarrinneal packages/web_benchmarks/** @yjbanov packages/webview_flutter/** @bparrishMines diff --git a/README.md b/README.md index e256308ebee..e56b7ab0f89 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ These are the packages hosted in this repository: | [standard\_message\_codec](./packages/standard_message_codec/) | [![pub package](https://img.shields.io/pub/v/standard_message_codec.svg)](https://pub.dev/packages/standard_message_codec) | [![pub points](https://img.shields.io/pub/points/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![popularity](https://img.shields.io/pub/popularity/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20standard_message_codec?label=)](https://github.com/flutter/flutter/labels/p%3A%20standard_message_codec) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20standard_message_codec?label=)](https://github.com/flutter/packages/labels/p%3A%20standard_message_codec) | | [two\_dimensional\_scrollables](./packages/two_dimensional_scrollables/) | [![pub package](https://img.shields.io/pub/v/two_dimensional_scrollables.svg)](https://pub.dev/packages/two_dimensional_scrollables) | [![pub points](https://img.shields.io/pub/points/two_dimensional_scrollables)](https://pub.dev/packages/two_dimensional_scrollables/score) | [![popularity](https://img.shields.io/pub/popularity/two_dimensional_scrollables)](https://pub.dev/packages/two_dimensional_scrollables/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20two_dimensional_scrollables?label=)](https://github.com/flutter/flutter/labels/p%3A%20two_dimensional_scrollables) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20two_dimensional_scrollables?label=)](https://github.com/flutter/packages/labels/p%3A%20two_dimensional_scrollables) | | [url\_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) | [![pub points](https://img.shields.io/pub/points/url_launcher)](https://pub.dev/packages/url_launcher/score) | [![popularity](https://img.shields.io/pub/popularity/url_launcher)](https://pub.dev/packages/url_launcher/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20url_launcher?label=)](https://github.com/flutter/flutter/labels/p%3A%20url_launcher) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20url_launcher?label=)](https://github.com/flutter/packages/labels/p%3A%20url_launcher) | +| [vector\_graphics](./packages/vector_graphics/) | [![pub package](https://img.shields.io/pub/v/vector_graphics.svg)](https://pub.dev/packages/vector_graphics) | [![pub points](https://img.shields.io/pub/points/vector_graphics)](https://pub.dev/packages/vector_graphics/score) | [![popularity](https://img.shields.io/pub/popularity/vector_graphics)](https://pub.dev/packages/vector_graphics/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20vector_graphics?label=)](https://github.com/flutter/flutter/labels/p%3A%20vector_graphics) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20vector_graphics?label=)](https://github.com/flutter/packages/labels/p%3A%20vector_graphics) | +| [vector\_graphics\_codec](./packages/vector_graphics_codec/) | [![pub package](https://img.shields.io/pub/v/vector_graphics_codec.svg)](https://pub.dev/packages/vector_graphics_codec) | [![pub points](https://img.shields.io/pub/points/vector_graphics_codec)](https://pub.dev/packages/vector_graphics_codec/score) | [![popularity](https://img.shields.io/pub/popularity/vector_graphics_codec)](https://pub.dev/packages/vector_graphics_codec/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20vector_graphics_codec?label=)](https://github.com/flutter/flutter/labels/p%3A%20vector_graphics_codec) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20vector_graphics_codec?label=)](https://github.com/flutter/packages/labels/p%3A%20vector_graphics_codec) | +| [vector\_graphics\_compiler](./packages/vector_graphics_compiler/) | [![pub package](https://img.shields.io/pub/v/vector_graphics_compiler.svg)](https://pub.dev/packages/vector_graphics_compiler) | [![pub points](https://img.shields.io/pub/points/vector_graphics_compiler)](https://pub.dev/packages/vector_graphics_compiler/score) | [![popularity](https://img.shields.io/pub/popularity/vector_graphics_compiler)](https://pub.dev/packages/vector_graphics_compiler/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20vector_graphics_compiler?label=)](https://github.com/flutter/flutter/labels/p%3A%20vector_graphics_compiler) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20vector_graphics_compiler?label=)](https://github.com/flutter/packages/labels/p%3A%20vector_graphics_compiler) | | [video\_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) | [![pub points](https://img.shields.io/pub/points/video_player)](https://pub.dev/packages/video_player/score) | [![popularity](https://img.shields.io/pub/popularity/video_player)](https://pub.dev/packages/video_player/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20video_player?label=)](https://github.com/flutter/flutter/labels/p%3A%20video_player) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20video_player?label=)](https://github.com/flutter/packages/labels/p%3A%20video_player) | | [web\_benchmarks](./packages/web_benchmarks/) | [![pub package](https://img.shields.io/pub/v/web_benchmarks.svg)](https://pub.dev/packages/web_benchmarks) | [![pub points](https://img.shields.io/pub/points/web_benchmarks)](https://pub.dev/packages/web_benchmarks/score) | [![popularity](https://img.shields.io/pub/popularity/web_benchmarks)](https://pub.dev/packages/web_benchmarks/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20web_benchmarks?label=)](https://github.com/flutter/flutter/labels/p%3A%20web_benchmarks) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20web_benchmarks?label=)](https://github.com/flutter/packages/labels/p%3A%20web_benchmarks) | | [webview\_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) | [![pub points](https://img.shields.io/pub/points/webview_flutter)](https://pub.dev/packages/webview_flutter/score) | [![popularity](https://img.shields.io/pub/popularity/webview_flutter)](https://pub.dev/packages/webview_flutter/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20webview?label=)](https://github.com/flutter/flutter/labels/p%3A%20webview) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20webview_flutter?label=)](https://github.com/flutter/packages/labels/p%3A%20webview_flutter) | diff --git a/packages/vector_graphics/.metadata b/packages/vector_graphics/.metadata new file mode 100644 index 00000000000..6281e523b24 --- /dev/null +++ b/packages/vector_graphics/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: a7790d8e3a6c1810b8ce77cc9339d8b30ff68019 + channel: unknown + +project_type: package diff --git a/packages/vector_graphics/AUTHORS b/packages/vector_graphics/AUTHORS new file mode 100644 index 00000000000..557dff97933 --- /dev/null +++ b/packages/vector_graphics/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/vector_graphics/CHANGELOG.md b/packages/vector_graphics/CHANGELOG.md new file mode 100644 index 00000000000..598d8a28026 --- /dev/null +++ b/packages/vector_graphics/CHANGELOG.md @@ -0,0 +1,132 @@ +## 1.1.12 + +* Transfers the package source from https://github.com/dnfield/vector_graphics + to https://github.com/flutter/packages. + +## 1.1.11+1 + +* Relax package:http constraint. + +## 1.1.11 + +* Use package:http to drop dependency on dart:html. + +## 1.1.10+1 + +* Add missing save before clip. + +## 1.1.10 + +* Add missing clip before saveLayer. + +## 1.1.9+2 + +* Fix case sensitivity on scientific notation parsing. + +## 1.1.9+1 + +* Fix publication error that did not have latest source code. + +## 1.1.9 + +* Fix handling of invalid XML `@id` attributes. +* Fix handling of self-referential `` elements. +* Add `--out-dir` option to compiler. +* Tweak warning message for unhandled eleemnts. + +## 1.1.8 + +* Fix bugs in transform parsing. + +## 1.1.7 + +* Support for matching the ambient text direction. + +## 1.1.6 + +* Fix bug in text position computation when transforms are involved. + +## 1.1.5+1 + +* Remove/update some invalid assertions related to image formats. + +## 1.1.5 + +* Support for decoding control points as IEEE 754-2008 half precision + floating point values. +* Migrate the test to extend DefaultWidgetsLocalizations. +* Added an error builder property to provide a fallback widget on exceptions. + +## 1.1.4 + +* Support more image formats and malformed MIME types. +* Fix inheritence for `fill-rule`s. + +## 1.1.3 + +* Further improvements to whitespace handling for text. + +## 1.1.2 + +* Fix handling and inheritence of `none`. + +## 1.1.1 + +* Multiple text positioning bug fixes. +* Preserve stroke-opacity when specified. + +## 1.1.0 + +* Fix a number of inheritence related bugs: + * Inheritence of properties specified on the root element now work. + * Opacity inheritence is more correct now. + * Inheritence of `use` elements is more correctly handled. +* Make `currentColor` non-null on SVG theme, and fix how it is applied. +* Remove the opacity peephole optimizer, which was incorrectly applying + optimizations in a few cases. A future release may add this back. +* Add clipBehavior to the widget. +* Fix patterns when multiple patterns are specified and applied within the + graphic. + +## 1.0.1+1 + +* Fix bug in asset loading from packages. + +## 1.0.1 + +* Fix handling of fill colors on use/group elements. + +## 1.0.0+1 + +* Fix issue in pattern decoding. +* Fix issue in matrix parsing for some combinations of matrices. + +## 1.0.0 + +* Stable release. +* Use `ImageCache` for images. +* Bug fixes for image rendering. +* Better support for synchronous usage in testing. +* Make clipping the viewbox optional. + +## 0.0.3 + +* Improvements to CLI utilities. +* Dispose unused objects. +* Improved support for HTML backend. +* Less aggressive rasterization strategy for flutter_svg compatibility. + +## 0.0.2 + +* Support for drawing images + +## 0.0.1 + +* Added `VectorGraphic` which consumes encoded vector graphics assets using + a `BytesLoader`. +* Added `AssetBytesLoader` and `NetworkBytesLoader` as example loader + implementations. + +## 0.0.0 + +* Create repository diff --git a/packages/vector_graphics/LICENSE b/packages/vector_graphics/LICENSE new file mode 100644 index 00000000000..c6823b81eb8 --- /dev/null +++ b/packages/vector_graphics/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/vector_graphics/README.md b/packages/vector_graphics/README.md new file mode 100644 index 00000000000..e7370543320 --- /dev/null +++ b/packages/vector_graphics/README.md @@ -0,0 +1,14 @@ +# vector_graphics + +A vector graphics rendering runtime for Flutter. 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`. + +## Commemoration + +This package was originally authored by +[Dan Field](https://github.com/dnfield) and has been forked here +from [dnfield/vector_graphics](https://github.com/dnfield/vector_graphics). +Dan was a member of the Flutter team at Google from 2018 until his death +in 2024. Dan’s impact and contributions to Flutter were immeasurable, and we +honor his memory by continuing to publish and maintain this package. diff --git a/packages/vector_graphics/example/.gitignore b/packages/vector_graphics/example/.gitignore new file mode 100644 index 00000000000..15183f48bcb --- /dev/null +++ b/packages/vector_graphics/example/.gitignore @@ -0,0 +1,61 @@ +android/ +ios/ +macos/ +linux/ +windows/ +web/ +test/ +android/ +ios/ +macos/ +linux/ +windows/ +web/ +test/ +.metadata + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/vector_graphics/example/README.md b/packages/vector_graphics/example/README.md new file mode 100644 index 00000000000..92c5ae6a3e3 --- /dev/null +++ b/packages/vector_graphics/example/README.md @@ -0,0 +1,3 @@ +# example + +An example of using package:vector_graphics to draw vector assets \ No newline at end of file diff --git a/packages/vector_graphics/example/assets/tiger.bin b/packages/vector_graphics/example/assets/tiger.bin new file mode 100644 index 00000000000..9d8de7e545f Binary files /dev/null and b/packages/vector_graphics/example/assets/tiger.bin differ diff --git a/packages/vector_graphics/example/lib/main.dart b/packages/vector_graphics/example/lib/main.dart new file mode 100644 index 00000000000..1237510ddb5 --- /dev/null +++ b/packages/vector_graphics/example/lib/main.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:vector_graphics/vector_graphics.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + runApp(const MyApp()); +} + +/// The main example app widget. +class MyApp extends StatelessWidget { + /// Creates a new [MyApp]. + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Vector Graphics Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const Scaffold( + body: Center( + child: VectorGraphic( + loader: NetworkSvgLoader( + 'https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg', + ), + ), + ), + ), + ); + } +} + +/// A [BytesLoader] that converts a network URL into encoded SVG data. +class NetworkSvgLoader extends BytesLoader { + /// Creates a [NetworkSvgLoader] that loads an SVG from [url]. + const NetworkSvgLoader(this.url); + + /// The SVG URL. + final String url; + + @override + Future loadBytes(BuildContext? context) async { + return compute((String svgUrl) async { + final http.Response request = await http.get(Uri.parse(svgUrl)); + final TimelineTask task = TimelineTask()..start('encodeSvg'); + final Uint8List compiledBytes = encodeSvg( + xml: request.body, + debugName: svgUrl, + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + task.finish(); + // sendAndExit will make sure this isn't copied. + return compiledBytes.buffer.asByteData(); + }, url, debugLabel: 'Load Bytes'); + } + + @override + int get hashCode => url.hashCode; + + @override + bool operator ==(Object other) { + return other is NetworkSvgLoader && other.url == url; + } +} diff --git a/packages/vector_graphics/example/lib/svg_string.dart b/packages/vector_graphics/example/lib/svg_string.dart new file mode 100644 index 00000000000..6423b29e8a0 --- /dev/null +++ b/packages/vector_graphics/example/lib/svg_string.dart @@ -0,0 +1,169 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:vector_graphics/vector_graphics.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +const String _flutterLogoString = ''' + + + + + + + + + + + + + + + +'''; + +void main() { + runApp(const MyApp()); +} + +/// The main example app widget. +class MyApp extends StatefulWidget { + /// Creates a new [MyApp]. + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final TextEditingController _controller = + TextEditingController(text: _flutterLogoString); + ByteData? _data; + Timer? _debounce; + int _svgLength = 0; + int _gzSvgLength = 0; + int _vgLength = 0; + int _gzVgLength = 0; + + void _reloadSvg(String text) { + if (_debounce?.isActive ?? false) { + _debounce?.cancel(); + } + _debounce = Timer(const Duration(milliseconds: 250), () { + compute((String svg) { + final Uint8List compiledBytes = encodeSvg( + xml: svg, + debugName: '', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + return compiledBytes.buffer.asByteData(); + }, text, debugLabel: 'Load Bytes') + .then((ByteData data) { + if (!mounted) { + return; + } + setState(() { + // String is UTF-16. + _svgLength = text.length * 2; + _gzSvgLength = gzip.encode(utf8.encode(text)).length; + _vgLength = data.lengthInBytes; + _gzVgLength = gzip.encode(data.buffer.asUint8List()).length; + _data = data; + }); + }, onError: (Object error, StackTrace stack) { + debugPrint(error.toString()); + debugPrint(stack.toString()); + }); + }); + } + + @override + void initState() { + super.initState(); + _reloadSvg(_flutterLogoString); + } + + @override + void dispose() { + _debounce?.cancel(); + _debounce = null; + _data = null; + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Vector Graphics Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: Scaffold( + body: Center( + child: ListView( + children: [ + const SizedBox(height: 10), + if (_data == null) + const Placeholder() + else + VectorGraphic( + loader: RawBytesLoader( + _data!, + ), + ), + const Divider(), + Text('SVG size (compressed): $_svgLength ($_gzSvgLength). ' + 'VG size (compressed): $_vgLength ($_gzVgLength)'), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _controller, + onChanged: _reloadSvg, + scrollPhysics: const NeverScrollableScrollPhysics(), + maxLines: null, + ), + ), + ], + ), + ), + ), + ); + } +} + +/// A [BytesLoader] that passes on existing bytes. +class RawBytesLoader extends BytesLoader { + /// Creates a [RawBytesLoader] that returns [data] directly. + const RawBytesLoader(this.data); + + /// The data to return. + final ByteData data; + + @override + Future loadBytes(BuildContext? context) async { + return data; + } + + @override + int get hashCode => data.hashCode; + + @override + bool operator ==(Object other) { + return other is RawBytesLoader && other.data == data; + } +} diff --git a/packages/vector_graphics/example/pubspec.yaml b/packages/vector_graphics/example/pubspec.yaml new file mode 100644 index 00000000000..b80875fbaa4 --- /dev/null +++ b/packages/vector_graphics/example/pubspec.yaml @@ -0,0 +1,37 @@ +name: example +description: An example of the vector_graphics package +publish_to: 'none' + +environment: + sdk: ^3.4.0 + +dependencies: + flutter: + sdk: flutter + http: ">=0.13.0 <2.0.0" + vector_graphics: any + vector_graphics_compiler: any + + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + +dependency_overrides: + vector_graphics: + path: ../ + vector_graphics_codec: + path: ../../vector_graphics_codec + vector_graphics_compiler: + path: ../../vector_graphics_compiler + +platforms: + android: + ios: + linux: + macos: + web: + windows: diff --git a/packages/vector_graphics/lib/src/_debug_io.dart b/packages/vector_graphics/lib/src/_debug_io.dart new file mode 100644 index 00000000000..c2aaa6d6c8d --- /dev/null +++ b/packages/vector_graphics/lib/src/_debug_io.dart @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +/// Skip rasterization if "VECTOR_GRAPHICS_SKIP_RASTER" is "true". +bool get debugSkipRaster { + final String? skip = Platform.environment['VECTOR_GRAPHICS_SKIP_RASTER']; + return skip == 'true'; +} diff --git a/packages/vector_graphics/lib/src/_debug_web.dart b/packages/vector_graphics/lib/src/_debug_web.dart new file mode 100644 index 00000000000..2467bbea936 --- /dev/null +++ b/packages/vector_graphics/lib/src/_debug_web.dart @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Don't skip rasterization on web platform debug mode. +// TODO(jonahwilliams): determine how this will be enabled/disabled. +bool get debugSkipRaster { + return false; +} diff --git a/packages/vector_graphics/lib/src/debug.dart b/packages/vector_graphics/lib/src/debug.dart new file mode 100644 index 00000000000..a4bf2f97a3a --- /dev/null +++ b/packages/vector_graphics/lib/src/debug.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export '_debug_io.dart' if (dart.library.html) '_debug_web.dart'; diff --git a/packages/vector_graphics/lib/src/html_render_vector_graphics.dart b/packages/vector_graphics/lib/src/html_render_vector_graphics.dart new file mode 100644 index 00000000000..bb577fab9c0 --- /dev/null +++ b/packages/vector_graphics/lib/src/html_render_vector_graphics.dart @@ -0,0 +1,176 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'debug.dart'; +import 'listener.dart'; + +/// A render object which draws a vector graphic instance as a picture +/// for HTML compatibility. +class RenderWebVectorGraphic extends RenderBox { + /// Create a new [RenderWebVectorGraphic]. + RenderWebVectorGraphic( + this._pictureInfo, + this._assetKey, + this._colorFilter, + this._opacity, + ) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + } + + /// A key that uniquely identifies the [pictureInfo] used for this vg. + Object get assetKey => _assetKey; + Object _assetKey; + set assetKey(Object value) { + if (value == assetKey) { + return; + } + _assetKey = value; + // Dont call mark needs paint here since a change in just the asset key + // isn't sufficient to force a re-draw. + } + + /// The [PictureInfo] which contains the vector graphic and size to draw. + PictureInfo get pictureInfo => _pictureInfo; + PictureInfo _pictureInfo; + set pictureInfo(PictureInfo value) { + if (identical(value, _pictureInfo)) { + return; + } + _pictureInfo = value; + markNeedsPaint(); + } + + /// An optional [ColorFilter] to apply to the rasterized vector graphic. + ColorFilter? get colorFilter => _colorFilter; + ColorFilter? _colorFilter; + set colorFilter(ColorFilter? value) { + if (colorFilter == value) { + return; + } + _colorFilter = value; + markNeedsPaint(); + } + + double _opacityValue = 1.0; + + /// An opacity to draw the rasterized vector graphic with. + Animation? get opacity => _opacity; + Animation? _opacity; + set opacity(Animation? value) { + if (value == opacity) { + return; + } + _opacity?.removeListener(_updateOpacity); + _opacity = value; + _opacity?.addListener(_updateOpacity); + markNeedsPaint(); + } + + void _updateOpacity() { + if (opacity == null) { + return; + } + final double newValue = opacity!.value; + if (newValue == _opacityValue) { + return; + } + _opacityValue = newValue; + markNeedsPaint(); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool get sizedByParent => true; + + @override + bool get alwaysNeedsCompositing => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.smallest; + } + + @override + void attach(covariant PipelineOwner owner) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + super.attach(owner); + } + + @override + void detach() { + _opacity?.removeListener(_updateOpacity); + super.detach(); + } + + @override + void dispose() { + _opacity?.removeListener(_updateOpacity); + _transformLayer.layer = null; + _opacityHandle.layer = null; + _filterLayer.layer = null; + super.dispose(); + } + + final LayerHandle _transformLayer = + LayerHandle(); + final LayerHandle _opacityHandle = LayerHandle(); + final LayerHandle _filterLayer = + LayerHandle(); + final Matrix4 _transform = Matrix4.identity(); + + @override + void paint(PaintingContext context, ui.Offset offset) { + assert(size == pictureInfo.size); + if (kDebugMode && debugSkipRaster) { + context.canvas + .drawRect(offset & size, Paint()..color = const Color(0xFFFF00FF)); + return; + } + + if (_opacityValue <= 0.0) { + return; + } + + // The HTML backend cannot correctly draw saveLayer opacity or color + // filters. Nor does it support toImageSync. + _transformLayer.layer = context.pushTransform( + true, + offset, + _transform, + (PaintingContext context, Offset offset) { + _opacityHandle.layer = context.pushOpacity( + offset, + (_opacityValue * 255).round(), + (PaintingContext context, Offset offset) { + if (colorFilter != null) { + _filterLayer.layer = context.pushColorFilter( + offset, + colorFilter!, + (PaintingContext context, Offset offset) { + context.canvas.drawPicture(pictureInfo.picture); + }, + oldLayer: _filterLayer.layer, + ); + } else { + _filterLayer.layer = null; + context.canvas.drawPicture(pictureInfo.picture); + } + }, + oldLayer: _opacityHandle.layer, + ); + }, + oldLayer: _transformLayer.layer, + ); + } +} diff --git a/packages/vector_graphics/lib/src/listener.dart b/packages/vector_graphics/lib/src/listener.dart new file mode 100644 index 00000000000..f266de4fb4a --- /dev/null +++ b/packages/vector_graphics/lib/src/listener.dart @@ -0,0 +1,850 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart' + show + ImageInfo, + ImageStreamCompleter, + ImageStreamListener, + OneFrameImageStreamCompleter, + imageCache; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'loader.dart'; + +const VectorGraphicsCodec _codec = VectorGraphicsCodec(); + +/// The deocded result of a vector graphics asset. +class PictureInfo { + /// Construct a new [PictureInfo]. + PictureInfo._(this.picture, this.size); + + /// A picture generated from a vector graphics image. + final Picture picture; + + /// The target size of the picture. + /// + /// This information should be used to scale and position + /// the picture based on the available space and alignment. + final Size size; +} + +/// Internal testing only. +@visibleForTesting +Locale? get debugLastLocale => _debugLastLocale; +Locale? _debugLastLocale; + +/// Internal testing only. +@visibleForTesting +TextDirection? get debugLastTextDirection => _debugLastTextDirection; +TextDirection? _debugLastTextDirection; + +/// Internal testing only. +@visibleForTesting +Iterable> get debugGetPendingDecodeTasks => + _pendingDecodes.values.map((Completer e) => e.future); +final Map> _pendingDecodes = + >{}; + +/// Decode a vector graphics binary asset into a [Picture]. +/// +/// Throws a [StateError] if the data is invalid. +Future decodeVectorGraphics( + ByteData data, { + required Locale? locale, + required TextDirection? textDirection, + required bool clipViewbox, + required BytesLoader loader, + VectorGraphicsErrorListener? onError, +}) { + try { + // We might be in a test that's using a fake async zone. Make sure that any + // real async work gets scheduled in the root zone so that it will not get + // blocked by microtasks in the fake async zone, but do not unnecessarily + // create zones outside of tests. + bool useZone = false; + assert(() { + _debugLastTextDirection = textDirection; + _debugLastLocale = locale; + useZone = Zone.current != Zone.root && + Zone.current.scheduleMicrotask != Zone.root.scheduleMicrotask; + return true; + }()); + + @pragma('vm:prefer-inline') + Future process() { + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener( + id: loader.hashCode, + locale: locale, + textDirection: textDirection, + clipViewbox: clipViewbox, + onError: onError, + ); + DecodeResponse response = _codec.decode(data, listener); + if (response.complete) { + return SynchronousFuture(listener.toPicture()); + } + assert(() { + _pendingDecodes[loader] = Completer(); + return true; + }()); + return listener.waitForImageDecode().then((_) { + response = _codec.decode(data, listener, response: response); + assert(response.complete); + assert(() { + _pendingDecodes.remove(loader)?.complete(); + return true; + }()); + return listener.toPicture(); + }); + } + + if (!kDebugMode || !useZone) { + return process(); + } + + return Zone.current + .fork( + specification: ZoneSpecification( + scheduleMicrotask: + (Zone self, ZoneDelegate parent, Zone zone, void Function() f) { + Zone.root.scheduleMicrotask(f); + }, + createTimer: (Zone self, ZoneDelegate parent, Zone zone, + Duration duration, void Function() f) { + return Zone.root.createTimer(duration, f); + }, + createPeriodicTimer: (Zone self, ZoneDelegate parent, Zone zone, + Duration period, void Function(Timer timer) f) { + return Zone.root.createPeriodicTimer(period, f); + }, + ), + ) + .run>(process); + } catch (e, s) { + _pendingDecodes.remove(loader)?.completeError(e, s); + throw VectorGraphicsDecodeException._(loader, e); + } +} + +/// Pattern configuration to be used when creating ImageShader. +class _PatternConfig { + /// Constructs a [_PatternConfig]. + _PatternConfig(this._patternId, this._width, this._height, this._transform); + + /// This id will match any path or text element that has a non-null patternId. + /// This number will also be used to map path and text elements to the + /// correct [ImageShader]. + final int _patternId; + + /// This is the width of the pattern's viewbox in px. + /// Values must be > = 1. + final double _width; + + /// The is the height of the pattern's viewbox in px. + /// Values must be > = 1. + final double _height; + + /// This is the transform of the pattern that has been created from the children, + /// of the original [ResolvedPatternNode]. + final Float64List _transform; +} + +/// Pattern state that holds information about how to construct the pattern. +class _PatternState { + /// The canvas that the element should draw to for a given [PatternConfig]. + Canvas? canvas; + + /// The image shader created by the pattern. + ImageShader? shader; + + /// The recorder that will capture the newly created canvas. + PictureRecorder? recorder; +} + +/// Used by [FlutterVectorGraphicsListener] for testing purposes. +@visibleForTesting +abstract class PictureFactory { + /// Allows const subclasses. + const PictureFactory(); + + /// Create a picture recorder. + PictureRecorder createPictureRecorder(); + + /// Create a canvas from the recorder. + Canvas createCanvas(PictureRecorder recorder); +} + +class _DefaultPictureFactory implements PictureFactory { + const _DefaultPictureFactory(); + + @override + Canvas createCanvas(PictureRecorder recorder) => Canvas(recorder); + + @override + PictureRecorder createPictureRecorder() => PictureRecorder(); +} + +/// A listener implementation for the vector graphics codec that converts the +/// format into a [Picture]. +class FlutterVectorGraphicsListener extends VectorGraphicsCodecListener { + /// Create a new [FlutterVectorGraphicsListener]. + /// + /// The [locale] and [textDirection] are used to configure any text created + /// by the vector_graphic. + factory FlutterVectorGraphicsListener({ + int id = 0, + Locale? locale, + TextDirection? textDirection, + bool clipViewbox = true, + @visibleForTesting + PictureFactory pictureFactory = const _DefaultPictureFactory(), + VectorGraphicsErrorListener? onError, + }) { + final PictureRecorder recorder = pictureFactory.createPictureRecorder(); + return FlutterVectorGraphicsListener._( + id, + pictureFactory, + recorder, + pictureFactory.createCanvas(recorder), + locale, + textDirection, + clipViewbox, + onError: onError, + ); + } + + FlutterVectorGraphicsListener._( + this._id, + this._pictureFactory, + this._recorder, + this._canvas, + this._locale, + this._textDirection, + this._clipViewbox, { + this.onError, + }); + + final int _id; + + final PictureFactory _pictureFactory; + + final Locale? _locale; + final TextDirection? _textDirection; + final bool _clipViewbox; + + final PictureRecorder _recorder; + final Canvas _canvas; + + /// This variable will receive the Signature for the error + final VectorGraphicsErrorListener? onError; + + final List _paints = []; + final List _paths = []; + final List _shaders = []; + final List<_TextConfig> _textConfig = <_TextConfig>[]; + final List<_TextPosition> _textPositions = <_TextPosition>[]; + final List> _pendingImages = >[]; + final Map _images = {}; + final Map _patterns = {}; + Path? _currentPath; + Size _size = Size.zero; + bool _done = false; + + double? _accumulatedTextPositionX; + double _textPositionY = 0; + Float64List? _textTransform; + + _PatternConfig? _currentPattern; + + static final Paint _emptyPaint = Paint(); + static final Paint _grayscaleDstInPaint = Paint() + ..blendMode = BlendMode.dstIn + ..colorFilter = const ColorFilter.matrix([ + 0, 0, 0, 0, 0, // + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + ]); //convert to grayscale (https://www.w3.org/Graphics/Color/sRGB) and use them as transparency + + /// Convert the vector graphics asset this listener decoded into a [Picture]. + /// + /// This method can only be called once for a given listener instance. + PictureInfo toPicture() { + assert(!_done); + _done = true; + try { + return PictureInfo._(_recorder.endRecording(), _size); + } finally { + for (final Image image in _images.values) { + image.dispose(); + } + _images.clear(); + for (final _PatternState pattern in _patterns.values) { + pattern.shader?.dispose(); + } + _patterns.clear(); + } + } + + /// Wait for all pending images to load. + Future waitForImageDecode() { + assert(_pendingImages.isNotEmpty); + return Future.wait(_pendingImages); + } + + @override + Future onDrawPath(int pathId, int? paintId, int? patternId) async { + final Path path = _paths[pathId]; + Paint? paint; + if (paintId != null) { + paint = _paints[paintId]; + } + if (patternId != null) { + if (paintId != null) { + paint!.shader = _patterns[patternId]!.shader; + } else { + final Paint newPaint = Paint(); + newPaint.shader = _patterns[patternId]!.shader; + paint = newPaint; + } + } + if (_currentPattern != null) { + _patterns[_currentPattern!._patternId]! + .canvas! + .drawPath(path, paint ?? _emptyPaint); + } else { + _canvas.drawPath(path, paint ?? _emptyPaint); + } + } + + @override + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) { + final Vertices vertexData = Vertices.raw( + VertexMode.triangles, + vertices, + indices: indices, + ); + Paint? paint; + if (paintId != null) { + paint = _paints[paintId]; + } + _canvas.drawVertices( + vertexData, + BlendMode.srcOver, + paint ?? _emptyPaint, + ); + vertexData.dispose(); + } + + @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, + }) { + assert(_paints.length == id, 'Expect ID to be ${_paints.length}'); + final Paint paint = Paint()..color = Color(color); + if (blendMode != 0) { + paint.blendMode = BlendMode.values[blendMode]; + } + + if (shaderId != null) { + paint.shader = _shaders[shaderId]; + } + + if (paintStyle == 1) { + paint.style = PaintingStyle.stroke; + if (strokeCap != null && strokeCap != 0) { + paint.strokeCap = StrokeCap.values[strokeCap]; + } + if (strokeJoin != null && strokeJoin != 0) { + paint.strokeJoin = StrokeJoin.values[strokeJoin]; + } + if (strokeMiterLimit != null && strokeMiterLimit != 4.0) { + paint.strokeMiterLimit = strokeMiterLimit; + } + // SVG's default stroke width is 1.0. Flutter's default is 0.0. + if (strokeWidth != null && strokeWidth != 0.0) { + paint.strokeWidth = strokeWidth; + } + } + _paints.add(paint); + } + + @override + void onPathClose() { + _currentPath!.close(); + } + + @override + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _currentPath!.cubicTo(x1, y1, x2, y2, x3, y3); + } + + @override + void onPathFinished() { + _currentPath = null; + } + + @override + void onPathLineTo(double x, double y) { + _currentPath!.lineTo(x, y); + } + + @override + void onPathMoveTo(double x, double y) { + _currentPath!.moveTo(x, y); + } + + @override + void onPathStart(int id, int fillType) { + assert(_currentPath == null); + assert(_paths.length == id, 'Expected Id to be $id'); + + final Path path = Path(); + path.fillType = PathFillType.values[fillType]; + _paths.add(path); + _currentPath = path; + } + + @override + void onRestoreLayer() { + if (_currentPattern != null) { + final int patternId = _currentPattern!._patternId; + onPatternFinished(_currentPattern, _patterns[patternId]!.recorder, + _patterns[patternId]!.canvas!); + } else { + _canvas.restore(); + } + } + + @override + void onSaveLayer(int paintId) { + _canvas.saveLayer(null, _paints[paintId]); + } + + @override + void onMask() { + _canvas.saveLayer(null, _grayscaleDstInPaint); + } + + @override + void onClipPath(int pathId) { + _canvas.save(); + _canvas.clipPath(_paths[pathId]); + } + + @override + void onPatternStart(int patternId, double x, double y, double width, + double height, Float64List transform) { + assert(_currentPattern == null); + _currentPattern = _PatternConfig(patternId, width, height, transform); + final PictureRecorder recorder = _pictureFactory.createPictureRecorder(); + final Canvas newCanvas = _pictureFactory.createCanvas(recorder); + newCanvas.clipRect(Offset(x, y) & Size(width, height)); + _patterns[patternId] = _PatternState() + ..recorder = recorder + ..canvas = newCanvas; + } + + /// Creates ImageShader for active pattern. + // TODO(stuartmorgan): Fix this violation, which predates enabling the lint + // to catch it. + // ignore: library_private_types_in_public_api + void onPatternFinished(_PatternConfig? currentPattern, + PictureRecorder? patternRecorder, Canvas canvas) { + final FlutterVectorGraphicsListener patternListener = + FlutterVectorGraphicsListener._( + 0, + _pictureFactory, + patternRecorder!, + canvas, + _locale, + _textDirection, + _clipViewbox, + ); + + patternListener._size = + Size(currentPattern!._width, currentPattern._height); + + final PictureInfo pictureInfo = patternListener.toPicture(); + _currentPattern = null; + final Image image = pictureInfo.picture.toImageSync( + currentPattern._width.round(), currentPattern._height.round()); + + final ImageShader pattern = ImageShader( + image, + TileMode.repeated, + TileMode.repeated, + currentPattern._transform, + ); + + _patterns[currentPattern._patternId]!.shader = pattern; + image.dispose(); // kept alive by the shader. + } + + @override + void onLinearGradient( + double fromX, + double fromY, + double toX, + double toY, + Int32List colors, + Float32List? offsets, + int tileMode, + int id, + ) { + assert(_shaders.length == id); + + final Offset from = Offset(fromX, fromY); + final Offset to = Offset(toX, toY); + final List colorValues = [ + for (int i = 0; i < colors.length; i++) Color(colors[i]) + ]; + final Gradient gradient = Gradient.linear( + from, + to, + colorValues, + offsets, + TileMode.values[tileMode], + ); + _shaders.add(gradient); + } + + @override + void onRadialGradient( + double centerX, + double centerY, + double radius, + double? focalX, + double? focalY, + Int32List colors, + Float32List? offsets, + Float64List? transform, + int tileMode, + int id, + ) { + assert(_shaders.length == id); + + final Offset center = Offset(centerX, centerY); + final Offset? focal = focalX == null ? null : Offset(focalX, focalY!); + final List colorValues = [ + for (int i = 0; i < colors.length; i++) Color(colors[i]) + ]; + final bool hasFocal = focal != center && focal != null; + final Gradient gradient = Gradient.radial( + center, + radius, + colorValues, + offsets, + TileMode.values[tileMode], + transform, + hasFocal ? focal : null, + ); + _shaders.add(gradient); + } + + @override + void onSize(double width, double height) { + if (_clipViewbox) { + _canvas.clipRect(Offset.zero & Size(width, height)); + } + _size = Size(width, height); + } + + @override + void onTextConfig( + String text, + String? fontFamily, + double xAnchorMultiplier, + int fontWeight, + double fontSize, + int decoration, + int decorationStyle, + int decorationColor, + int id, + ) { + final List decorations = []; + if (decoration & kUnderlineMask != 0) { + decorations.add(TextDecoration.underline); + } + if (decoration & kOverlineMask != 0) { + decorations.add(TextDecoration.overline); + } + if (decoration & kLineThroughMask != 0) { + decorations.add(TextDecoration.lineThrough); + } + + _textConfig.add(_TextConfig( + text, + fontFamily, + xAnchorMultiplier, + FontWeight.values[fontWeight], + fontSize, + TextDecoration.combine(decorations), + TextDecorationStyle.values[decorationStyle], + Color(decorationColor), + )); + } + + @override + void onTextPosition( + int textPositionId, + double? x, + double? y, + double? dx, + double? dy, + bool reset, + Float64List? transform, + ) { + _textPositions.add(_TextPosition(x, y, dx, dy, reset, transform)); + } + + @override + void onUpdateTextPosition(int textPositionId) { + final _TextPosition position = _textPositions[textPositionId]; + if (position.reset) { + _accumulatedTextPositionX = 0; + _textPositionY = 0; + } + + if (position.x != null) { + _accumulatedTextPositionX = position.x; + } + if (position.y != null) { + _textPositionY = position.y ?? _textPositionY; + } + + if (position.dx != null) { + _accumulatedTextPositionX = + (_accumulatedTextPositionX ?? 0) + position.dx!; + } + if (position.dy != null) { + _textPositionY = _textPositionY + position.dy!; + } + + _textTransform = position.transform; + } + + @override + Future onDrawText( + int textId, + int? fillId, + int? strokeId, + int? patternId, + ) async { + final _TextConfig textConfig = _textConfig[textId]; + final double dx = _accumulatedTextPositionX ?? 0; + final double dy = _textPositionY; + double paragraphWidth = 0; + + void draw(int paintId) { + final Paint paint = _paints[paintId]; + if (patternId != null) { + paint.shader = _patterns[patternId]!.shader; + } + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( + textDirection: _textDirection, + )); + builder.pushStyle(TextStyle( + locale: _locale, + foreground: paint, + fontWeight: textConfig.fontWeight, + fontSize: textConfig.fontSize, + fontFamily: textConfig.fontFamily, + decoration: textConfig.decoration, + decorationStyle: textConfig.decorationStyle, + decorationColor: textConfig.decorationColor, + )); + + builder.addText(textConfig.text); + + final Paragraph paragraph = builder.build(); + paragraph.layout(const ParagraphConstraints( + width: double.infinity, + )); + paragraphWidth = paragraph.maxIntrinsicWidth; + + if (_textTransform != null) { + _canvas.save(); + _canvas.transform(_textTransform!); + } + _canvas.drawParagraph( + paragraph, + Offset( + dx - paragraph.maxIntrinsicWidth * textConfig.xAnchorMultiplier, + dy - paragraph.alphabeticBaseline, + ), + ); + paragraph.dispose(); + if (_textTransform != null) { + _canvas.restore(); + } + } + + if (fillId != null) { + draw(fillId); + } + if (strokeId != null) { + draw(strokeId); + } + _accumulatedTextPositionX = dx + paragraphWidth; + } + + int _createImageKey(int imageId, int format) { + return Object.hash(_id, imageId, format); + } + + @override + void onImage( + int imageId, + int format, + Uint8List data, { + VectorGraphicsErrorListener? onError, + }) { + final Completer completer = Completer(); + _pendingImages.add(completer.future); + final ImageStreamCompleter? cacheCompleter = + imageCache.putIfAbsent(_createImageKey(imageId, format), () { + return OneFrameImageStreamCompleter(ImmutableBuffer.fromUint8List(data) + .then((ImmutableBuffer buffer) async { + try { + final ImageDescriptor descriptor = + await ImageDescriptor.encoded(buffer); + final Codec codec = await descriptor.instantiateCodec(); + final FrameInfo info = await codec.getNextFrame(); + final Image image = info.image; + descriptor.dispose(); + codec.dispose(); + return ImageInfo(image: image); + } finally { + buffer.dispose(); + } + })); + }); + // an error occurred. + if (cacheCompleter == null) { + completer.completeError('Failed to load image'); + return; + } + late ImageStreamListener listener; + listener = ImageStreamListener( + (ImageInfo image, bool synchronousCall) { + cacheCompleter.removeListener(listener); + _images[imageId] = image.image; + completer.complete(); + }, + onError: (Object exception, StackTrace? stackTrace) { + if (!completer.isCompleted) { + completer.complete(); + } + cacheCompleter.removeListener(listener); + if (onError != null) { + onError(exception, stackTrace); + } else { + FlutterError.reportError(FlutterErrorDetails( + context: ErrorDescription('Failed to load image'), + library: 'image resource service', + exception: exception, + stack: stackTrace, + silent: true, + )); + } + }, + ); + cacheCompleter.addListener(listener); + } + + @override + void onDrawImage(int imageId, double x, double y, double width, double height, + Float64List? transform) { + final Image image = _images[imageId]!; + if (transform != null) { + _canvas.save(); + _canvas.transform(transform); + } + _canvas.drawImageRect( + image, + Rect.fromLTRB(0, 0, image.width.toDouble(), image.height.toDouble()), + Rect.fromLTWH(x, y, width, height), + Paint(), + ); + if (transform != null) { + _canvas.restore(); + } + } +} + +class _TextPosition { + const _TextPosition( + this.x, + this.y, + this.dx, + this.dy, + this.reset, + this.transform, + ); + + final double? x; + final double? y; + final double? dx; + final double? dy; + final bool reset; + final Float64List? transform; +} + +class _TextConfig { + const _TextConfig( + this.text, + this.fontFamily, + this.xAnchorMultiplier, + this.fontWeight, + this.fontSize, + this.decoration, + this.decorationStyle, + this.decorationColor, + ); + + final String text; + final String? fontFamily; + final double fontSize; + final double xAnchorMultiplier; + final FontWeight fontWeight; + final TextDecoration decoration; + final TextDecorationStyle decorationStyle; + final Color decorationColor; +} + +/// An exception thrown if decoding fails. +/// +/// The [originalException] is a detailed exception about what failed in +/// decoding. The [source] contains the object that was used to load the bytes. +class VectorGraphicsDecodeException implements Exception { + const VectorGraphicsDecodeException._(this.source, this.originalException); + + /// The object used to load the bytes for this + final BytesLoader source; + + /// The original exception thrown by the decoder, for example a [StateError] + /// indicating what specifically went wrong. + final Object originalException; + + @override + String toString() => + 'VectorGraphicsDecodeException: Failed to decode vector graphic from $source.\n\nAdditional error: $originalException'; +} diff --git a/packages/vector_graphics/lib/src/loader.dart b/packages/vector_graphics/lib/src/loader.dart new file mode 100644 index 00000000000..8f12e296715 --- /dev/null +++ b/packages/vector_graphics/lib/src/loader.dart @@ -0,0 +1,177 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:http/http.dart' as http; + +/// An interface that can be implemented to support decoding vector graphic +/// binary assets from different byte sources. +/// +/// A bytes loader class should not be constructed directly in a build method, +/// if this is done the corresponding [VectorGraphic] widget may repeatedly +/// reload the bytes. +/// +/// Implementations must overide [toString] for debug reporting. +/// +/// See also: +/// * [AssetBytesLoader], for loading from the asset bundle. +/// * [NetworkBytesLoader], for loading network bytes. +@immutable +abstract class BytesLoader { + /// Const constructor to allow subtypes to be const. + const BytesLoader(); + + /// Load the byte data for a vector graphic binary asset. + Future loadBytes(BuildContext? context); + + /// Create an object that can be used to uniquely identify this asset + /// and loader combination. + /// + /// For most [BytesLoader] subclasses, this can safely return the same + /// instance. If the loader looks up additional dependencies using the + /// [context] argument of [loadBytes], then those objects should be + /// incorporated into a new cache key. + Object cacheKey(BuildContext? context) => this; +} + +/// Loads vector graphics data from an asset bundle. +/// +/// This loader does not cache bytes by default. The Flutter framework +/// implementations of [AssetBundle] also do not typically cache binary data. +/// +/// Callers that would benefit from caching should provide a custom +/// [AssetBundle] that caches data, or should create their own implementation +/// of an asset bytes loader. +class AssetBytesLoader extends BytesLoader { + /// A loader that retrieves bytes from an [AssetBundle]. + /// + /// See [AssetBytesLoader]. + const AssetBytesLoader( + this.assetName, { + this.packageName, + this.assetBundle, + }); + + /// The name of the asset to load. + final String assetName; + + /// The package name to load from, if any. + final String? packageName; + + /// The asset bundle to use. + /// + /// If unspecified, [DefaultAssetBundle.of] the current context will be used. + final AssetBundle? assetBundle; + + AssetBundle _resolveBundle(BuildContext? context) { + if (assetBundle != null) { + return assetBundle!; + } + if (context != null) { + return DefaultAssetBundle.of(context); + } + return rootBundle; + } + + @override + Future loadBytes(BuildContext? context) { + return _resolveBundle(context).load( + packageName == null ? assetName : 'packages/$packageName/$assetName', + ); + } + + @override + int get hashCode => Object.hash(assetName, packageName, assetBundle); + + @override + bool operator ==(Object other) { + return other is AssetBytesLoader && + other.assetName == assetName && + other.assetBundle == assetBundle && + other.packageName == packageName; + } + + @override + Object cacheKey(BuildContext? context) { + return _AssetByteLoaderCacheKey( + assetName, + packageName, + _resolveBundle(context), + ); + } + + @override + String toString() => + 'VectorGraphicAsset(${packageName != null ? '$packageName/' : ''}$assetName)'; +} + +// Replaces the cache key for [AssetBytesLoader] to account for the fact that +// different widgets may select a different asset bundle based on the return +// value of `DefaultAssetBundle.of(context)`. +@immutable +class _AssetByteLoaderCacheKey { + const _AssetByteLoaderCacheKey( + this.assetName, this.packageName, this.assetBundle); + + final String assetName; + final String? packageName; + + final AssetBundle assetBundle; + + @override + int get hashCode => Object.hash(assetName, packageName, assetBundle); + + @override + bool operator ==(Object other) { + return other is _AssetByteLoaderCacheKey && + other.assetName == assetName && + other.assetBundle == assetBundle && + other.packageName == packageName; + } + + @override + String toString() => + 'VectorGraphicAsset(${packageName != null ? '$packageName/' : ''}$assetName)'; +} + +/// A controller for loading vector graphics data from over the network. +/// +/// This loader does not cache bytes requested from the network. +class NetworkBytesLoader extends BytesLoader { + /// Creates a new loading context for network bytes. + const NetworkBytesLoader( + this.url, { + this.headers, + http.Client? httpClient, + }) : _httpClient = httpClient; + + /// The HTTP headers to use for the network request. + final Map? headers; + + /// The [Uri] of the resource to request. + final Uri url; + + final http.Client? _httpClient; + + @override + Future loadBytes(BuildContext? context) async { + final http.Client client = _httpClient ?? http.Client(); + final Uint8List bytes = (await client.get(url, headers: headers)).bodyBytes; + return bytes.buffer.asByteData(); + } + + @override + int get hashCode => Object.hash(url, headers); + + @override + bool operator ==(Object other) { + return other is NetworkBytesLoader && + other.headers == headers && + other.url == url; + } + + @override + String toString() => 'VectorGraphicNetwork($url)'; +} diff --git a/packages/vector_graphics/lib/src/render_object_selection.dart b/packages/vector_graphics/lib/src/render_object_selection.dart new file mode 100644 index 00000000000..d8e8c1ef1db --- /dev/null +++ b/packages/vector_graphics/lib/src/render_object_selection.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; + +bool? _cachedUseHtmlRenderObject; + +/// Whether or not the HTML compatibility render object should be used. +/// +/// This render object has worse performance and supports fewer features. +bool useHtmlRenderObject() { + if (!kIsWeb) { + return false; + } + + if (_cachedUseHtmlRenderObject != null) { + return _cachedUseHtmlRenderObject!; + } + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + ui.Canvas(recorder); + final ui.Picture picture = recorder.endRecording(); + ui.Image? image; + try { + image = picture.toImageSync(1, 1); + _cachedUseHtmlRenderObject = false; + } on UnsupportedError catch (_) { + _cachedUseHtmlRenderObject = true; + } finally { + image?.dispose(); + picture.dispose(); + } + return _cachedUseHtmlRenderObject!; +} diff --git a/packages/vector_graphics/lib/src/render_vector_graphic.dart b/packages/vector_graphics/lib/src/render_vector_graphic.dart new file mode 100644 index 00000000000..a6b530469e5 --- /dev/null +++ b/packages/vector_graphics/lib/src/render_vector_graphic.dart @@ -0,0 +1,449 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui; + +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'debug.dart'; +import 'listener.dart'; + +/// The cache key for a rasterized vector graphic. +@immutable +class RasterKey { + /// Create a new [RasterKey]. + const RasterKey(this.assetKey, this.width, this.height); + + /// An object that is used to identify the raster data this key will store. + /// + /// Typically this is the value returned from [BytesLoader.cacheKey]. + final Object assetKey; + + /// The height of this vector graphic raster, in physical pixels. + final int width; + + /// The width of this vector graphic raster, in physical pixels. + final int height; + + @override + bool operator ==(Object other) { + return other is RasterKey && + other.assetKey == assetKey && + other.width == width && + other.height == height; + } + + @override + int get hashCode => Object.hash(assetKey, width, height); +} + +/// The cache entry for a rasterized vector graphic. +class RasterData { + /// Create a new [RasterData]. + RasterData(this._image, this.count, this.key); + + /// The rasterized vector graphic. + ui.Image get image => _image!; + ui.Image? _image; + + /// The cache key used to identify this vector graphic. + final RasterKey key; + + /// The number of render objects currently using this + /// vector graphic raster data. + int count = 0; + + /// Dispose this raster data. + void dispose() { + _image?.dispose(); + _image = null; + } +} + +/// For testing only, clear all pending rasters. +@visibleForTesting +void debugClearRasteCaches() { + if (!kDebugMode) { + return; + } + RenderVectorGraphic._liveRasterCache.clear(); +} + +/// A render object which draws a vector graphic instance as a raster. +class RenderVectorGraphic extends RenderBox { + /// Create a new [RenderVectorGraphic]. + RenderVectorGraphic( + this._pictureInfo, + this._assetKey, + this._colorFilter, + this._devicePixelRatio, + this._opacity, + this._scale, + ) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + } + + static final Map _liveRasterCache = + {}; + + /// A key that uniquely identifies the [pictureInfo] used for this vg. + Object get assetKey => _assetKey; + Object _assetKey; + set assetKey(Object value) { + if (value == assetKey) { + return; + } + _assetKey = value; + // Dont call mark needs paint here since a change in just the asset key + // isn't sufficient to force a re-draw. + } + + /// The [PictureInfo] which contains the vector graphic and size to draw. + PictureInfo get pictureInfo => _pictureInfo; + PictureInfo _pictureInfo; + set pictureInfo(PictureInfo value) { + if (identical(value, _pictureInfo)) { + return; + } + _pictureInfo = value; + markNeedsPaint(); + } + + /// An optional [ColorFilter] to apply to the rasterized vector graphic. + ColorFilter? get colorFilter => _colorFilter; + ColorFilter? _colorFilter; + set colorFilter(ColorFilter? value) { + if (colorFilter == value) { + return; + } + _colorFilter = value; + markNeedsPaint(); + } + + /// The device pixel ratio the vector graphic should be rasterized at. + double get devicePixelRatio => _devicePixelRatio; + double _devicePixelRatio; + set devicePixelRatio(double value) { + if (value == devicePixelRatio) { + return; + } + _devicePixelRatio = value; + markNeedsPaint(); + } + + double _opacityValue = 1.0; + + /// An opacity to draw the rasterized vector graphic with. + Animation? get opacity => _opacity; + Animation? _opacity; + set opacity(Animation? value) { + if (value == opacity) { + return; + } + _opacity?.removeListener(_updateOpacity); + _opacity = value; + _opacity?.addListener(_updateOpacity); + markNeedsPaint(); + } + + void _updateOpacity() { + if (opacity == null) { + return; + } + final double newValue = opacity!.value; + if (newValue == _opacityValue) { + return; + } + _opacityValue = newValue; + markNeedsPaint(); + } + + /// An additional ratio the picture will be transformed by. + /// + /// This value is used to ensure the computed raster does not + /// have extra pixelation from scaling in the case that a the [BoxFit] + /// value used in the [VectorGraphic] widget implies a scaling factor + /// greater than 1.0. + /// + /// For example, if the vector graphic widget is sized at 100x100, + /// the vector graphic itself has a size of 50x50, and [BoxFit.fill] + /// is used. This will compute a scale of 2.0, which will result in a + /// raster that is 100x100. + double get scale => _scale; + double _scale; + set scale(double value) { + assert(value != 0); + if (value == scale) { + return; + } + _scale = value; + markNeedsPaint(); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.smallest; + } + + static RasterData _createRaster( + RasterKey key, double scaleFactor, PictureInfo info) { + final int scaledWidth = key.width; + final int scaledHeight = key.height; + // In order to scale a picture, it must be placed in a new picture + // with a transform applied. Surprisingly, the height and width + // arguments of Picture.toImage do not control the resolution that the + // picture is rendered at, instead it controls how much of the picture to + // capture in a raster. + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + + canvas.scale(scaleFactor); + canvas.drawPicture(info.picture); + final ui.Picture rasterPicture = recorder.endRecording(); + + final ui.Image pending = + rasterPicture.toImageSync(scaledWidth, scaledHeight); + return RasterData(pending, 0, key); + } + + void _maybeReleaseRaster(RasterData? data) { + if (data == null) { + return; + } + data.count -= 1; + if (data.count == 0 && _liveRasterCache.containsKey(data.key)) { + _liveRasterCache.remove(data.key); + data.dispose(); + } + } + + // Re-create the raster for a given vector graphic if the target size + // is sufficiently different. Returns `null` if rasterData has been + // updated immediately. + void _maybeUpdateRaster() { + final int scaledWidth = + (pictureInfo.size.width * devicePixelRatio / scale).round(); + final int scaledHeight = + (pictureInfo.size.height * devicePixelRatio / scale).round(); + final RasterKey key = RasterKey(assetKey, scaledWidth, scaledHeight); + + // First check if the raster is available synchronously. This also handles + // a no-op change that would resolve to an identical picture. + if (_liveRasterCache.containsKey(key)) { + final RasterData data = _liveRasterCache[key]!; + if (data != _rasterData) { + _maybeReleaseRaster(_rasterData); + data.count += 1; + } + _rasterData = data; + return; + } + final RasterData data = + _createRaster(key, devicePixelRatio / scale, pictureInfo); + data.count += 1; + + assert(!_liveRasterCache.containsKey(key)); + assert(data.count == 1); + assert(!debugDisposed!); + + _liveRasterCache[key] = data; + _maybeReleaseRaster(_rasterData); + _rasterData = data; + } + + RasterData? _rasterData; + + @override + void attach(covariant PipelineOwner owner) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + super.attach(owner); + } + + @override + void detach() { + _opacity?.removeListener(_updateOpacity); + super.detach(); + } + + @override + void dispose() { + _maybeReleaseRaster(_rasterData); + _opacity?.removeListener(_updateOpacity); + super.dispose(); + } + + @override + void paint(PaintingContext context, ui.Offset offset) { + assert(size == pictureInfo.size); + if (kDebugMode && debugSkipRaster) { + context.canvas + .drawRect(offset & size, Paint()..color = const Color(0xFFFF00FF)); + return; + } + + if (_opacityValue <= 0.0) { + return; + } + + _maybeUpdateRaster(); + final ui.Image image = _rasterData!.image; + final int width = _rasterData!.key.width; + final int height = _rasterData!.key.height; + + // Use `FilterQuality.low` to scale the image, which corresponds to + // bilinear interpolation. + final Paint colorPaint = Paint()..filterQuality = ui.FilterQuality.low; + if (colorFilter != null) { + colorPaint.colorFilter = colorFilter; + } + colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue); + final Rect src = ui.Rect.fromLTWH( + 0, + 0, + width.toDouble(), + height.toDouble(), + ); + final Rect dst = ui.Rect.fromLTWH( + offset.dx, + offset.dy, + pictureInfo.size.width, + pictureInfo.size.height, + ); + + context.canvas.drawImageRect( + image, + src, + dst, + colorPaint, + ); + } +} + +/// A render object which draws a vector graphic instance as a picture. +class RenderPictureVectorGraphic extends RenderBox { + /// Create a new [RenderPictureVectorGraphic]. + RenderPictureVectorGraphic( + this._pictureInfo, + this._colorFilter, + this._opacity, + ) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + } + + /// The [PictureInfo] which contains the vector graphic and size to draw. + PictureInfo get pictureInfo => _pictureInfo; + PictureInfo _pictureInfo; + set pictureInfo(PictureInfo value) { + if (identical(value, _pictureInfo)) { + return; + } + _pictureInfo = value; + markNeedsPaint(); + } + + /// An optional [ColorFilter] to apply to the rasterized vector graphic. + ColorFilter? get colorFilter => _colorFilter; + ColorFilter? _colorFilter; + set colorFilter(ColorFilter? value) { + if (colorFilter == value) { + return; + } + _colorFilter = value; + markNeedsPaint(); + } + + double _opacityValue = 1.0; + + /// An opacity to draw the rasterized vector graphic with. + Animation? get opacity => _opacity; + Animation? _opacity; + set opacity(Animation? value) { + if (value == opacity) { + return; + } + _opacity?.removeListener(_updateOpacity); + _opacity = value; + _opacity?.addListener(_updateOpacity); + markNeedsPaint(); + } + + void _updateOpacity() { + if (opacity == null) { + return; + } + final double newValue = opacity!.value; + if (newValue == _opacityValue) { + return; + } + _opacityValue = newValue; + markNeedsPaint(); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.smallest; + } + + @override + void attach(covariant PipelineOwner owner) { + _opacity?.addListener(_updateOpacity); + _updateOpacity(); + super.attach(owner); + } + + @override + void detach() { + _opacity?.removeListener(_updateOpacity); + super.detach(); + } + + @override + void dispose() { + _opacity?.removeListener(_updateOpacity); + super.dispose(); + } + + @override + void paint(PaintingContext context, ui.Offset offset) { + assert(size == pictureInfo.size); + if (_opacityValue <= 0.0) { + return; + } + + final Paint colorPaint = Paint(); + if (colorFilter != null) { + colorPaint.colorFilter = colorFilter; + } + colorPaint.color = Color.fromRGBO(0, 0, 0, _opacityValue); + final int saveCount = context.canvas.getSaveCount(); + if (offset != Offset.zero) { + context.canvas.save(); + context.canvas.translate(offset.dx, offset.dy); + } + if (_opacityValue != 1.0 || colorFilter != null) { + context.canvas.save(); + context.canvas.clipRect(Offset.zero & size); + context.canvas.saveLayer(Offset.zero & size, colorPaint); + } + context.canvas.drawPicture(pictureInfo.picture); + context.canvas.restoreToCount(saveCount); + } +} diff --git a/packages/vector_graphics/lib/src/vector_graphics.dart b/packages/vector_graphics/lib/src/vector_graphics.dart new file mode 100644 index 00000000000..eb5eca76fd1 --- /dev/null +++ b/packages/vector_graphics/lib/src/vector_graphics.dart @@ -0,0 +1,708 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'html_render_vector_graphics.dart'; +import 'listener.dart'; +import 'loader.dart'; +import 'render_object_selection.dart'; +import 'render_vector_graphic.dart'; + +export 'listener.dart' show PictureInfo; +export 'loader.dart'; + +/// How the vector graphic will be rendered by the Flutter framework. +/// +/// This is ultimately a performance versus fidelity tradeoff. While the +/// raster strategy performs better than the picture strategy in most benchmarks, +/// it can be more difficult to use. Any parent transforms that are not +/// accounted by the application developer can cause the vector graphic to +/// appear pixelated. The picture strategy has no such trade-off, and roughly +/// corresponds to the previous behavior of flutter_svg +/// +/// Consider using the raster strategy for very large or complicated vector graphics +/// that are used as backdrops at fixed scales. The picture strategy makes a better +/// default choice for icon-like vector graphics or vector graphics that have +/// small dimensions. +enum RenderingStrategy { + /// Draw the vector graphic as a raster. + /// + /// This raster is reused from frame to frame which can significantly improve + /// performance if the vector graphic is complicated. + raster, + + /// Draw the vector graphic as a picture. + picture, +} + +/// The signature that [VectorGraphic.errorBuilder] uses to report exceptions. +typedef VectorGraphicsErrorWidget = Widget Function( + BuildContext context, + Object error, + StackTrace stackTrace, +); + +/// A vector graphic/flutter_svg compatibility shim. +VectorGraphic createCompatVectorGraphic({ + Key? key, + required BytesLoader loader, + double? width, + double? height, + BoxFit fit = BoxFit.contain, + AlignmentGeometry alignment = Alignment.center, + String? semanticsLabel, + bool excludeFromSemantics = false, + Clip clipBehavior = Clip.hardEdge, + WidgetBuilder? placeholderBuilder, + VectorGraphicsErrorWidget? errorBuilder, + ColorFilter? colorFilter, + Animation? opacity, + RenderingStrategy strategy = RenderingStrategy.picture, + bool clipViewbox = true, + bool matchTextDirection = false, +}) { + return VectorGraphic._( + key: key, + loader: loader, + width: width, + height: height, + fit: fit, + alignment: alignment, + semanticsLabel: semanticsLabel, + excludeFromSemantics: excludeFromSemantics, + clipBehavior: clipBehavior, + placeholderBuilder: placeholderBuilder, + errorBuilder: errorBuilder, + colorFilter: colorFilter, + opacity: opacity, + strategy: strategy, + clipViewbox: clipViewbox, + matchTextDirection: matchTextDirection, + ); +} + +/// A widget that displays a [VectorGraphicsCodec] encoded asset. +/// +/// This widget will ask the loader to load the bytes whenever its +/// dependencies change or it is configured with a new loader. A loader may +/// or may not choose to cache its responses, potentially resulting in multiple +/// disk or network accesses for the same bytes. +class VectorGraphic extends StatefulWidget { + /// A widget that displays a vector graphics created via a + /// [VectorGraphicsCodec]. + /// + /// If `matchTextDirection` is set to true, the picture will be flipped + /// horizontally in [TextDirection.rtl] contexts. + /// + /// The [semanticsLabel] can be used to identify the purpose of this picture for + /// screen reading software. + /// + /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored. + /// + /// See [VectorGraphic]. + const VectorGraphic({ + super.key, + required this.loader, + this.width, + this.height, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.semanticsLabel, + this.excludeFromSemantics = false, + this.clipBehavior = Clip.hardEdge, + this.placeholderBuilder, + this.errorBuilder, + this.colorFilter, + this.opacity, + this.clipViewbox = true, + this.matchTextDirection = false, + }) : strategy = RenderingStrategy.raster; + + /// A specialized constructor for flutter_svg interop. + const VectorGraphic._({ + super.key, + required this.loader, + this.width, + this.height, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.semanticsLabel, + this.excludeFromSemantics = false, + this.clipBehavior = Clip.hardEdge, + this.placeholderBuilder, + this.errorBuilder, + this.colorFilter, + this.opacity, + this.strategy = RenderingStrategy.picture, + this.clipViewbox = true, + this.matchTextDirection = false, + }); + + /// A delegate for fetching the raw bytes of the vector graphic. + /// + /// The [BytesLoader.loadBytes] method will be called with this + /// widget's [BuildContext] whenever dependencies change or the widget + /// configuration changes the loader. + final BytesLoader loader; + + /// If specified, the width to use for the vector graphic. If unspecified, + /// the vector graphic will take the width of its parent. + final double? width; + + /// If specified, the height to use for the vector graphic. If unspecified, + /// the vector graphic 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 AlignmentGeometry alignment; + + /// If true, will horizontally flip the picture in [TextDirection.rtl] contexts. + final bool matchTextDirection; + + /// The [Semantics] label for this picture. + /// + /// The value indicates the purpose of the picture, and will be read out by + /// screen readers. + final String? semanticsLabel; + + /// Whether to exclude this picture from semantics. + /// + /// Useful for pictures which do not contribute meaningful semantic information to an + /// application. + final bool excludeFromSemantics; + + /// The content will be clipped (or not) according to this option. + /// + /// See the enum [Clip] for details of all possible options and their common + /// use cases. + /// + /// Defaults to [Clip.hardEdge], and must not be null. + final Clip clipBehavior; + + /// The placeholder to use while fetching, decoding, and parsing the vector_graphics data. + final WidgetBuilder? placeholderBuilder; + + /// A callback that fires if some exception happens during data acquisition or decoding. + final VectorGraphicsErrorWidget? errorBuilder; + + /// If provided, a color filter to apply to the vector graphic when painting. + /// + /// For example, `ColorFilter.mode(Colors.red, BlendMode.srcIn)` to give the vector + /// graphic a solid red color. + /// + /// This is more efficient than using a [ColorFiltered] widget to wrap the vector + /// graphic, since this avoids creating a new composited layer. Composited layers + /// may double memory usage as the image is painted onto an offscreen render target. + /// + /// Example: + /// + /// ```dart + /// VectorGraphic(loader: _assetLoader, colorFilter: ColorFilter.mode(Colors.red, BlendMode.srcIn)); + /// ``` + final ColorFilter? colorFilter; + + /// If non-null, the value from the Animation is multiplied with the opacity + /// of each vector graphic pixel before painting onto the canvas. + /// + /// This is more efficient than using FadeTransition to change the opacity of an image, + /// since this avoids creating a new composited layer. Composited layers may double memory + /// usage as the image is painted onto an offscreen render target. + /// + /// This value does not apply to the widgets created by a [placeholderBuilder]. + /// + /// To provide a fixed opacity value, or to convert from a callback based API that + /// does not use animation objects, consider using an [AlwaysStoppedAnimation]. + /// + /// Example: + /// + /// ```dart + /// VectorGraphic(loader: _assetLoader, opacity: const AlwaysStoppedAnimation(0.33)); + /// ``` + final Animation? opacity; + + /// The rendering strategy used by the vector graphic. + /// + /// By default this is [RenderingStrategy.raster]. + final RenderingStrategy strategy; + + /// Whether the graphic should be clipped to its viewbox. + /// + /// If true, this adds a clip sized to the dimensions of the graphic before + /// drawing. This prevents the graphic from accidentally drawing outside of + /// its specified dimensions. Some graphics intentionally draw outside of + /// their specified dimensions and thus must not be clipped. + final bool clipViewbox; + + @override + State createState() => _VectorGraphicWidgetState(); +} + +class _PictureData { + _PictureData(this.pictureInfo, this.count, this.key); + + final PictureInfo pictureInfo; + _PictureKey key; + int count = 0; +} + +@immutable +class _PictureKey { + const _PictureKey( + this.cacheKey, this.locale, this.textDirection, this.clipViewbox); + + final Object cacheKey; + final Locale? locale; + final TextDirection? textDirection; + final bool clipViewbox; + + @override + int get hashCode => Object.hash(cacheKey, locale, textDirection, clipViewbox); + + @override + bool operator ==(Object other) => + other is _PictureKey && + other.cacheKey == cacheKey && + other.locale == locale && + other.textDirection == textDirection && + other.clipViewbox == clipViewbox; +} + +class _VectorGraphicWidgetState extends State { + _PictureData? _pictureInfo; + Object? _error; + StackTrace? _stackTrace; + Locale? locale; + TextDirection? textDirection; + + static final Map<_PictureKey, _PictureData> _livePictureCache = + <_PictureKey, _PictureData>{}; + static final Map<_PictureKey, Future<_PictureData>> _pendingPictures = + <_PictureKey, Future<_PictureData>>{}; + + @override + void didChangeDependencies() { + locale = Localizations.maybeLocaleOf(context); + textDirection = Directionality.maybeOf(context); + _loadAssetBytes(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(covariant VectorGraphic oldWidget) { + if (oldWidget.loader != widget.loader) { + _loadAssetBytes(); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _maybeReleasePicture(_pictureInfo); + _pictureInfo = null; + super.dispose(); + } + + void _maybeReleasePicture(_PictureData? data) { + if (data == null) { + return; + } + data.count -= 1; + if (data.count == 0 && _livePictureCache.containsKey(data.key)) { + _livePictureCache.remove(data.key); + data.pictureInfo.picture.dispose(); + } + } + + Future<_PictureData> _loadPicture( + BuildContext context, _PictureKey key, BytesLoader loader) { + if (_pendingPictures.containsKey(key)) { + return _pendingPictures[key]!; + } + final Future<_PictureData> result = + loader.loadBytes(context).then((ByteData data) { + return decodeVectorGraphics( + data, + locale: key.locale, + textDirection: key.textDirection, + clipViewbox: key.clipViewbox, + loader: loader, + onError: (Object error, StackTrace? stackTrace) { + return _handleError( + error, + stackTrace, + ); + }, + ); + }).then((PictureInfo pictureInfo) { + return _PictureData(pictureInfo, 0, key); + }); + _pendingPictures[key] = result; + result.whenComplete(() { + _pendingPictures.remove(key); + }); + return result; + } + + void _handleError(Object error, StackTrace? stackTrace) { + setState(() { + _error = error; + _stackTrace = stackTrace; + }); + } + + void _loadAssetBytes() { + // First check if we have an avilable picture and use this immediately. + final Object loaderKey = widget.loader.cacheKey(context); + final _PictureKey key = + _PictureKey(loaderKey, locale, textDirection, widget.clipViewbox); + final _PictureData? data = _livePictureCache[key]; + if (data != null) { + data.count += 1; + setState(() { + _maybeReleasePicture(_pictureInfo); + _pictureInfo = data; + }); + return; + } + // If not, then check if there is a pending load. + final BytesLoader loader = widget.loader; + _loadPicture(context, key, loader).then((_PictureData data) { + data.count += 1; + + // The widget may have changed, requesting a new vector graphic before + // this operation could complete. + if (!mounted || loader != widget.loader) { + _maybeReleasePicture(data); + return; + } + if (data.count == 1) { + _livePictureCache[key] = data; + } + setState(() { + _maybeReleasePicture(_pictureInfo); + _pictureInfo = data; + }); + }); + } + + static final bool _webRenderObject = useHtmlRenderObject(); + + @override + Widget build(BuildContext context) { + final PictureInfo? pictureInfo = _pictureInfo?.pictureInfo; + + Widget child; + if (pictureInfo != null) { + // If the caller did not specify a width or height, fall back to the + // size of the graphic. + // If the caller did specify a width or height, preserve the aspect ratio + // of the graphic and center it within that width and height. + double? width = widget.width; + double? height = widget.height; + + if (width == null && height == null) { + width = pictureInfo.size.width; + height = pictureInfo.size.height; + } else if (height != null && !pictureInfo.size.isEmpty) { + width = height / pictureInfo.size.height * pictureInfo.size.width; + } else if (width != null && !pictureInfo.size.isEmpty) { + height = width / pictureInfo.size.width * pictureInfo.size.height; + } + + assert(width != null && height != null); + + double scale = 1.0; + scale = math.min( + pictureInfo.size.width / width!, + pictureInfo.size.height / height!, + ); + + if (_webRenderObject) { + child = _RawWebVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + ); + } else if (widget.strategy == RenderingStrategy.raster) { + child = _RawVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + scale: scale, + ); + } else { + child = _RawPictureVectorGraphicWidget( + pictureInfo: pictureInfo, + assetKey: _pictureInfo!.key, + colorFilter: widget.colorFilter, + opacity: widget.opacity, + ); + } + + if (widget.matchTextDirection) { + final TextDirection direction = Directionality.of(context); + if (direction == TextDirection.rtl) { + child = Transform( + transform: Matrix4.identity() + ..translate(pictureInfo.size.width) + ..scale(-1.0, 1.0), + child: child, + ); + } + } + + child = SizedBox( + width: width, + height: height, + child: FittedBox( + fit: widget.fit, + alignment: widget.alignment, + clipBehavior: widget.clipBehavior, + child: SizedBox.fromSize( + size: pictureInfo.size, + child: child, + ), + ), + ); + } else if (_error != null && widget.errorBuilder != null) { + child = widget.errorBuilder!( + context, + _error!, + _stackTrace ?? StackTrace.empty, + ); + } else { + child = widget.placeholderBuilder?.call(context) ?? + SizedBox( + width: widget.width, + height: widget.height, + ); + } + + if (!widget.excludeFromSemantics) { + child = Semantics( + container: widget.semanticsLabel != null, + image: true, + label: widget.semanticsLabel ?? '', + child: child, + ); + } + return child; + } +} + +class _RawVectorGraphicWidget extends SingleChildRenderObjectWidget { + const _RawVectorGraphicWidget({ + required this.pictureInfo, + required this.colorFilter, + required this.opacity, + required this.scale, + required this.assetKey, + }); + + final PictureInfo pictureInfo; + final ColorFilter? colorFilter; + final double scale; + final Animation? opacity; + final Object assetKey; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderVectorGraphic( + pictureInfo, + assetKey, + colorFilter, + MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0, + opacity, + scale, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant RenderVectorGraphic renderObject, + ) { + renderObject + ..pictureInfo = pictureInfo + ..assetKey = assetKey + ..colorFilter = colorFilter + ..devicePixelRatio = MediaQuery.maybeOf(context)?.devicePixelRatio ?? 1.0 + ..opacity = opacity + ..scale = scale; + } +} + +class _RawWebVectorGraphicWidget extends SingleChildRenderObjectWidget { + const _RawWebVectorGraphicWidget({ + required this.pictureInfo, + required this.colorFilter, + required this.opacity, + required this.assetKey, + }); + + final PictureInfo pictureInfo; + final ColorFilter? colorFilter; + final Animation? opacity; + final Object assetKey; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderWebVectorGraphic( + pictureInfo, + assetKey, + colorFilter, + opacity, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant RenderWebVectorGraphic renderObject, + ) { + renderObject + ..pictureInfo = pictureInfo + ..assetKey = assetKey + ..colorFilter = colorFilter + ..opacity = opacity; + } +} + +class _RawPictureVectorGraphicWidget extends SingleChildRenderObjectWidget { + const _RawPictureVectorGraphicWidget({ + required this.pictureInfo, + required this.colorFilter, + required this.opacity, + required this.assetKey, + }); + + final PictureInfo pictureInfo; + final ColorFilter? colorFilter; + final Animation? opacity; + final Object assetKey; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderPictureVectorGraphic( + pictureInfo, + colorFilter, + opacity, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant RenderPictureVectorGraphic renderObject, + ) { + renderObject + ..pictureInfo = pictureInfo + ..colorFilter = colorFilter + ..opacity = opacity; + } +} + +/// Utility functionality for interaction with vector graphic assets. +class VectorGraphicUtilities { + const VectorGraphicUtilities._(); + + /// A future that completes when any in-flight vector graphic decodes have + /// completed. + /// + /// A vector graphic may require asynchronous work during decoding, for + /// example to decode an image that was embedded in the source graphic. This + /// method may be useful in golden image unit tests. + /// + /// ```dart + /// await tester.pumpWidget(MyWidgetThatHasVectorGraphics()); + /// await tester.runAsync(() => vg.waitForPendingDecodes()); + /// await expect( + /// find.byType(MyWidgetThatHasVectorGraphics), + /// matchesGoldenFile('golden_file.png'), + /// ); + /// ``` + /// + /// Without the `waitForPendingDecodes` call, the golden file would have the + /// placeholder for the [VectorGraphic] widgets, which defaults to a blank + /// sized box. + @visibleForTesting + Future waitForPendingDecodes() { + if (kDebugMode) { + // ignore: invalid_use_of_visible_for_testing_member + return Future.wait(debugGetPendingDecodeTasks); + } + throw UnsupportedError( + 'This method is only for use in tests in debug mode for tests.', + ); + } + + /// Load the [PictureInfo] from a given [loader]. + /// + /// It is the caller's responsibility to handle disposing the picture when + /// they are done with it. + Future loadPicture( + BytesLoader loader, + BuildContext? context, { + bool clipViewbox = true, + VectorGraphicsErrorListener? onError, + }) async { + TextDirection textDirection = TextDirection.ltr; + Locale locale = ui.PlatformDispatcher.instance.locale; + if (context != null) { + locale = Localizations.maybeLocaleOf(context) ?? locale; + textDirection = Directionality.maybeOf(context) ?? textDirection; + } + return loader.loadBytes(context).then((ByteData data) { + try { + return decodeVectorGraphics( + data, + locale: locale, + textDirection: textDirection, + loader: loader, + clipViewbox: clipViewbox, + onError: onError, + ); + } catch (e) { + debugPrint('Failed to decode $loader'); + rethrow; + } + }); + } +} + +/// The [VectorGraphicUtilities] instance. +const VectorGraphicUtilities vg = VectorGraphicUtilities._(); diff --git a/packages/vector_graphics/lib/vector_graphics.dart b/packages/vector_graphics/lib/vector_graphics.dart new file mode 100644 index 00000000000..f49df93d747 --- /dev/null +++ b/packages/vector_graphics/lib/vector_graphics.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/vector_graphics.dart' + show + AssetBytesLoader, + BytesLoader, + NetworkBytesLoader, + PictureInfo, + VectorGraphic, + VectorGraphicUtilities, + vg; diff --git a/packages/vector_graphics/lib/vector_graphics_compat.dart b/packages/vector_graphics/lib/vector_graphics_compat.dart new file mode 100644 index 00000000000..b3efc9f6145 --- /dev/null +++ b/packages/vector_graphics/lib/vector_graphics_compat.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/vector_graphics.dart' + show + AssetBytesLoader, + BytesLoader, + NetworkBytesLoader, + PictureInfo, + RenderingStrategy, + VectorGraphic, + VectorGraphicUtilities, + createCompatVectorGraphic, + vg; diff --git a/packages/vector_graphics/pubspec.yaml b/packages/vector_graphics/pubspec.yaml new file mode 100644 index 00000000000..f37c8386f59 --- /dev/null +++ b/packages/vector_graphics/pubspec.yaml @@ -0,0 +1,36 @@ +name: vector_graphics +description: A vector graphics rendering package for Flutter using a binary encoding. +repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 +# See https://github.com/flutter/flutter/issues/157626 before publishing a new +# version. +version: 1.1.12 + +environment: + sdk: ^3.4.0 + flutter: ">=3.22.0" + +dependencies: + flutter: + sdk: flutter + http: ^1.0.0 + # See https://github.com/flutter/flutter/issues/157626 + vector_graphics_codec: ">=1.1.11+1 <= 1.1.12" + +dev_dependencies: + flutter_test: + sdk: flutter + # See https://github.com/flutter/flutter/issues/157626 + vector_graphics_compiler: ">=1.1.11+1 <= 1.1.12" + +platforms: + android: + ios: + linux: + macos: + web: + windows: + +topics: + - svg + - vector-graphics diff --git a/packages/vector_graphics/test/caching_test.dart b/packages/vector_graphics/test/caching_test.dart new file mode 100644 index 00000000000..0eb242e5a33 --- /dev/null +++ b/packages/vector_graphics/test/caching_test.dart @@ -0,0 +1,343 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/vector_graphics.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +const VectorGraphicsCodec codec = VectorGraphicsCodec(); + +void main() { + setUp(() { + imageCache.clear(); + imageCache.clearLiveImages(); + }); + + testWidgets( + 'Does not reload identical bytes when forced to re-create state object', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget(DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + await tester.pumpWidget(DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys, ['foo.svg']); + }); + + testWidgets('Only loads bytes once for a repeated vg', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundle, + child: Column( + children: [ + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + ], + ), + ), + ); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + await tester.pumpWidget(const SizedBox()); + + await tester.pumpWidget( + DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundle, + child: Column( + children: [ + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + VectorGraphic( + key: GlobalKey(), + loader: const AssetBytesLoader('foo.svg'), + ), + ], + ), + ), + ); + + expect(testBundle.loadKeys, ['foo.svg', 'foo.svg']); + }); + + testWidgets('Does not cache bytes that come from different asset bundles', + (WidgetTester tester) async { + final TestAssetBundle testBundleA = TestAssetBundle(); + final TestAssetBundle testBundleB = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget(DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundleA, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundleA.loadKeys.single, 'foo.svg'); + expect(testBundleB.loadKeys, isEmpty); + + await tester.pumpWidget(DefaultAssetBundle( + key: UniqueKey(), + bundle: testBundleB, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundleA.loadKeys.single, 'foo.svg'); + expect(testBundleB.loadKeys.single, 'foo.svg'); + }); + + testWidgets('reload bytes when locale changes', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + Localizations( + delegates: >[ + TestLocalizationsDelegate() + ], + locale: const Locale('fr', 'CH'), + child: DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + ), + ), + ); + // async localization loading requires extra pump and settle. + await tester.pumpAndSettle(); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + await tester.pumpWidget( + Localizations( + delegates: >[ + TestLocalizationsDelegate() + ], + locale: const Locale('ab', 'cd'), + child: DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + ), + ), + ); + // async localization loading requires extra pump and settle. + await tester.pumpAndSettle(); + + expect(testBundle.loadKeys, ['foo.svg', 'foo.svg']); + }); + + testWidgets('reload bytes when text direction changes', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + ), + ), + ); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + ), + ), + ); + + expect(testBundle.loadKeys, ['foo.svg', 'foo.svg']); + }); + + testWidgets( + 'Cache is purged immediately after last VectorGraphic removed from tree', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + // Force VectorGraphic removed from tree. + await tester.pumpWidget(const SizedBox()); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys, ['foo.svg', 'foo.svg']); + }); + + // For this test we evaluate an edge case where asset loading starts, but then a new + // asset is requested before the first can load. We want to ensure that first asset does + // not populate the cache in such a way that it gets "stuck". + testWidgets('Bytes loading that becomes stale does not populate the cache', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + final ControlledAssetBytesLoader loader = + ControlledAssetBytesLoader('foo.svg'); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: loader, + ), + )); + + expect(testBundle.loadKeys, isEmpty); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('bar.svg'), + ), + )); + + expect(testBundle.loadKeys, ['bar.svg']); + loader.completer.complete(); + await tester.pumpAndSettle(); + + expect(testBundle.loadKeys, ['bar.svg', 'foo.svg']); + + // Even though foo.svg was loaded above, it should have been immediately discarded since + // the vector graphic widget was no longer requesting it. Thus we should see it loaded + // a second time below. + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys, ['bar.svg', 'foo.svg', 'foo.svg']); + }); +} + +class TestAssetBundle extends Fake implements AssetBundle { + final List loadKeys = []; + + @override + Future load(String key) async { + loadKeys.add(key); + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 200); + return buffer.done(); + } +} + +class ControlledAssetBytesLoader extends AssetBytesLoader { + ControlledAssetBytesLoader(super.assetName); + + final Completer completer = Completer(); + + @override + Future loadBytes(BuildContext? context) async { + await completer.future; + return super.loadBytes(context); + } +} + +class TestLocalizationsDelegate + extends LocalizationsDelegate { + @override + bool isSupported(Locale locale) { + return true; + } + + @override + Future load(Locale locale) async { + return TestWidgetsLocalizations(); + } + + @override + bool shouldReload(covariant LocalizationsDelegate old) { + return false; + } +} + +class TestWidgetsLocalizations extends DefaultWidgetsLocalizations { + @override + TextDirection get textDirection => TextDirection.ltr; +} diff --git a/packages/vector_graphics/test/debug_test.dart b/packages/vector_graphics/test/debug_test.dart new file mode 100644 index 00000000000..c9884548a8c --- /dev/null +++ b/packages/vector_graphics/test/debug_test.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/debug.dart'; + +void main() { + test('debugSkipRaster is false by default', () { + expect(debugSkipRaster, false); + }); +} diff --git a/packages/vector_graphics/test/listener_test.dart b/packages/vector_graphics/test/listener_test.dart new file mode 100644 index 00000000000..cabf8051a11 --- /dev/null +++ b/packages/vector_graphics/test/listener_test.dart @@ -0,0 +1,170 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert' show base64; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/listener.dart'; +import 'package:vector_graphics/vector_graphics_compat.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + const String svgString = ''' + + + +'''; + + const String bluePngPixel = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=='; + + late ByteData vectorGraphicBuffer; + + setUpAll(() async { + final Uint8List bytes = encodeSvg( + xml: svgString, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + vectorGraphicBuffer = bytes.buffer.asByteData(); + }); + + setUp(() { + imageCache.clear(); + imageCache.clearLiveImages(); + }); + + test('decode without clip', () async { + final PictureInfo info = await decodeVectorGraphics( + vectorGraphicBuffer, + locale: ui.PlatformDispatcher.instance.locale, + textDirection: ui.TextDirection.ltr, + clipViewbox: true, + loader: const AssetBytesLoader('test'), + ); + final ui.Image image = info.picture.toImageSync(15, 15); + final Uint32List imageBytes = + (await image.toByteData())!.buffer.asUint32List(); + expect(imageBytes.first, 0xFF000000); + expect(imageBytes.last, 0x00000000); + }, skip: kIsWeb); + + test('decode with clip', () async { + final PictureInfo info = await decodeVectorGraphics( + vectorGraphicBuffer, + locale: ui.PlatformDispatcher.instance.locale, + textDirection: ui.TextDirection.ltr, + clipViewbox: false, + loader: const AssetBytesLoader('test'), + ); + final ui.Image image = info.picture.toImageSync(15, 15); + final Uint32List imageBytes = + (await image.toByteData())!.buffer.asUint32List(); + expect(imageBytes.first, 0xFF000000); + expect(imageBytes.last, 0xFF000000); + }, skip: kIsWeb); + + test('Scales image correctly', () async { + final TestPictureFactory factory = TestPictureFactory(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener( + pictureFactory: factory, + ); + listener.onImage(0, 0, base64.decode(bluePngPixel)); + await listener.waitForImageDecode(); + listener.onDrawImage(0, 10, 10, 30, 30, null); + final Invocation drawRect = factory.fakeCanvases.first.invocations.single; + expect(drawRect.isMethod, true); + expect(drawRect.memberName, #drawImageRect); + expect( + drawRect.positionalArguments[1], + const ui.Rect.fromLTRB(0, 0, 1, 1), + ); + expect( + drawRect.positionalArguments[2], + const ui.Rect.fromLTRB(10, 10, 40, 40), + ); + }); + + test('Pattern start clips the new canvas', () async { + final TestPictureFactory factory = TestPictureFactory(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener( + pictureFactory: factory, + ); + listener.onPatternStart(0, 0, 0, 100, 100, Matrix4.identity().storage); + final Invocation clipRect = factory.fakeCanvases.last.invocations.single; + expect(clipRect.isMethod, true); + expect(clipRect.memberName, #clipRect); + expect( + clipRect.positionalArguments.single, + const ui.Rect.fromLTRB(0, 0, 100, 100), + ); + }); + + test('Text position is respected', () async { + final TestPictureFactory factory = TestPictureFactory(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener( + pictureFactory: factory, + ); + listener.onPaintObject( + color: const ui.Color(0xff000000).value, + strokeCap: null, + strokeJoin: null, + blendMode: BlendMode.srcIn.index, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: ui.PaintingStyle.fill.index, + id: 0, + shaderId: null, + ); + listener.onTextPosition(0, 10, 10, null, null, true, null); + listener.onUpdateTextPosition(0); + listener.onTextConfig('foo', null, 0, 0, 16, 0, 0, 0, 0); + await listener.onDrawText(0, 0, null, null); + await listener.onDrawText(0, 0, null, null); + + final Invocation drawParagraph0 = factory.fakeCanvases.last.invocations[0]; + final Invocation drawParagraph1 = factory.fakeCanvases.last.invocations[1]; + + expect(drawParagraph0.memberName, #drawParagraph); + // Only checking the X because Y seems to vary a bit by platform within + // acceptable range. X is what gets managed by the listener anyway. + expect((drawParagraph0.positionalArguments[1] as Offset).dx, 10); + + expect(drawParagraph1.memberName, #drawParagraph); + expect((drawParagraph1.positionalArguments[1] as Offset).dx, 58); + }); +} + +class TestPictureFactory implements PictureFactory { + final List fakeCanvases = []; + @override + ui.Canvas createCanvas(ui.PictureRecorder recorder) { + fakeCanvases.add(FakeCanvas()); + return fakeCanvases.last; + } + + @override + ui.PictureRecorder createPictureRecorder() => FakePictureRecorder(); +} + +class FakePictureRecorder extends Fake implements ui.PictureRecorder {} + +class FakeCanvas implements ui.Canvas { + final List invocations = []; + + @override + dynamic noSuchMethod(Invocation invocation) { + invocations.add(invocation); + } +} diff --git a/packages/vector_graphics/test/render_vector_graphics_test.dart b/packages/vector_graphics/test/render_vector_graphics_test.dart new file mode 100644 index 00000000000..bf5f12889ca --- /dev/null +++ b/packages/vector_graphics/test/render_vector_graphics_test.dart @@ -0,0 +1,560 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This Render Object is not used by the HTML renderer. +@TestOn('!chrome') +library; + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/listener.dart'; +import 'package:vector_graphics/src/render_vector_graphic.dart'; +import 'package:vector_graphics/vector_graphics.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +void main() { + late PictureInfo pictureInfo; + + tearDown(() { + // Since we don't always explicitly dispose render objects in unit tests, manually clear + // the rasters. + debugClearRasteCaches(); + }); + + setUpAll(() async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + const VectorGraphicsCodec().writeSize(buffer, 50, 50); + + pictureInfo = await decodeVectorGraphics( + buffer.done(), + locale: const Locale('fr', 'CH'), + textDirection: TextDirection.ltr, + clipViewbox: true, + loader: TestBytesLoader(Uint8List(0).buffer.asByteData()), + ); + }); + + test('Rasterizes a picture to a draw image call', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + // When the rasterization is finished, it marks self as needing paint. + expect(renderVectorGraphic.debugNeedsPaint, true); + + renderVectorGraphic.paint(context, Offset.zero); + + expect(context.canvas.lastImage, isNotNull); + }); + + test('Multiple render objects with the same scale share a raster', () async { + final RenderVectorGraphic renderVectorGraphicA = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + final RenderVectorGraphic renderVectorGraphicB = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphicA.layout(BoxConstraints.tight(const Size(50, 50))); + renderVectorGraphicB.layout(BoxConstraints.tight(const Size(50, 50))); + final FakeHistoryPaintingContext context = FakeHistoryPaintingContext(); + + renderVectorGraphicA.paint(context, Offset.zero); + renderVectorGraphicB.paint(context, Offset.zero); + + // Same image is recycled. + expect(context.canvas.images, hasLength(2)); + expect(identical(context.canvas.images[0], context.canvas.images[1]), true); + }); + + test('disposing render object release raster', () async { + final RenderVectorGraphic renderVectorGraphicA = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + final RenderVectorGraphic renderVectorGraphicB = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphicA.layout(BoxConstraints.tight(const Size(50, 50))); + final FakeHistoryPaintingContext context = FakeHistoryPaintingContext(); + + renderVectorGraphicA.paint(context, Offset.zero); + + expect(context.canvas.images, hasLength(1)); + renderVectorGraphicA.dispose(); + + renderVectorGraphicB.layout(BoxConstraints.tight(const Size(50, 50))); + + renderVectorGraphicB.paint(context, Offset.zero); + expect(context.canvas.images, hasLength(2)); + expect( + identical(context.canvas.images[0], context.canvas.images[1]), false); + }); + + test( + 'Multiple render objects with the same scale share a raster, different load order', + () async { + final RenderVectorGraphic renderVectorGraphicA = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + final RenderVectorGraphic renderVectorGraphicB = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphicA.layout(BoxConstraints.tight(const Size(50, 50))); + final FakeHistoryPaintingContext context = FakeHistoryPaintingContext(); + + renderVectorGraphicA.paint(context, Offset.zero); + + expect(context.canvas.images, hasLength(1)); + + // Second rasterization immediately paints image. + renderVectorGraphicB.layout(BoxConstraints.tight(const Size(50, 50))); + renderVectorGraphicB.paint(context, Offset.zero); + + expect(context.canvas.images, hasLength(2)); + expect(identical(context.canvas.images[0], context.canvas.images[1]), true); + }); + + test('Changing color filter does not re-rasterize', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + final ui.Image firstImage = context.canvas.lastImage!; + + renderVectorGraphic.colorFilter = + const ui.ColorFilter.mode(Colors.red, ui.BlendMode.colorBurn); + renderVectorGraphic.paint(context, Offset.zero); + + expect(firstImage.debugDisposed, false); + + renderVectorGraphic.paint(context, Offset.zero); + + expect(context.canvas.lastImage, equals(firstImage)); + }); + + test('Changing device pixel ratio does re-rasterize and dispose old raster', + () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + final ui.Image firstImage = context.canvas.lastImage!; + + renderVectorGraphic.devicePixelRatio = 2.0; + renderVectorGraphic.paint(context, Offset.zero); + + expect(firstImage.debugDisposed, true); + + renderVectorGraphic.paint(context, Offset.zero); + + expect(context.canvas.lastImage!.debugDisposed, false); + }); + + test('Changing scale does re-rasterize and dispose old raster', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + final ui.Image firstImage = context.canvas.lastImage!; + + renderVectorGraphic.scale = 2.0; + renderVectorGraphic.paint(context, Offset.zero); + + expect(firstImage.debugDisposed, true); + + renderVectorGraphic.paint(context, Offset.zero); + + expect(context.canvas.lastImage!.debugDisposed, false); + }); + + test('The raster size is increased by the inverse picture scale', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 0.5, // twice as many pixels + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + // Dst rect is always size of RO. + expect(context.canvas.lastDst, const Rect.fromLTWH(0, 0, 50, 50)); + expect( + context.canvas.lastSrc, const Rect.fromLTWH(0, 0, 50 / 0.5, 50 / 0.5)); + }); + + test('The raster size is increased by the device pixel ratio', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 2.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + // Dst rect is always size of RO. + expect(context.canvas.lastDst, const Rect.fromLTWH(0, 0, 50, 50)); + expect(context.canvas.lastSrc, const Rect.fromLTWH(0, 0, 100, 100)); + }); + + test('The raster size is increased by the device pixel ratio and ratio', + () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 2.0, + null, + 0.5, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + // Dst rect is always size of RO. + expect(context.canvas.lastDst, const Rect.fromLTWH(0, 0, 50, 50)); + expect(context.canvas.lastSrc, const Rect.fromLTWH(0, 0, 200, 200)); + }); + + test('Changing size asserts if it is different from the picture size', + () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + // change size. + renderVectorGraphic.layout(BoxConstraints.tight(const Size(1000, 1000))); + + expect(() => renderVectorGraphic.paint(context, Offset.zero), + throwsAssertionError); + }); + + test('Does not rasterize a picture when fully transparent', () async { + final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.0); + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + opacity, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + opacity.value = 1.0; + opacity.notifyListeners(); + + // Changing opacity requires painting. + expect(renderVectorGraphic.debugNeedsPaint, true); + + renderVectorGraphic.paint(context, Offset.zero); + + // Rasterization is now complete. + expect(context.canvas.lastImage, isNotNull); + }); + + test('paints partially opaque picture', () async { + final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.5); + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + opacity, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + expect(context.canvas.lastPaint?.color, const Color.fromRGBO(0, 0, 0, 0.5)); + }); + + test('Disposing render object disposes picture', () async { + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + null, + 1.0, + ); + renderVectorGraphic.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + renderVectorGraphic.paint(context, Offset.zero); + + final ui.Image lastImage = context.canvas.lastImage!; + + renderVectorGraphic.dispose(); + + expect(lastImage.debugDisposed, true); + }); + + test('Removes listeners on detach, dispose, adds then on attach', () async { + final FixedOpacityAnimation opacity = FixedOpacityAnimation(0.5); + final RenderVectorGraphic renderVectorGraphic = RenderVectorGraphic( + pictureInfo, + 'test', + null, + 1.0, + opacity, + 1.0, + ); + final PipelineOwner pipelineOwner = PipelineOwner(); + expect(opacity._listeners, hasLength(1)); + + renderVectorGraphic.attach(pipelineOwner); + expect(opacity._listeners, hasLength(1)); + + renderVectorGraphic.detach(); + expect(opacity._listeners, hasLength(0)); + + renderVectorGraphic.attach(pipelineOwner); + expect(opacity._listeners, hasLength(1)); + + renderVectorGraphic.dispose(); + expect(opacity._listeners, hasLength(0)); + }); + + test('RasterData.dispose is safe to call multiple times', () async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + ui.Canvas(recorder); + final ui.Image image = await recorder.endRecording().toImage(1, 1); + final RasterData data = RasterData(image, 1, const RasterKey('test', 1, 1)); + + data.dispose(); + + expect(data.dispose, returnsNormally); + }); + + test('Color filter applies clip', () async { + final RenderPictureVectorGraphic render = RenderPictureVectorGraphic( + pictureInfo, + const ui.ColorFilter.mode(Colors.green, ui.BlendMode.difference), + null, + ); + render.layout(BoxConstraints.tight(const Size(50, 50))); + final FakePaintingContext context = FakePaintingContext(); + render.paint(context, Offset.zero); + + expect(context.canvas.lastClipRect, + equals(const ui.Rect.fromLTRB(0, 0, 50, 50))); + expect(context.canvas.saveCount, 0); + expect(context.canvas.totalSaves, 1); + expect(context.canvas.totalSaveLayers, 1); + }); +} + +class FakeCanvas extends Fake implements Canvas { + ui.Image? lastImage; + Rect? lastSrc; + Rect? lastDst; + Paint? lastPaint; + Rect? lastClipRect; + int saveCount = 0; + int totalSaves = 0; + int totalSaveLayers = 0; + + @override + void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) { + lastImage = image; + lastSrc = src; + lastDst = dst; + lastPaint = paint; + } + + @override + void drawPicture(ui.Picture picture) {} + + @override + int getSaveCount() { + return saveCount; + } + + @override + void restoreToCount(int count) { + saveCount = count; + } + + @override + void saveLayer(Rect? bounds, Paint paint) { + saveCount++; + totalSaveLayers++; + } + + @override + void save() { + saveCount++; + totalSaves++; + } + + @override + void restore() { + saveCount--; + } + + @override + void clipRect(ui.Rect rect, + {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) { + lastClipRect = rect; + } +} + +class FakeHistoryCanvas extends Fake implements Canvas { + final List images = []; + + @override + void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) { + images.add(image); + } +} + +class FakePaintingContext extends Fake implements PaintingContext { + @override + final FakeCanvas canvas = FakeCanvas(); +} + +class FakeHistoryPaintingContext extends Fake implements PaintingContext { + @override + final FakeHistoryCanvas canvas = FakeHistoryCanvas(); +} + +class FixedOpacityAnimation extends Animation { + FixedOpacityAnimation(this.value); + + final Set _listeners = {}; + + @override + void addListener(ui.VoidCallback listener) { + _listeners.add(listener); + } + + @override + void addStatusListener(AnimationStatusListener listener) { + throw UnsupportedError('addStatusListener'); + } + + @override + void removeListener(ui.VoidCallback listener) { + _listeners.remove(listener); + } + + @override + void removeStatusListener(AnimationStatusListener listener) { + throw UnsupportedError('removeStatusListener'); + } + + @override + AnimationStatus get status => AnimationStatus.forward; + + @override + double value = 1.0; + + void notifyListeners() { + for (final ui.VoidCallback listener in _listeners) { + listener(); + } + } +} + +class TestBytesLoader extends BytesLoader { + const TestBytesLoader(this.data); + + final ByteData data; + + @override + Future loadBytes(BuildContext? context) async { + return data; + } + + @override + int get hashCode => data.hashCode; + + @override + bool operator ==(Object other) { + return other is TestBytesLoader && other.data == data; + } +} diff --git a/packages/vector_graphics/test/vector_graphics_test.dart b/packages/vector_graphics/test/vector_graphics_test.dart new file mode 100644 index 00000000000..33da9600af7 --- /dev/null +++ b/packages/vector_graphics/test/vector_graphics_test.dart @@ -0,0 +1,721 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert' show base64Decode; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/src/listener.dart'; +import 'package:vector_graphics/src/vector_graphics.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +const VectorGraphicsCodec codec = VectorGraphicsCodec(); + +void main() { + setUp(() { + imageCache.clear(); + imageCache.clearLiveImages(); + }); + + test('Can decode a message without a stroke and vertices', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + final int paintId = codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0); + codec.writeDrawVertices( + buffer, + Float32List.fromList([ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ]), + null, + paintId); + + codec.decode(buffer.done(), listener); + + expect(listener.toPicture, returnsNormally); + }); + + test('Can decode a message with a fill and path', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + final int paintId = codec.writeFill(buffer, 23, 0); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.moveTo, + ControlPointTypes.lineTo, + ControlPointTypes.close, + ]), + Float32List.fromList([ + 1, + 2, + 2, + 3, + ]), + 0, + ); + codec.writeDrawPath(buffer, pathId, paintId, null); + + codec.decode(buffer.done(), listener); + + expect(listener.toPicture, returnsNormally); + }); + + test('Asserts if toPicture is called more than once', () { + final FlutterVectorGraphicsListener listener = + FlutterVectorGraphicsListener(); + listener.toPicture(); + + expect(listener.toPicture, throwsAssertionError); + }); + + testWidgets( + 'Creates layout widgets when VectorGraphic is sized (0x0 graphic)', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + width: 100, + height: 100, + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + find.byType(SizedBox).evaluate().first.widget as SizedBox; + + expect(sizedBox.width, 100); + expect(sizedBox.height, 100); + }); + + testWidgets('Creates layout widgets when VectorGraphic is sized (1:1 ratio)', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + const VectorGraphicsCodec().writeSize(buffer, 50, 50); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + width: 100, + height: 100, + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + find.byType(SizedBox).evaluate().first.widget as SizedBox; + + expect(sizedBox.width, 100); + expect(sizedBox.height, 100); + }); + + testWidgets('Creates layout widgets when VectorGraphic is sized (3:5 ratio)', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + const VectorGraphicsCodec().writeSize(buffer, 30, 50); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + width: 100, + height: 100, + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + find.byType(SizedBox).evaluate().first.widget as SizedBox; + + expect(sizedBox.width, 60); + expect(sizedBox.height, 100); + }); + + testWidgets('Creates alignment widgets when VectorGraphic is aligned', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + alignment: Alignment.centerLeft, + fit: BoxFit.fitHeight, + )); + await tester.pumpAndSettle(); + + expect(find.byType(FittedBox), findsOneWidget); + + final FittedBox fittedBox = + find.byType(FittedBox).evaluate().first.widget as FittedBox; + + expect(fittedBox.fit, BoxFit.fitHeight); + expect(fittedBox.alignment, Alignment.centerLeft); + expect(fittedBox.clipBehavior, Clip.hardEdge); + }); + + group('ClipBehavior', () { + testWidgets('Sets clipBehavior to hardEdge if not provided', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + )); + await tester.pumpAndSettle(); + + expect(find.byType(FittedBox), findsOneWidget); + + final FittedBox fittedBox = + find.byType(FittedBox).evaluate().first.widget as FittedBox; + + expect(fittedBox.clipBehavior, Clip.hardEdge); + }); + + testWidgets('Passes clipBehavior to FittedBox if provided', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + clipBehavior: Clip.none, + )); + await tester.pumpAndSettle(); + + expect(find.byType(FittedBox), findsOneWidget); + + final FittedBox fittedBox = + find.byType(FittedBox).evaluate().first.widget as FittedBox; + + expect(fittedBox.clipBehavior, Clip.none); + }); + }); + + testWidgets('Sizes VectorGraphic based on encoded viewbox information', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 200); + + await tester.pumpWidget(VectorGraphic( + loader: TestBytesLoader(buffer.done()), + )); + await tester.pumpAndSettle(); + + expect(find.byType(SizedBox), findsNWidgets(2)); + + final SizedBox sizedBox = + find.byType(SizedBox).evaluate().last.widget as SizedBox; + + expect(sizedBox.width, 100); + expect(sizedBox.height, 200); + }); + + testWidgets('Reloads bytes when configuration changes', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('foo.svg'), + ), + )); + + expect(testBundle.loadKeys.single, 'foo.svg'); + + await tester.pumpWidget(DefaultAssetBundle( + bundle: testBundle, + child: VectorGraphic( + key: key, + loader: const AssetBytesLoader('bar.svg'), + ), + )); + + expect(testBundle.loadKeys, ['foo.svg', 'bar.svg']); + }); + + testWidgets('Can update SVG picture', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.layers, contains(isA())); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('bar.svg'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.layers, contains(isA())); + }); + + testWidgets('Can set locale and text direction', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + await tester.pumpWidget( + Localizations( + delegates: const >[ + DefaultWidgetsLocalizations.delegate + ], + locale: const Locale('fr', 'CH'), + child: Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('bar.svg'), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(debugLastLocale, const Locale('fr', 'CH')); + expect(debugLastTextDirection, TextDirection.rtl); + + await tester.pumpWidget( + Localizations( + delegates: const >[ + DefaultWidgetsLocalizations.delegate + ], + locale: const Locale('ab', 'AB'), + child: Directionality( + textDirection: TextDirection.ltr, + child: DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('bar.svg'), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(debugLastLocale, const Locale('ab', 'AB')); + expect(debugLastTextDirection, TextDirection.ltr); + }); + + testWidgets('Can exclude from semantics', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + excludeFromSemantics: true, + semanticsLabel: 'Foo', + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.bySemanticsLabel('Foo'), findsNothing); + }); + + testWidgets('Can add semantic label', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: const Directionality( + textDirection: TextDirection.ltr, + child: VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + semanticsLabel: 'Foo', + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + tester.getSemantics(find.bySemanticsLabel('Foo')), + matchesSemantics( + label: 'Foo', + isImage: true, + ), + ); + }); + + testWidgets('Default placeholder builder', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: const Directionality( + textDirection: TextDirection.ltr, + child: VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + semanticsLabel: 'Foo', + ), + ), + ), + ); + + expect(find.byType(SizedBox), findsOneWidget); + }); + + testWidgets('Custom placeholder builder', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: Directionality( + textDirection: TextDirection.ltr, + child: VectorGraphic( + loader: const AssetBytesLoader('foo.svg'), + semanticsLabel: 'Foo', + placeholderBuilder: (BuildContext context) { + return Container(key: const ValueKey(23)); + }, + ), + ), + ), + ); + + expect(find.byKey(const ValueKey(23)), findsOneWidget); + }); + + testWidgets('Does not call setState after unmounting', + (WidgetTester tester) async { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 200); + final Completer completer = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: VectorGraphic( + loader: DelayedBytesLoader(completer.future), + ), + ), + ); + await tester.pumpWidget(const Placeholder()); + completer.complete(buffer.done()); + }); + + testWidgets('Loads a picture with loadPicture', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final Completer completer = Completer(); + await tester.pumpWidget( + Localizations( + delegates: const >[ + DefaultWidgetsLocalizations.delegate + ], + locale: const Locale('fr', 'CH'), + child: Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: Builder(builder: (BuildContext context) { + vg + .loadPicture(const AssetBytesLoader('foo.svg'), context) + .then(completer.complete); + return const Center(); + }), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(await completer.future, isA()); + expect(debugLastLocale, const Locale('fr', 'CH')); + expect(debugLastTextDirection, TextDirection.rtl); + }); + + testWidgets('Loads a picture with loadPicture and null build context', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + final Completer completer = Completer(); + await tester.pumpWidget( + Localizations( + delegates: const >[ + DefaultWidgetsLocalizations.delegate + ], + locale: const Locale('fr', 'CH'), + child: Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: Builder(builder: (BuildContext context) { + vg + .loadPicture( + AssetBytesLoader('foo.svg', assetBundle: testBundle), + null) + .then(completer.complete); + return const Center(); + }), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(await completer.future, isA()); + expect(debugLastLocale, PlatformDispatcher.instance.locale); + expect(debugLastTextDirection, TextDirection.ltr); + }); + + testWidgets('Throws a helpful exception if decoding fails', + (WidgetTester tester) async { + final Uint8List data = Uint8List(256); + final TestBytesLoader loader = TestBytesLoader( + data.buffer.asByteData(), + '/foo/bar/whatever.vec', + ); + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(Placeholder(key: key)); + + late final VectorGraphicsDecodeException exception; + try { + await vg.loadPicture(loader, key.currentContext); + } on VectorGraphicsDecodeException catch (e) { + exception = e; + } + + expect(exception.source, loader); + expect(exception.originalException, isA()); + expect(exception.toString(), contains(loader.toString())); + }); + + testWidgets( + 'Construct vector graphic with drawPicture strategy', + (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + DefaultAssetBundle( + bundle: testBundle, + child: Directionality( + textDirection: TextDirection.ltr, + child: createCompatVectorGraphic( + loader: const AssetBytesLoader('foo.svg'), + colorFilter: const ColorFilter.mode(Colors.red, BlendMode.srcIn), + opacity: const AlwaysStoppedAnimation(0.5), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.layers.last, isA()); + // Opacity and color filter are drawn as savelayer + expect(tester.layers, isNot(contains(isA()))); + expect(tester.layers, isNot(contains(isA()))); + }, + skip: kIsWeb, + ); // picture rasterization works differently on HTML due to saveLayer bugs in HTML backend + + testWidgets('Can render VG with image', (WidgetTester tester) async { + const String bluePngPixel = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=='; + + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + codec.writeSize(buffer, 100, 100); + + codec.writeDrawImage( + buffer, + codec.writeImage(buffer, 0, base64Decode(bluePngPixel)), + 0, + 0, + 100, + 100, + null, + ); + final UniqueKey key = UniqueKey(); + final TestBytesLoader loader = TestBytesLoader(buffer.done()); + // See listener.dart. + final int imageKey = Object.hash(loader.hashCode, 0, 0); + + expect(imageCache.currentSize, 0); + expect(imageCache.statusForKey(imageKey).untracked, true); + + await tester.pumpWidget(RepaintBoundary( + key: key, + child: VectorGraphic( + loader: loader, + width: 100, + height: 100, + ), + )); + + expect(imageCache.currentSize, 0); + expect(imageCache.statusForKey(imageKey).pending, true); + + // A blank image, because the image hasn't loaded yet. + await expectLater( + find.byKey(key), + matchesGoldenFile('vg_with_image_blank.png'), + ); + + expect(imageCache.currentSize, 1); + expect(imageCache.statusForKey(imageKey).live, false); + expect(imageCache.statusForKey(imageKey).keepAlive, true); + + await tester.runAsync(() => vg.waitForPendingDecodes()); + await tester.pump(); + + expect(imageCache.currentSize, 1); + expect(imageCache.statusForKey(imageKey).live, false); + expect(imageCache.statusForKey(imageKey).keepAlive, true); + + // A blue square, becuase the image is available now. + await expectLater( + find.byKey(key), + matchesGoldenFile('vg_with_image_blue.png'), + ); + }, skip: kIsWeb); + + test('AssetBytesLoader respects packages', () async { + final TestBundle bundle = TestBundle({ + 'foo': Uint8List(0).buffer.asByteData(), + 'packages/packageName/foo': Uint8List(1).buffer.asByteData(), + }); + final AssetBytesLoader loader = + AssetBytesLoader('foo', assetBundle: bundle); + final AssetBytesLoader packageLoader = AssetBytesLoader('foo', + assetBundle: bundle, packageName: 'packageName'); + expect((await loader.loadBytes(null)).lengthInBytes, 0); + expect((await packageLoader.loadBytes(null)).lengthInBytes, 1); + }); + + testWidgets('Respects text direction', (WidgetTester tester) async { + final TestAssetBundle testBundle = TestAssetBundle(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(Transform), findsNothing); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.rtl, + child: DefaultAssetBundle( + bundle: testBundle, + child: const VectorGraphic( + loader: AssetBytesLoader('foo.svg'), + matchTextDirection: true, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final Matrix4 matrix = Matrix4.identity(); + final RenderObject transformObject = + find.byType(Transform).evaluate().first.renderObject!; + bool visited = false; + transformObject.visitChildren((RenderObject child) { + if (!visited) { + transformObject.applyPaintTransform(child, matrix); + } + visited = true; + }); + expect(visited, true); + expect(matrix.getTranslation().x, + 100); // Width specified in the TestAssetBundle. + expect(matrix.getTranslation().y, 0); + expect(matrix.row0.x, -1); + expect(matrix.row1.y, 1); + }); +} + +class TestBundle extends Fake implements AssetBundle { + TestBundle(this.map); + + final Map map; + + @override + Future load(String key) async { + return map[key]!; + } +} + +class TestAssetBundle extends Fake implements AssetBundle { + final List loadKeys = []; + + @override + Future load(String key) async { + loadKeys.add(key); + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + codec.writeSize(buffer, 100, 200); + return buffer.done(); + } +} + +class DelayedBytesLoader extends BytesLoader { + const DelayedBytesLoader(this.data); + + final Future data; + + @override + Future loadBytes(BuildContext? context) async { + return data; + } + + @override + int get hashCode => data.hashCode; + + @override + bool operator ==(Object other) { + return other is DelayedBytesLoader && other.data == data; + } +} + +class TestBytesLoader extends BytesLoader { + const TestBytesLoader(this.data, [this.source]); + + final ByteData data; + final String? source; + + @override + Future loadBytes(BuildContext? context) async { + return data; + } + + @override + int get hashCode => data.hashCode; + + @override + bool operator ==(Object other) { + return other is TestBytesLoader && other.data == data; + } + + @override + String toString() => 'TestBytesLoader: $source'; +} diff --git a/packages/vector_graphics/test/vg_with_image_blank.png b/packages/vector_graphics/test/vg_with_image_blank.png new file mode 100644 index 00000000000..6a97f2a7c43 Binary files /dev/null and b/packages/vector_graphics/test/vg_with_image_blank.png differ diff --git a/packages/vector_graphics/test/vg_with_image_blue.png b/packages/vector_graphics/test/vg_with_image_blue.png new file mode 100644 index 00000000000..455e97b9f65 Binary files /dev/null and b/packages/vector_graphics/test/vg_with_image_blue.png differ diff --git a/packages/vector_graphics_codec/.gitignore b/packages/vector_graphics_codec/.gitignore new file mode 100644 index 00000000000..3c8a157278c --- /dev/null +++ b/packages/vector_graphics_codec/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/packages/vector_graphics_codec/AUTHORS b/packages/vector_graphics_codec/AUTHORS new file mode 100644 index 00000000000..557dff97933 --- /dev/null +++ b/packages/vector_graphics_codec/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/vector_graphics_codec/CHANGELOG.md b/packages/vector_graphics_codec/CHANGELOG.md new file mode 100644 index 00000000000..a4781180d58 --- /dev/null +++ b/packages/vector_graphics_codec/CHANGELOG.md @@ -0,0 +1,119 @@ +## 1.1.12 + +* Transfers the package source from https://github.com/dnfield/vector_graphics + to https://github.com/flutter/packages. + +## 1.1.11+1 + +* Relax package:http constraint. + +## 1.1.11 + +* Use package:http to drop dependency on dart:html. + +## 1.1.10+1 + +* Add missing save before clip. + +## 1.1.10 + +* Add missing clip before saveLayer. + +## 1.1.9+2 + +* Fix case sensitivity on scientific notation parsing. + +## 1.1.9+1 + +* Fix publication error that did not have latest source code. + +## 1.1.9 + +* Fix handling of invalid XML `@id` attributes. +* Fix handling of self-referential `` elements. +* Add `--out-dir` option to compiler. +* Tweak warning message for unhandled eleemnts. + +## 1.1.8 + +* Fix bugs in transform parsing. + +## 1.1.7 + +* Support for matching the ambient text direction. + +## 1.1.6 + +* Fix bug in text position computation when transforms are involved. + +## 1.1.5+1 + +* Remove/update some invalid assertions related to image formats. + +## 1.1.5 + +* Add support for encoding control points as IEEE 754-2008 half precision + floating point values. +* Increase minimum SDK to 2.17.0. +* Added an error builder property to provide a fallback widget on exceptions. + +## 1.1.4 + +* Support more image formats and malformed MIME types. +* Fix inheritence for `fill-rule`s. + +## 1.1.3 + +* Further improvements to whitespace handling for text. + +## 1.1.2 + +* Fix handling and inheritence of `none`. + +## 1.1.1 + +* Multiple text positioning bug fixes. +* Preserve stroke-opacity when specified. + +## 1.1.0 + +* Fix a number of inheritence related bugs: + * Inheritence of properties specified on the root element now work. + * Opacity inheritence is more correct now. + * Inheritence of `use` elements is more correctly handled. +* Make `currentColor` non-null on SVG theme, and fix how it is applied. +* Remove the opacity peephole optimizer, which was incorrectly applying + optimizations in a few cases. A future release may add this back. +* Add clipBehavior to the widget. +* Fix patterns when multiple patterns are specified and applied within the + graphic. + +## 1.0.1 + +* Fix handling of unspecified fill colors on use/group elements. + +## 1.0.0+1 + +* Fix issue in pattern decoding. +* Fix issue in matrix parsing for some combinations of matrices. + +## 1.0.0 + +* Initial stable release. + +## 0.0.3 + +* Pattern support. + +## 0.0.2 + +* Add support for encoding and decoding inline images. + +## 0.0.1 + +* Add [VectorGraphicsCodec], [VectorGraphicsCodecListener], and [VectorGraphicsBuffer] + types used to construct and decode a vector graphics binary asset. + +## 0.0.0 + +* Create repository. diff --git a/packages/vector_graphics_codec/LICENSE b/packages/vector_graphics_codec/LICENSE new file mode 100644 index 00000000000..c6823b81eb8 --- /dev/null +++ b/packages/vector_graphics_codec/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/vector_graphics_codec/README.md b/packages/vector_graphics_codec/README.md new file mode 100644 index 00000000000..64beff82f54 --- /dev/null +++ b/packages/vector_graphics_codec/README.md @@ -0,0 +1,19 @@ +# vector_graphics_codec + +An encoding library for `package:vector_graphics`. + +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. + +## Commemoration + +This package was originally authored by +[Dan Field](https://github.com/dnfield) and has been forked here +from [dnfield/vector_graphics](https://github.com/dnfield/vector_graphics). +Dan was a member of the Flutter team at Google from 2018 until his death +in 2024. Dan’s impact and contributions to Flutter were immeasurable, and we +honor his memory by continuing to publish and maintain this package. diff --git a/packages/vector_graphics_codec/lib/src/fp16.dart b/packages/vector_graphics_codec/lib/src/fp16.dart new file mode 100644 index 00000000000..86301c7e9d8 --- /dev/null +++ b/packages/vector_graphics_codec/lib/src/fp16.dart @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(stuartmorgan): Fix the lack of documentation, and remove the +// public_member_api_docs ignore directive. See +// https://github.com/flutter/flutter/issues/157616 +// ignore_for_file: constant_identifier_names, public_member_api_docs + +/// Adapted from libcore/util/FP16.java from the Android SDK. +/// https://en.wikipedia.org/wiki/Half-precision_floating-point_format +library fp16; + +import 'dart:typed_data'; + +const int FP32_SIGN_SHIFT = 31; +const int FP32_EXPONENT_SHIFT = 23; +const int FP32_SHIFTED_EXPONENT_MASK = 0xff; +const int FP32_SIGNIFICAND_MASK = 0x7fffff; +const int FP32_EXPONENT_BIAS = 127; +const int FP32_QNAN_MASK = 0x400000; +const int FP32_DENORMAL_MAGIC = 126 << 23; +const int EXPONENT_BIAS = 15; +const int SIGN_SHIFT = 15; +const int EXPONENT_SHIFT = 10; +const int SIGN_MASK = 0x8000; +const int SHIFTED_EXPONENT_MASK = 0x1f; +const int SIGNIFICAND_MASK = 0x3ff; + +// ignore: non_constant_identifier_names +final ByteData FP32_DENORMAL_FLOAT = ByteData(4) + ..setUint32(0, FP32_DENORMAL_MAGIC); + +/// Convert the single precision floating point value stored in [byteData] into a half-precision floating point value. +/// +/// This value is stored in the same bytedata instance. +void toHalf(ByteData byteData) { + final int bits = byteData.getInt32(0); + final int s = bits >> FP32_SIGN_SHIFT; + int e = (bits >> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK; + int m = bits & FP32_SIGNIFICAND_MASK; + int outE = 0; + int outM = 0; + + if (e == 0xff) { + // Infinite or NaN + outE = 0x1f; + outM = m != 0 ? 0x200 : 0; + } else { + e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS; + if (e >= 0x1f) { + // Overflow + outE = 0x1f; + } else if (e <= 0) { + // Underflow + if (e < -10) { + // The absolute fp32 value is less than MIN_VALUE, flush to +/-0 + } else { + // The fp32 value is a normalized float less than MIN_NORMAL, + // we convert to a denorm fp16 + m = m | 0x800000; + final int shift = 14 - e; + outM = m >> shift; + final int lowm = m & ((1 << shift) - 1); + final int hway = 1 << (shift - 1); + // if above halfway or exactly halfway and outM is odd + if (lowm + (outM & 1) > hway) { + // Round to nearest even + // Can overflow into exponent bit, which surprisingly is OK. + // This increment relies on the +outM in the return statement below + outM++; + } + } + } else { + outE = e; + outM = m >> 13; + // if above halfway or exactly halfway and outM is odd + if ((m & 0x1fff) + (outM & 0x1) > 0x1000) { + // Round to nearest even + // Can overflow into exponent bit, which surprisingly is OK. + // This increment relies on the +outM in the return statement below + outM++; + } + } + } + // The outM is added here as the +1 increments for outM above can + // cause an overflow in the exponent bit which is OK. + byteData.setUint16(0, (s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM); +} + +/// Convert the single precision floating point value stored in [byteData] into a double +/// precision floating point value. +double toDouble(ByteData byteData) { + final int h = byteData.getUint16(0); + final int bits = h & 0xffff; + final int s = bits & SIGN_MASK; + final int e = (bits >> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK; + final int m = bits & SIGNIFICAND_MASK; + int outE = 0; + int outM = 0; + if (e == 0) { + // Denormal or 0 + if (m != 0) { + // Convert denorm fp16 into normalized fp32 + byteData.setUint32(0, FP32_DENORMAL_MAGIC + m); + double o = byteData.getFloat32(0); + o -= FP32_DENORMAL_FLOAT.getFloat32(0); + return s == 0 ? o : -o; + } + } else { + outM = m << 13; + if (e == 0x1f) { + // Infinite or NaN + outE = 0xff; + if (outM != 0) { + // SNaNs are quieted + outM |= FP32_QNAN_MASK; + } + } else { + outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS; + } + } + final int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM; + byteData.setUint32(0, out); + return byteData.getFloat32(0); +} diff --git a/packages/vector_graphics_codec/lib/vector_graphics_codec.dart b/packages/vector_graphics_codec/lib/vector_graphics_codec.dart new file mode 100644 index 00000000000..5888860a419 --- /dev/null +++ b/packages/vector_graphics_codec/lib/vector_graphics_codec.dart @@ -0,0 +1,1506 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'src/fp16.dart' as fp16; + +// TODO(stuartmorgan): Fix the lack of documentation, and remove this. See +// https://github.com/flutter/flutter/issues/157616 +// ignore_for_file: public_member_api_docs + +/// enumeration of the types of control points accepted by [VectorGraphicsCodec.writePath]. +abstract class ControlPointTypes { + const ControlPointTypes._(); + + static const int moveTo = 0; + static const int lineTo = 1; + static const int cubicTo = 2; + static const int close = 3; +} + +// See definitions in dart:ui's TextDecoration. + +/// The mask used to clear text decorations. +const int kNoTextDecorationMask = 0x0; + +/// The mask for an underline text decoration. +const int kUnderlineMask = 0x1; + +/// The mask constant for an overline text decoration. +const int kOverlineMask = 0x2; + +/// The mask constant for a line through or strike text decoration. +const int kLineThroughMask = 0x4; + +/// The signature for an error callback if an error occurs during image +/// decoding. +/// +/// See [VectorGraphicsCodecListener.onImage]. +typedef VectorGraphicsErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + +/// Enumeration of the types of image data accepted by [VectorGraphicsCodec.writeImage]. +/// +// Must match ImageFormat from vector_graphics_compiler. +abstract class ImageFormatTypes { + /// PNG format. + /// + /// A loss-less compression format for images. This format is well suited for + /// images with hard edges, such as screenshots or sprites, and images with + /// text. Transparency is supported. The PNG format supports images up to + /// 2,147,483,647 pixels in either dimension, though in practice available + /// memory provides a more immediate limitation on maximum image size. + /// + /// PNG images normally use the `.png` file extension and the `image/png` MIME + /// type. + /// + /// See also: + /// + /// * , the Wikipedia page on PNG. + /// * , the PNG standard. + static const int png = 0; + + /// A JPEG format image. + /// + /// This library does not support JPEG 2000. + static const int jpeg = 1; + + /// A WebP format image. + static const int webp = 2; + + /// A Graphics Interchange Format image. + static const int gif = 3; + + /// A Windows Bitmap format image. + static const int bmp = 4; + + static const List values = [png, jpeg, webp, gif, bmp]; +} + +class DecodeResponse { + // TODO(stuartmorgan): Fix this use of a private type in public API (likely + // the constructor should be private). + // ignore: library_private_types_in_public_api + const DecodeResponse(this.complete, this._buffer); + + final bool complete; + final _ReadBuffer? _buffer; +} + +/// The [VectorGraphicsCodec] provides support for both encoding and +/// decoding the vector_graphics binary format. +class VectorGraphicsCodec { + /// Create a new [VectorGraphicsCodec]. + /// + /// The codec is stateless and the const constructor should be preferred. + const VectorGraphicsCodec(); + + /// The maximum supported value for an id. + /// + /// The codec does not support encoding more than this many paths, paints, + /// or shaders in a single buffer. + /// + /// Vertices are written inline and not subject to this constraint. + static const int kMaxId = 65535; + + static const int _pathTag = 27; + static const int _fillPaintTag = 28; + static const int _strokePaintTag = 29; + static const int _drawPathTag = 30; + static const int _drawVerticesTag = 31; + static const int _saveLayerTag = 37; + static const int _restoreTag = 38; + static const int _linearGradientTag = 39; + static const int _radialGradientTag = 40; + static const int _sizeTag = 41; + static const int _clipPathTag = 42; + static const int _maskTag = 43; + static const int _drawTextTag = 44; + static const int _textConfigTag = 45; + static const int _imageConfigTag = 46; + static const int _drawImageTag = 47; + static const int _beginCommandsTag = 48; + static const int _patternTag = 49; + static const int _textPositionTag = 50; + static const int _updateTextPositionTag = 51; + static const int _pathTagHalfPrecision = 52; + + static const int _version = 1; + static const int _magicNumber = 0x00882d62; + + /// Decode the vector_graphics binary. + /// + /// 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]. + /// + /// Throws a [StateError] If the message is invalid. + DecodeResponse decode(ByteData data, VectorGraphicsCodecListener? listener, + {DecodeResponse? response}) { + final _ReadBuffer buffer; + if (response == null) { + buffer = _ReadBuffer(data); + if (data.lengthInBytes < 5) { + throw StateError( + 'The provided data was not a vector_graphics binary asset.'); + } + final int magicNumber = buffer.getUint32(); + if (magicNumber != _magicNumber) { + throw StateError( + 'The provided data was not a vector_graphics binary asset.'); + } + final int version = buffer.getUint8(); + if (version != _version) { + throw StateError( + 'The provided data does not match the currently supported version.'); + } + } else { + buffer = response._buffer!; + } + + bool readImage = false; + while (buffer.hasRemaining) { + final int type = buffer.getUint8(); + switch (type) { + case _beginCommandsTag: + if (readImage) { + return DecodeResponse(false, buffer); + } + continue; + case _linearGradientTag: + _readLinearGradient(buffer, listener); + continue; + case _radialGradientTag: + _readRadialGradient(buffer, listener); + continue; + case _fillPaintTag: + _readFillPaint(buffer, listener); + continue; + case _strokePaintTag: + _readStrokePaint(buffer, listener); + continue; + case _pathTag: + _readPath(buffer, listener, half: false); + continue; + case _pathTagHalfPrecision: + _readPath(buffer, listener, half: true); + continue; + case _drawPathTag: + _readDrawPath(buffer, listener); + continue; + case _drawVerticesTag: + _readDrawVertices(buffer, listener); + continue; + case _restoreTag: + listener?.onRestoreLayer(); + continue; + case _saveLayerTag: + _readSaveLayer(buffer, listener); + continue; + case _sizeTag: + _readSize(buffer, listener); + continue; + case _clipPathTag: + _readClipPath(buffer, listener); + continue; + case _maskTag: + listener?.onMask(); + continue; + case _textConfigTag: + _readTextConfig(buffer, listener); + continue; + case _drawTextTag: + _readDrawText(buffer, listener); + continue; + case _imageConfigTag: + readImage = true; + _readImageConfig(buffer, listener); + continue; + case _drawImageTag: + _readDrawImage(buffer, listener); + continue; + case _patternTag: + _readPattern(buffer, listener); + continue; + case _textPositionTag: + _readTextPosition(buffer, listener); + continue; + case _updateTextPositionTag: + _readUpdateTextPosition(buffer, listener); + continue; + default: + throw StateError('Unknown type tag $type'); + } + } + return const DecodeResponse(true, null); + } + + /// Encode the dimensions of the vector graphic. + /// + /// This should be the first attribute encoded. + void writeSize( + VectorGraphicsBuffer buffer, + double width, + double height, + ) { + if (buffer._decodePhase.index != _CurrentSection.size.index) { + throw StateError('Size already written'); + } + buffer._decodePhase = _CurrentSection.images; + buffer._putUint8(_sizeTag); + buffer._putFloat32(width); + buffer._putFloat32(height); + } + + /// Encode a draw path command in the current buffer. + /// + /// Requires that [pathId] and [paintId] to already be encoded. + void writeDrawPath( + VectorGraphicsBuffer buffer, + int pathId, + int paintId, + int? patternId, + ) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + + buffer._putUint8(_drawPathTag); + buffer._putUint16(pathId); + buffer._putUint16(paintId); + buffer._putUint16(patternId ?? kMaxId); + } + + /// Encode a draw vertices command in the current buffer. + /// + /// The [indices] are the index buffer used and is optional. + void writeDrawVertices( + VectorGraphicsBuffer buffer, + Float32List vertices, + Uint16List? indices, + int? paintId, + ) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + + // Type Tag + // Vertex Length + // Vertex Buffer + // Index Length + // Index Buffer (If non zero) + // Paint Id. + buffer._putUint8(_drawVerticesTag); + buffer._putUint16(paintId ?? kMaxId); + buffer._putUint16(vertices.length); + buffer._putFloat32List(vertices); + if (indices != null) { + buffer._putUint16(indices.length); + buffer._putUint16List(indices); + } else { + buffer._putUint16(0); + } + } + + /// Encode a paint object used for a fill in the current buffer, returning + /// the identifier assigned to it. + /// + /// + /// [color] is the 32-bit ARBG color representation used by Flutter + /// internally. The [blendMode] fields should be the index of the + /// corresponding enumeration. + /// + /// This method is only used to write the paint used for fill commands. + /// To write a paint used for a stroke command, see [writeStroke]. + int writeFill( + VectorGraphicsBuffer buffer, + int color, + int blendMode, [ + int? shaderId, + ]) { + buffer._checkPhase(_CurrentSection.paints); + + final int paintId = buffer._nextPaintId++; + assert(paintId < kMaxId); + buffer._putUint8(_fillPaintTag); + buffer._putUint32(color); + buffer._putUint8(blendMode); + buffer._putUint16(paintId); + buffer._putUint16(shaderId ?? kMaxId); + return paintId; + } + + /// Write a linear gradient into the current buffer. + int writeLinearGradient( + VectorGraphicsBuffer buffer, { + required double fromX, + required double fromY, + required double toX, + required double toY, + required Int32List colors, + required Float32List? offsets, + required int tileMode, + }) { + buffer._checkPhase(_CurrentSection.shaders); + + final int shaderId = buffer._nextShaderId++; + assert(shaderId < kMaxId); + buffer._putUint8(_linearGradientTag); + buffer._putUint16(shaderId); + buffer._putFloat32(fromX); + buffer._putFloat32(fromY); + buffer._putFloat32(toX); + buffer._putFloat32(toY); + buffer._putUint16(colors.length); + buffer._putInt32List(colors); + if (offsets == null) { + buffer._putUint16(0); + } else { + buffer._putUint16(offsets.length); + buffer._putFloat32List(offsets); + } + buffer._putUint8(tileMode); + return shaderId; + } + + /// Write a radial gradient into the current buffer. + /// + /// [focalX] and [focalY] must be either both `null` or both `non-null`. + int writeRadialGradient( + VectorGraphicsBuffer buffer, { + required double centerX, + required double centerY, + required double radius, + required double? focalX, + required double? focalY, + required Int32List colors, + required Float32List? offsets, + required Float64List? transform, + required int tileMode, + }) { + assert((focalX == null && focalY == null) || + (focalX != null && focalY != null)); + assert(transform == null || transform.length == 16); + buffer._checkPhase(_CurrentSection.shaders); + + final int shaderId = buffer._nextShaderId++; + assert(shaderId < kMaxId); + buffer._putUint8(_radialGradientTag); + buffer._putUint16(shaderId); + buffer._putFloat32(centerX); + buffer._putFloat32(centerY); + buffer._putFloat32(radius); + + if (focalX != null) { + buffer._putUint8(1); + buffer._putFloat32(focalX); + buffer._putFloat32(focalY!); + } else { + buffer._putUint8(0); + } + buffer._putUint16(colors.length); + buffer._putInt32List(colors); + if (offsets != null) { + buffer._putUint16(offsets.length); + buffer._putFloat32List(offsets); + } else { + buffer._putUint16(0); + } + buffer._writeTransform(transform); + buffer._putUint8(tileMode); + return shaderId; + } + + /// Encode a paint object in the current buffer, returning the identifier + /// assigned to it. + /// + /// [color] is the 32-bit ARBG color representation used by Flutter + /// internally. The [strokeCap], [strokeJoin], [blendMode], [style] + /// fields should be the index of the corresponding enumeration. + /// + /// This method is only used to write the paint used for fill commands. + /// To write a paint used for a stroke command, see [writeStroke]. + int writeStroke( + VectorGraphicsBuffer buffer, + int color, + int strokeCap, + int strokeJoin, + int blendMode, + double strokeMiterLimit, + double strokeWidth, [ + int? shaderId, + ]) { + buffer._checkPhase(_CurrentSection.paints); + final int paintId = buffer._nextPaintId++; + assert(paintId < kMaxId); + buffer._putUint8(_strokePaintTag); + buffer._putUint32(color); + buffer._putUint8(strokeCap); + buffer._putUint8(strokeJoin); + buffer._putUint8(blendMode); + buffer._putFloat32(strokeMiterLimit); + buffer._putFloat32(strokeWidth); + buffer._putUint16(paintId); + buffer._putUint16(shaderId ?? kMaxId); + return paintId; + } + + void _readLinearGradient( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int id = buffer.getUint16(); + final double fromX = buffer.getFloat32(); + final double fromY = buffer.getFloat32(); + final double toX = buffer.getFloat32(); + final double toY = buffer.getFloat32(); + final int colorLength = buffer.getUint16(); + final Int32List colors = buffer.getInt32List(colorLength); + final int offsetLength = buffer.getUint16(); + final Float32List offsets = buffer.getFloat32List(offsetLength); + final int tileMode = buffer.getUint8(); + listener?.onLinearGradient( + fromX, + fromY, + toX, + toY, + colors, + offsets, + tileMode, + id, + ); + } + + void _readRadialGradient( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int id = buffer.getUint16(); + final double centerX = buffer.getFloat32(); + final double centerY = buffer.getFloat32(); + final double radius = buffer.getFloat32(); + final int hasFocal = buffer.getUint8(); + double? focalX; + double? focalY; + if (hasFocal == 1) { + focalX = buffer.getFloat32(); + focalY = buffer.getFloat32(); + } + final int colorsLength = buffer.getUint16(); + final Int32List colors = buffer.getInt32List(colorsLength); + final int offsetsLength = buffer.getUint16(); + final Float32List offsets = buffer.getFloat32List(offsetsLength); + final Float64List? transform = buffer.getTransform(); + final int tileMode = buffer.getUint8(); + listener?.onRadialGradient( + centerX, + centerY, + radius, + focalX, + focalY, + colors, + offsets, + transform, + tileMode, + id, + ); + } + + void _readFillPaint( + _ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int color = buffer.getUint32(); + final int blendMode = buffer.getUint8(); + final int id = buffer.getUint16(); + final int shaderId = buffer.getUint16(); + + listener?.onPaintObject( + color: color, + strokeCap: null, + strokeJoin: null, + blendMode: blendMode, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, // Fill + id: id, + shaderId: shaderId == kMaxId ? null : shaderId, + ); + } + + void _readStrokePaint( + _ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int color = buffer.getUint32(); + final int strokeCap = buffer.getUint8(); + final int strokeJoin = buffer.getUint8(); + final int blendMode = buffer.getUint8(); + final double strokeMiterLimit = buffer.getFloat32(); + final double strokeWidth = buffer.getFloat32(); + final int id = buffer.getUint16(); + final int shaderId = buffer.getUint16(); + + listener?.onPaintObject( + color: color, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + blendMode: blendMode, + strokeMiterLimit: strokeMiterLimit, + strokeWidth: strokeWidth, + paintStyle: 1, // Stroke + id: id, + shaderId: shaderId == kMaxId ? null : shaderId, + ); + } + + /// Saves a copy of the current transform and clip on the save stack, and then + /// creates a new group which subsequent calls will become a part of. When the + /// save stack is later popped, the group will be flattened into a layer and + /// have the given `paint`'s [Paint.blendMode] applied. + /// + /// See also: + /// * [Canvas.saveLayer] + void writeSaveLayer(VectorGraphicsBuffer buffer, int paint) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + + buffer._putUint8(_saveLayerTag); + buffer._putUint16(paint); + } + + /// Pops the current save stack, if there is anything to pop. + /// Otherwise, does nothing. + /// + /// See also: + /// * [Canvas.restore] + void writeRestoreLayer(VectorGraphicsBuffer buffer) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + buffer._putUint8(_restoreTag); + } + + /// Write the [text] contents given starting at [x], [y]. + int writeTextConfig({ + required VectorGraphicsBuffer buffer, + required String text, + required String? fontFamily, + required double xAnchorMultiplier, + required int fontWeight, + required double fontSize, + required int decoration, + required int decorationStyle, + required int decorationColor, + }) { + buffer._checkPhase(_CurrentSection.text); + + final int textId = buffer._nextTextId++; + assert(textId < kMaxId); + + buffer._putUint8(_textConfigTag); + buffer._putUint16(textId); + buffer._putFloat32(xAnchorMultiplier); + buffer._putFloat32(fontSize); + buffer._putUint8(fontWeight); + buffer._putUint8(decoration); + buffer._putUint8(decorationStyle); + buffer._putUint32(decorationColor); + + // font-family + if (fontFamily != null) { + // Newer versions of Dart will make this a Uint8List and not require the cast. + // ignore: unnecessary_cast + final Uint8List encoded = utf8.encode(fontFamily) as Uint8List; + buffer._putUint16(encoded.length); + buffer._putUint8List(encoded); + } else { + buffer._putUint16(0); + } + + // text-value + // Newer versions of Dart will make this a Uint8List and not require the cast. + // ignore: unnecessary_cast + final Uint8List encoded = utf8.encode(text) as Uint8List; + buffer._putUint16(encoded.length); + buffer._putUint8List(encoded); + + return textId; + } + + void writeDrawText( + VectorGraphicsBuffer buffer, + int textId, + int? fillId, + int? strokeId, + int? patternId, + ) { + assert(fillId != null || strokeId != null); + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + buffer._putUint8(_drawTextTag); + buffer._putUint16(textId); + buffer._putUint16(fillId ?? kMaxId); + buffer._putUint16(strokeId ?? kMaxId); + buffer._putUint16(patternId ?? kMaxId); + } + + void writeTextPosition( + VectorGraphicsBuffer buffer, + double? x, + double? y, + double? dx, + double? dy, + bool reset, + Float64List? transform, + ) { + buffer._checkPhase(_CurrentSection.textPositions); + final int id = buffer._nextTextPositionId++; + assert(id < kMaxId); + + buffer._putUint8(_textPositionTag); + buffer._putUint16(id); + + buffer._putFloat32(x ?? double.nan); + buffer._putFloat32(y ?? double.nan); + buffer._putFloat32(dx ?? double.nan); + buffer._putFloat32(dy ?? double.nan); + buffer._putUint8(reset ? 1 : 0); + buffer._writeTransform(transform); + } + + void writeUpdateTextPosition( + VectorGraphicsBuffer buffer, int textPositionId) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + buffer._putUint8(_updateTextPositionTag); + buffer._putUint16(textPositionId); + } + + void writeClipPath(VectorGraphicsBuffer buffer, int path) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + buffer._putUint8(_clipPathTag); + buffer._putUint16(path); + } + + void writeMask(VectorGraphicsBuffer buffer) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + buffer._putUint8(_maskTag); + } + + int writePattern( + VectorGraphicsBuffer buffer, + double x, + double y, + double width, + double height, + Float64List transform, + ) { + buffer._checkPhase(_CurrentSection.commands); + assert(buffer._nextPatternId < kMaxId); + final int id = buffer._nextPatternId; + buffer._nextPatternId += 1; + buffer._putUint8(_patternTag); + buffer._putUint16(id); + buffer._putFloat32(x); + buffer._putFloat32(y); + buffer._putFloat32(width); + buffer._putFloat32(height); + buffer._writeTransform(transform); + return id; + } + + /// Write a new path to the [buffer], returing the identifier + /// assigned to it. + /// + /// The [fillType] argument is either `1` for a fill or `0` for a stroke. + /// + /// [controlTypes] is a buffer of the types of control points in order. + /// [controlPoints] is a buffer of the control points in order. + /// + /// If [half] is true, control points will be written to the buffer using + /// half precision floating point values. This will reduce the binary + /// size at the cost of reduced precision. This option defaults to `false`. + int writePath( + VectorGraphicsBuffer buffer, + Uint8List controlTypes, + Float32List controlPoints, + int fillType, { + bool half = false, + }) { + buffer._checkPhase(_CurrentSection.paths); + assert(buffer._nextPathId < kMaxId); + + final int id = buffer._nextPathId; + buffer._nextPathId += 1; + + buffer._putUint8(half ? _pathTagHalfPrecision : _pathTag); + buffer._putUint8(fillType); + buffer._putUint16(id); + buffer._putUint32(controlTypes.length); + buffer._putUint8List(controlTypes); + buffer._putUint32(controlPoints.length); + if (half) { + buffer._putUint16List(_encodeToHalfPrecision(controlPoints)); + } else { + buffer._putFloat32List(controlPoints); + } + return id; + } + + Uint16List _encodeToHalfPrecision(Float32List list) { + final Uint16List output = Uint16List(list.length); + final ByteData buffer = ByteData(8); + for (int i = 0; i < list.length; i++) { + buffer.setFloat32(0, list[i]); + fp16.toHalf(buffer); + output[i] = buffer.getInt16(0); + } + return output; + } + + Float32List _decodeFromHalfPrecision(Uint16List list) { + final Float32List output = Float32List(list.length); + final ByteData buffer = ByteData(8); + for (int i = 0; i < list.length; i++) { + buffer.setUint16(0, list[i]); + output[i] = fp16.toDouble(buffer); + } + return output; + } + + /// Write an image to the [buffer], returning the identifier + /// assigned to it. + /// + /// The [data] argument should be the image data encoded according + /// to the [format] argument. Currently only PNG is supported. + int writeImage( + VectorGraphicsBuffer buffer, + int format, + Uint8List data, + ) { + buffer._checkPhase(_CurrentSection.images); + assert(buffer._nextImageId < kMaxId); + assert(ImageFormatTypes.values.contains(format)); + + final int id = buffer._nextImageId; + buffer._nextImageId += 1; + + buffer._putUint8(_imageConfigTag); + buffer._putUint16(id); + buffer._putUint8(format); + buffer._putUint32(data.length); + buffer._putUint8List(data); + return id; + } + + void writeDrawImage( + VectorGraphicsBuffer buffer, + int imageId, + double x, + double y, + double width, + double height, + Float64List? transform, + ) { + buffer._checkPhase(_CurrentSection.commands); + buffer._addCommandsTag(); + assert(width > 0 && height > 0); + + buffer._putUint8(_drawImageTag); + buffer._putUint16(imageId); + buffer._putFloat32(x); + buffer._putFloat32(y); + buffer._putFloat32(width); + buffer._putFloat32(height); + buffer._writeTransform(transform); + } + + void _readPath( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, { + required bool half, + }) { + final int fillType = buffer.getUint8(); + final int id = buffer.getUint16(); + final int tagLength = buffer.getUint32(); + final Uint8List tags = buffer.getUint8List(tagLength); + final int pointLength = buffer.getUint32(); + final Float32List points; + if (half) { + points = _decodeFromHalfPrecision(buffer.getUint16List(pointLength)); + } else { + points = buffer.getFloat32List(pointLength); + } + listener?.onPathStart(id, fillType); + for (int i = 0, j = 0; i < tagLength; i += 1) { + switch (tags[i]) { + case ControlPointTypes.moveTo: + listener?.onPathMoveTo(points[j], points[j + 1]); + j += 2; + continue; + case ControlPointTypes.lineTo: + listener?.onPathLineTo(points[j], points[j + 1]); + j += 2; + continue; + case ControlPointTypes.cubicTo: + listener?.onPathCubicTo( + points[j], + points[j + 1], + points[j + 2], + points[j + 3], + points[j + 4], + points[j + 5], + ); + j += 6; + continue; + case ControlPointTypes.close: + listener?.onPathClose(); + continue; + default: + assert(false); + } + } + listener?.onPathFinished(); + } + + void _readDrawPath( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int pathId = buffer.getUint16(); + final int paintId = buffer.getUint16(); + int? patternId = buffer.getUint16(); + if (patternId == kMaxId) { + patternId = null; + } + listener?.onDrawPath(pathId, paintId, patternId); + } + + void _readDrawVertices( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int paintId = buffer.getUint16(); + final int verticesLength = buffer.getUint16(); + final Float32List vertices = buffer.getFloat32List(verticesLength); + final int indexLength = buffer.getUint16(); + Uint16List? indices; + if (indexLength != 0) { + indices = buffer.getUint16List(indexLength); + } + listener?.onDrawVertices( + vertices, indices, paintId != kMaxId ? paintId : null); + } + + void _readSaveLayer( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int paintId = buffer.getUint16(); + listener?.onSaveLayer(paintId); + } + + void _readClipPath( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int pathId = buffer.getUint16(); + listener?.onClipPath(pathId); + } + + void _readSize(_ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final double width = buffer.getFloat32(); + final double height = buffer.getFloat32(); + listener?.onSize(width, height); + } + + void _readTextPosition( + _ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int id = buffer.getUint16(); + final double x = buffer.getFloat32(); + final double y = buffer.getFloat32(); + final double dx = buffer.getFloat32(); + final double dy = buffer.getFloat32(); + + final bool reset = buffer.getUint8() != 0; + final Float64List? transform = buffer.getTransform(); + + listener?.onTextPosition( + id, + x.isNaN ? null : x, + y.isNaN ? null : y, + dx.isNaN ? null : dx, + dy.isNaN ? null : dy, + reset, + transform, + ); + } + + void _readUpdateTextPosition( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int textPositionId = buffer.getUint16(); + listener?.onUpdateTextPosition(textPositionId); + } + + void _readTextConfig( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int id = buffer.getUint16(); + final double xAnchorMultiplier = buffer.getFloat32(); + final double fontSize = buffer.getFloat32(); + final int fontWeight = buffer.getUint8(); + final int decoration = buffer.getUint8(); + final int decorationStyle = buffer.getUint8(); + final int decorationColor = buffer.getUint32(); + String? fontFamily; + final int fontFamilyLength = buffer.getUint16(); + if (fontFamilyLength > 0) { + fontFamily = utf8.decode(buffer.getUint8List(fontFamilyLength)); + } + final int textLength = buffer.getUint16(); + final String text = utf8.decode(buffer.getUint8List(textLength)); + + listener?.onTextConfig( + text, + fontFamily, + xAnchorMultiplier, + fontWeight, + fontSize, + decoration, + decorationStyle, + decorationColor, + id, + ); + } + + void _readDrawText( + _ReadBuffer buffer, + VectorGraphicsCodecListener? listener, + ) { + final int textId = buffer.getUint16(); + int? fillId = buffer.getUint16(); + if (fillId == kMaxId) { + fillId = null; + } + int? strokeId = buffer.getUint16(); + if (strokeId == kMaxId) { + strokeId = null; + } + assert(fillId != null || strokeId != null); + int? patternId = buffer.getUint16(); + if (patternId == kMaxId) { + patternId = null; + } + listener?.onDrawText(textId, fillId, strokeId, patternId); + } + + void _readImageConfig( + _ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int id = buffer.getUint16(); + final int format = buffer.getUint8(); + final int dataLength = buffer.getUint32(); + final Uint8List data = buffer.getUint8List(dataLength); + listener?.onImage(id, format, data); + } + + void _readDrawImage( + _ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int id = buffer.getUint16(); + final double x = buffer.getFloat32(); + final double y = buffer.getFloat32(); + final double width = buffer.getFloat32(); + final double height = buffer.getFloat32(); + final Float64List? transformLength = buffer.getTransform(); + + listener?.onDrawImage(id, x, y, width, height, transformLength); + } + + void _readPattern(_ReadBuffer buffer, VectorGraphicsCodecListener? listener) { + final int patternId = buffer.getUint16(); + final double x = buffer.getFloat32(); + final double y = buffer.getFloat32(); + final double width = buffer.getFloat32(); + final double height = buffer.getFloat32(); + final Float64List? transform = buffer.getTransform(); + listener?.onPatternStart(patternId, x, y, width, height, transform!); + } +} + +/// Implement this listener class to support decoding of vector_graphics binary +/// assets. +abstract class VectorGraphicsCodecListener { + /// The size of the vector graphic has been decoded. + void onSize( + double width, + double height, + ); + + /// A paint object has been decoded. + /// + /// If the paint object is for a fill, then [strokeCap], [strokeJoin], + /// [strokeMiterLimit], and [strokeWidget] will be `null`. + 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, + }); + + /// A path object is being created, with the given [id] and [fillType]. + /// + /// All subsequent path commands will refer to this path, until + /// [onPathFinished] is invoked. + void onPathStart(int id, int fillType); + + /// A path object should move to (x, y). + void onPathMoveTo(double x, double y); + + /// A path object should line to (x, y). + void onPathLineTo(double x, double y); + + /// A path object will draw a cubic to (x1, y1), with control point 1 as + /// (x2, y2) and control point 2 as (x3, y3). + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3); + + /// The current path has been closed. + void onPathClose(); + + /// The current path is completed. + void onPathFinished(); + + /// Draw the given [pathId] with the given [paintId]. + /// + /// If the [paintId] is `null`, a default empty paint should be used instead. + void onDrawPath( + int pathId, + int? paintId, + int? patternId, + ); + + /// Draw the vertices with the given [vertices] and optionally index buffer + /// [indices]. + /// + /// If the [paintId] is `null`, a default empty paint should be used instead. + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId); + + /// Save a new layer with the given [paintId]. + void onSaveLayer(int paintId); + + /// Apply the specified paths as clips to the current canvas. + void onClipPath(int pathId); + + /// Restore the save stack. + void onRestoreLayer(); + + /// Prepare to draw a new mask, until the next [onRestoreLayer] command. + void onMask(); + + /// A radial gradient shader has been parsed. + /// + /// [focalX] and [focalY] are either both `null` or `non-null`. + void onRadialGradient( + double centerX, + double centerY, + double radius, + double? focalX, + double? focalY, + Int32List colors, + Float32List? offsets, + Float64List? transform, + int tileMode, + int id, + ); + + /// A linear gradient shader has been parsed. + void onLinearGradient( + double fromX, + double fromY, + double toX, + double toY, + Int32List colors, + Float32List? offsets, + int tileMode, + int id, + ); + + /// A text configuration block has been decoded. + void onTextConfig( + String text, + String? fontFamily, + double xAnchorMultiplier, + int fontWeight, + double fontSize, + int decoration, + int decorationStyle, + int decorationColor, + int id, + ); + + /// A text block has been decoded. + void onDrawText( + int textId, + int? fillId, + int? strokeId, + int? patternId, + ); + + /// An encoded image has been decoded. + /// + /// The format is one of the values in [ImageFormatTypes]. + /// + /// If the [onError] callback is not null, it must be called if an error + /// occurs while attempting to decode the image [data]. + void onImage( + int imageId, + int format, + Uint8List data, { + VectorGraphicsErrorListener? onError, + }); + + /// An image should be drawn at the provided location. + void onDrawImage( + int imageId, + double x, + double y, + double width, + double height, + Float64List? transform, + ); + + /// A pattern has been decoded. + /// + /// All subsequent pattern commands will refer to this pattern, until + /// [onPatternFinished] is invoked. + void onPatternStart(int patternId, double x, double y, double width, + double height, Float64List transform); + + /// Record a new text position. + void onTextPosition( + int textPositionId, + double? x, + double? y, + double? dx, + double? dy, + bool reset, + Float64List? transform, + ); + + /// An instruction to update the current text position. + void onUpdateTextPosition(int textPositionId); +} + +enum _CurrentSection { + size, + images, + shaders, + paints, + paths, + textPositions, + text, + commands, +} + +/// Write-only buffer for incrementally building a [ByteData] instance. +/// +/// A [VectorGraphicsBuffer] instance can be used only once. Attempts to reuse will result +/// in [StateError]s being thrown. +/// +/// The byte order used is [Endian.little] throughout. +class VectorGraphicsBuffer { + /// Creates an interface for incrementally building a [ByteData] instance. + VectorGraphicsBuffer() + : _buffer = [], + _isDone = false, + _eightBytes = ByteData(8) { + _eightBytesAsList = _eightBytes.buffer.asUint8List(); + // Begin message with the magic number and current version. + _putUint32(VectorGraphicsCodec._magicNumber); + _putUint8(VectorGraphicsCodec._version); + } + + List _buffer; + bool _isDone; + final ByteData _eightBytes; + late Uint8List _eightBytesAsList; + static final Uint8List _zeroBuffer = Uint8List(8); + + /// The next paint id to be used. + int _nextPaintId = 0; + + /// The next path id to be used. + int _nextPathId = 0; + + /// The next shader id to be used. + int _nextShaderId = 0; + + /// The next text id to be used. + int _nextTextId = 0; + + /// The next text position id to be used. + int _nextTextPositionId = 0; + + /// The next image id to be used. + int _nextImageId = 0; + + /// The next pattern id to be used. + int _nextPatternId = 0; + + bool _addedCommandTag = false; + + /// The current decoding phase. + /// + /// Objects must be written in the correct order, the same as the + /// enum order. + _CurrentSection _decodePhase = _CurrentSection.size; + + /// Add a commands tag section if it is not already present. + void _addCommandsTag() { + if (_addedCommandTag) { + return; + } + _putUint8(VectorGraphicsCodec._beginCommandsTag); + _addedCommandTag = true; + } + + void _checkPhase(_CurrentSection expected) { + if (_decodePhase.index > expected.index) { + final String name = expected.name; + throw StateError('${name[0].toUpperCase()}${name.substring(1)} ' + 'must be encoded together (current phase is ${_decodePhase.name}).'); + } + _decodePhase = expected; + } + + void _writeTransform(Float64List? transform) { + if (transform != null) { + _putUint8(transform.length); + _putFloat64List(transform); + } else { + _putUint8(0); + } + } + + /// Write a Uint8 into the buffer. + void _putUint8(int byte) { + assert(!_isDone); + _buffer.add(byte); + } + + void _putUint16(int value) { + assert(!_isDone); + _eightBytes.setUint16(0, value, Endian.little); + _buffer.addAll(_eightBytesAsList.take(2)); + } + + /// Write a Uint32 into the buffer. + void _putUint32(int value) { + assert(!_isDone); + _eightBytes.setUint32(0, value, Endian.little); + _buffer.addAll(_eightBytesAsList.take(4)); + } + + /// Write an Int32List into the buffer. + void _putInt32List(Int32List list) { + assert(!_isDone); + _alignTo(4); + _buffer + .addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length)); + } + + /// Write an Float32 into the buffer. + void _putFloat32(double value) { + assert(!_isDone); + _eightBytes.setFloat32(0, value, Endian.little); + _buffer.addAll(_eightBytesAsList.take(4)); + } + + void _putUint8List(Uint8List list) { + assert(!_isDone); + _buffer.addAll(list.buffer.asUint8List(list.offsetInBytes, list.length)); + } + + void _putUint16List(Uint16List list) { + assert(!_isDone); + _alignTo(2); + _buffer + .addAll(list.buffer.asUint8List(list.offsetInBytes, 2 * list.length)); + } + + /// Write all the values from a [Float32List] into the buffer. + void _putFloat32List(Float32List list) { + assert(!_isDone); + _alignTo(4); + _buffer + .addAll(list.buffer.asUint8List(list.offsetInBytes, 4 * list.length)); + } + + void _putFloat64List(Float64List list) { + assert(!_isDone); + _alignTo(8); + _buffer + .addAll(list.buffer.asUint8List(list.offsetInBytes, 8 * list.length)); + } + + void _alignTo(int alignment) { + assert(!_isDone); + final int mod = _buffer.length % alignment; + if (mod != 0) { + _buffer.addAll(_zeroBuffer.take(alignment - mod)); + } + } + + /// Finalize and return the written [ByteData]. + ByteData done() { + if (_isDone) { + throw StateError( + 'done() must not be called more than once on the same VectorGraphicsBuffer.'); + } + final ByteData result = Uint8List.fromList(_buffer).buffer.asByteData(); + _buffer = []; + _isDone = true; + return result; + } +} + +/// Read-only buffer for reading sequentially from a [ByteData] instance. +/// +/// The byte order used is [Endian.little] throughout. +class _ReadBuffer { + /// Creates a [_ReadBuffer] for reading from the specified [data]. + _ReadBuffer(this.data); + + /// The underlying data being read. + final ByteData data; + + /// The position to read next. + int _position = 0; + + /// Whether the buffer has data remaining to read. + bool get hasRemaining => _position < data.lengthInBytes; + + /// Reads a Uint8 from the buffer. + int getUint8() { + return data.getUint8(_position++); + } + + /// Reads a Uint16 from the buffer. + int getUint16() { + final int value = data.getUint16(_position, Endian.little); + _position += 2; + return value; + } + + /// Reads a Uint32 from the buffer. + int getUint32() { + final int value = data.getUint32(_position, Endian.little); + _position += 4; + return value; + } + + /// Reads an Int32 from the buffer. + int getInt32() { + final int value = data.getInt32(_position, Endian.little); + _position += 4; + return value; + } + + /// Reads an Int64 from the buffer. + int getInt64() { + final int value = data.getInt64(_position, Endian.little); + _position += 8; + return value; + } + + /// Reads a Float32 from the buffer. + double getFloat32() { + final double value = data.getFloat32(_position, Endian.little); + _position += 4; + return value; + } + + /// Reads a Float64 from the buffer. + double getFloat64() { + _alignTo(8); + final double value = data.getFloat64(_position, Endian.little); + _position += 8; + return value; + } + + /// Reads the given number of Uint8s from the buffer. + Uint8List getUint8List(int length) { + final Uint8List list = + data.buffer.asUint8List(data.offsetInBytes + _position, length); + _position += length; + return list; + } + + Uint16List getUint16List(int length) { + _alignTo(2); + final Uint16List list = + data.buffer.asUint16List(data.offsetInBytes + _position, length); + _position += 2 * length; + return list; + } + + /// Reads the given number of Int32s from the buffer. + Int32List getInt32List(int length) { + _alignTo(4); + final Int32List list = + data.buffer.asInt32List(data.offsetInBytes + _position, length); + _position += 4 * length; + return list; + } + + /// Reads the given number of Int64s from the buffer. + Int64List getInt64List(int length) { + _alignTo(8); + final Int64List list = + data.buffer.asInt64List(data.offsetInBytes + _position, length); + _position += 8 * length; + return list; + } + + /// Reads the given number of Float32s from the buffer + Float32List getFloat32List(int length) { + _alignTo(4); + final Float32List list = + data.buffer.asFloat32List(data.offsetInBytes + _position, length); + _position += 4 * length; + return list; + } + + /// Reads the given number of Float64s from the buffer. + Float64List getFloat64List(int length) { + _alignTo(8); + final Float64List list = + data.buffer.asFloat64List(data.offsetInBytes + _position, length); + _position += 8 * length; + return list; + } + + void _alignTo(int alignment) { + final int mod = _position % alignment; + if (mod != 0) { + _position += alignment - mod; + } + } + + Float64List? getTransform() { + final int transformLength = getUint8(); + if (transformLength > 0) { + assert(transformLength == 16); + return getFloat64List(transformLength); + } + return null; + } +} diff --git a/packages/vector_graphics_codec/pubspec.yaml b/packages/vector_graphics_codec/pubspec.yaml new file mode 100644 index 00000000000..2a12bef0f46 --- /dev/null +++ b/packages/vector_graphics_codec/pubspec.yaml @@ -0,0 +1,28 @@ +name: vector_graphics_codec +description: An encoding library for the binary format used in `package:vector_graphics` +repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_codec +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 +# See https://github.com/flutter/flutter/issues/157626 before publishing a new +# version. +version: 1.1.12 + +environment: + sdk: ^3.4.0 + +dev_dependencies: + flutter_test: + sdk: flutter + meta: ^1.15.0 + test: ^1.25.0 + +platforms: + android: + ios: + linux: + macos: + web: + windows: + +topics: + - svg + - vector-graphics diff --git a/packages/vector_graphics_codec/test/fp16_test.dart b/packages/vector_graphics_codec/test/fp16_test.dart new file mode 100644 index 00000000000..3d9a3e06ebe --- /dev/null +++ b/packages/vector_graphics_codec/test/fp16_test.dart @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:test/test.dart'; +import 'package:vector_graphics_codec/src/fp16.dart'; + +double convert(double value) { + final ByteData byteData = ByteData(8); + byteData.setFloat32(0, value); + toHalf(byteData); + return toDouble(byteData); +} + +void main() { + test('fp16 positive values', () { + final List> missed = >[]; + + /// Validate that all numbers between [min] and [max] can be converted within [tolerance]. + void checkRange( + {required double min, required double max, required double tolerance}) { + final ByteData byteData = ByteData(8); + for (double i = min; i < max; i += 1) { + byteData.setFloat32(0, i); + toHalf(byteData); + + final double result = toDouble(byteData); + if ((result - i).abs() > tolerance) { + missed.add([i, result]); + } + } + } + + // The first 2048 values can be represented within 1.0. + checkRange(min: 0, max: 2048, tolerance: 1.0); + + // 2048-4096 values can be represented within 2.0. + checkRange(min: 2048, max: 4096, tolerance: 2.0); + + // 4096 - 8192 can be represented within 4.0. + checkRange(min: 4096, max: 8192, tolerance: 4.0); + + // 8192 - 16384 can be represented within 8.0. + checkRange(min: 8192, max: 16384, tolerance: 8.0); + + // 16384 - 32768 can be represented within 16.0. + checkRange(min: 16384, max: 32768, tolerance: 16.0); + + // 32768 - 65519 can be represented within 32.0. + checkRange(min: 32768, max: 65519, tolerance: 16.0); + + expect(missed, isEmpty); + }); + + test('fp16 signed values', () { + expect(convert(-1.0), -1.0); + expect(convert(-100.0), -100.0); + expect(convert(-125.4375), -125.4375); + expect(convert(-12500.5), -12504.0); + }); + + test('fp16 sentinel values', () { + expect(convert(double.infinity), double.infinity); + expect(convert(65520), double.infinity); + expect(convert(double.nan), isNaN); + expect(convert(double.negativeInfinity), double.negativeInfinity); + }); +} diff --git a/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart b/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart new file mode 100644 index 00000000000..53b96380696 --- /dev/null +++ b/packages/vector_graphics_codec/test/vector_graphics_codec_test.dart @@ -0,0 +1,1691 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:test/test.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +const VectorGraphicsCodec codec = VectorGraphicsCodec(); +final Float64List mat4 = Float64List.fromList( + [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); + +void bufferContains(VectorGraphicsBuffer buffer, List expectedBytes) { + final Uint8List data = buffer.done().buffer.asUint8List(); + expect(data, equals(expectedBytes)); +} + +void main() { + test('Messages begin with a magic number and version', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + + bufferContains(buffer, [98, 45, 136, 0, 1]); + }); + + test('Messages without any contents cannot be decoded', () { + expect( + () => codec.decode(Uint8List(0).buffer.asByteData(), null), + throwsA(isA().having( + (StateError se) => se.message, + 'message', + contains( + 'The provided data was not a vector_graphics binary asset.')))); + }); + + test('Messages without a magic number cannot be decoded', () { + expect( + () => codec.decode(Uint8List(6).buffer.asByteData(), null), + throwsA(isA().having( + (StateError se) => se.message, + 'message', + contains( + 'The provided data was not a vector_graphics binary asset.')))); + }); + + test('Messages without an incompatible version cannot be decoded', () { + final Uint8List bytes = Uint8List(6); + bytes[0] = 98; + bytes[1] = 45; + bytes[2] = 136; + bytes[3] = 0; + bytes[4] = 6; // version 6. + + expect( + () => codec.decode(bytes.buffer.asByteData(), null), + throwsA(isA().having( + (StateError se) => se.message, + 'message', + contains( + 'he provided data does not match the currently supported version.')))); + }); + + test('Basic message encode and decode with filled path', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int paintId = codec.writeFill(buffer, 23, 0); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.moveTo, + ControlPointTypes.lineTo, + ControlPointTypes.close + ]), + Float32List.fromList([1, 2, 2, 3]), + 0, + ); + codec.writeDrawPath(buffer, pathId, paintId, null); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 23, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: paintId, + shaderId: null, + ), + OnPathStart(pathId, 0), + const OnPathMoveTo(1, 2), + const OnPathLineTo(2, 3), + const OnPathClose(), + const OnPathFinished(), + OnDrawPath(pathId, paintId, null), + ]); + }); + + test('Basic message encode and decode with shaded path', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int shaderId = codec.writeLinearGradient( + buffer, + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + colors: Int32List.fromList([0, 1]), + offsets: Float32List.fromList([0, 1]), + tileMode: 1, + ); + final int fillId = codec.writeFill(buffer, 23, 0, shaderId); + final int strokeId = + codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0, shaderId); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.moveTo, + ControlPointTypes.lineTo, + ControlPointTypes.close + ]), + Float32List.fromList([1, 2, 2, 3]), + 0, + ); + codec.writeDrawPath(buffer, pathId, fillId, null); + codec.writeDrawPath(buffer, pathId, strokeId, null); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnLinearGradient( + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + colors: Int32List.fromList([0, 1]), + offsets: Float32List.fromList([0, 1]), + tileMode: 1, + id: shaderId, + ), + OnPaintObject( + color: 23, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: fillId, + shaderId: shaderId, + ), + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: strokeId, + shaderId: shaderId, + ), + OnPathStart(pathId, 0), + const OnPathMoveTo(1, 2), + const OnPathLineTo(2, 3), + const OnPathClose(), + const OnPathFinished(), + OnDrawPath(pathId, fillId, null), + OnDrawPath(pathId, strokeId, null), + ]); + }); + + test('Basic message encode and decode with stroked vertex', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int paintId = codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0); + codec.writeDrawVertices( + buffer, + Float32List.fromList([ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ]), + null, + paintId); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: paintId, + shaderId: null, + ), + OnDrawVertices(const [ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ], null, paintId), + ]); + }); + + test('Basic message encode and decode with stroked vertex and indexes', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int paintId = codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0); + codec.writeDrawVertices( + buffer, + Float32List.fromList([ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ]), + Uint16List.fromList([ + 0, + 1, + 2, + 3, + 4, + 5, + ]), + paintId, + ); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: paintId, + shaderId: null, + ), + OnDrawVertices(const [ + 0.0, + 2.0, + 3.0, + 4.0, + 2.0, + 4.0, + ], const [ + 0, + 1, + 2, + 3, + 4, + 5, + ], paintId), + ]); + }); + + test('Can encode opacity/save/restore layers', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int paintId = codec.writeFill(buffer, 0xAA000000, 0); + + codec.writeSaveLayer(buffer, paintId); + codec.writeRestoreLayer(buffer); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 0xAA000000, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: paintId, + shaderId: null, + ), + OnSaveLayer(paintId), + const OnRestoreLayer(), + ]); + }); + + test('Can encode a radial gradient', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int shaderId = codec.writeRadialGradient( + buffer, + centerX: 2.0, + centerY: 3.0, + radius: 5.0, + focalX: 1.0, + focalY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + tileMode: 0, + transform: mat4, + ); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnRadialGradient( + centerX: 2.0, + centerY: 3.0, + radius: 5.0, + focalX: 1.0, + focalY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + transform: mat4, + tileMode: 0, + id: shaderId, + ), + ]); + }); + + test('Can encode a radial gradient (no matrix)', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int shaderId = codec.writeRadialGradient( + buffer, + centerX: 2.0, + centerY: 3.0, + radius: 5.0, + focalX: 1.0, + focalY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + tileMode: 0, + transform: null, + ); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnRadialGradient( + centerX: 2.0, + centerY: 3.0, + radius: 5.0, + focalX: 1.0, + focalY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + transform: null, + tileMode: 0, + id: shaderId, + ), + ]); + }); + + test('Can encode a linear gradient', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int shaderId = codec.writeLinearGradient( + buffer, + fromX: 2.0, + fromY: 3.0, + toX: 1.0, + toY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + tileMode: 0, + ); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnLinearGradient( + fromX: 2.0, + fromY: 3.0, + toX: 1.0, + toY: 1.0, + colors: Int32List.fromList([0xFFAABBAA]), + offsets: Float32List.fromList([2.2, 1.2]), + tileMode: 0, + id: shaderId, + ), + ]); + }); + + test('Can encode clips', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.lineTo, + ControlPointTypes.lineTo, + ControlPointTypes.lineTo, + ControlPointTypes.close, + ]), + Float32List.fromList([0, 10, 20, 10, 20, 0]), + 0, + ); + + codec.writeClipPath(buffer, pathId); + codec.writeRestoreLayer(buffer); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPathStart(pathId, 0), + const OnPathLineTo(0, 10), + const OnPathLineTo(20, 10), + const OnPathLineTo(20, 0), + const OnPathClose(), + const OnPathFinished(), + OnClipPath(pathId), + const OnRestoreLayer(), + ]); + }); + + test('Can encode masks', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + codec.writeMask(buffer); + codec.decode(buffer.done(), listener); + expect(listener.commands, [const OnMask()]); + }); + + test('Encodes a size', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + codec.writeSize(buffer, 20, 30); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [const OnSize(20, 30)]); + }); + + test('Only supports a single size', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + + codec.writeSize(buffer, 20, 30); + expect(() => codec.writeSize(buffer, 1, 1), throwsStateError); + }); + + test('Encodes text', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int paintId = codec.writeFill(buffer, 0xFFAABBAA, 0); + final int textId = codec.writeTextConfig( + buffer: buffer, + text: 'Hello', + fontFamily: 'Roboto', + xAnchorMultiplier: 0, + fontWeight: 0, + fontSize: 16, + decoration: 0, + decorationStyle: 0, + decorationColor: 0, + ); + codec.writeDrawText(buffer, textId, paintId, null, null); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 0xFFAABBAA, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: paintId, + shaderId: null, + ), + OnTextConfig('Hello', 0, 16, 'Roboto', 0, 0, 0, 0, textId), + OnDrawText(textId, paintId, null, null), + ]); + }); + + test('Encodes text with null font family', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int paintId = codec.writeFill(buffer, 0xFFAABBAA, 0); + final int textId = codec.writeTextConfig( + buffer: buffer, + text: 'Hello', + fontFamily: null, + xAnchorMultiplier: 0, + fontWeight: 0, + fontSize: 16, + decoration: 0, + decorationStyle: 0, + decorationColor: 0, + ); + codec.writeDrawText(buffer, textId, paintId, null, null); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 0xFFAABBAA, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: paintId, + shaderId: null, + ), + OnTextConfig('Hello', 0, 16, null, 0, 0, 0, 0, textId), + OnDrawText(textId, paintId, null, null), + ]); + }); + + test('Encodes empty text', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int paintId = codec.writeFill(buffer, 0xFFAABBAA, 0); + final int textId = codec.writeTextConfig( + buffer: buffer, + text: '', + fontFamily: null, + xAnchorMultiplier: 0, + fontWeight: 0, + fontSize: 16, + decoration: 0, + decorationStyle: 0, + decorationColor: 0, + ); + codec.writeDrawText(buffer, textId, paintId, null, null); + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnPaintObject( + color: 0xFFAABBAA, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: paintId, + shaderId: null, + ), + OnTextConfig('', 0, 16, null, 0, 0, 0, 0, textId), + OnDrawText(textId, paintId, null, null), + ]); + }); + + test('Encodes text position', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + codec.writeTextPosition(buffer, 1, 2, 3, 4, true, mat4); + + codec.decode(buffer.done(), listener); + + expect(listener.commands, [ + OnTextPosition( + id: 0, + x: 1, + y: 2, + dx: 3, + dy: 4, + reset: true, + transform: mat4, + ), + ]); + }); + + test('Encodes image data without transform', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int id = + codec.writeImage(buffer, 0, Uint8List.fromList([0, 1, 3, 4, 5])); + codec.writeDrawImage(buffer, id, 1, 2, 100, 100, null); + final ByteData data = buffer.done(); + final DecodeResponse response = codec.decode(data, listener); + + expect(response.complete, false); + expect(listener.commands, [ + OnImage(id, 0, const [0, 1, 3, 4, 5]), + ]); + + final DecodeResponse nextResponse = + codec.decode(data, listener, response: response); + + expect(nextResponse.complete, true); + expect(listener.commands, [ + OnImage(id, 0, const [0, 1, 3, 4, 5]), + OnDrawImage(id, 1, 2, 100, 100, null), + ]); + }); + + test('Encodes image data with transform', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int id = + codec.writeImage(buffer, 0, Uint8List.fromList([0, 1, 3, 4, 5])); + codec.writeDrawImage(buffer, id, 1, 2, 100, 100, mat4); + final ByteData data = buffer.done(); + final DecodeResponse response = codec.decode(data, listener); + + expect(response.complete, false); + expect(listener.commands, [ + OnImage(id, 0, const [0, 1, 3, 4, 5]), + ]); + + final DecodeResponse nextResponse = + codec.decode(data, listener, response: response); + + expect(nextResponse.complete, true); + expect(listener.commands, [ + OnImage(id, 0, const [0, 1, 3, 4, 5]), + OnDrawImage(id, 1, 2, 100, 100, mat4), + ]); + }); + + test('Encodes image data with various formats', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + + for (final int format in ImageFormatTypes.values) { + expect( + codec.writeImage( + buffer, format, Uint8List.fromList([0, 1, 3, 4, 5])), + greaterThan(-1), + ); + } + }); + + test('Basic message encode and decode with shaded path and image', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int imageId = + codec.writeImage(buffer, 0, Uint8List.fromList([0, 1, 3, 4, 5])); + final int shaderId = codec.writeLinearGradient( + buffer, + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + colors: Int32List.fromList([0, 1]), + offsets: Float32List.fromList([0, 1]), + tileMode: 1, + ); + final int fillId = codec.writeFill(buffer, 23, 0, shaderId); + final int strokeId = + codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0, shaderId); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.moveTo, + ControlPointTypes.lineTo, + ControlPointTypes.close + ]), + Float32List.fromList([1, 2, 2, 3]), + 0, + ); + codec.writeDrawPath(buffer, pathId, fillId, null); + codec.writeDrawPath(buffer, pathId, strokeId, null); + codec.writeDrawImage(buffer, imageId, 1, 2, 100, 100, null); + + final ByteData data = buffer.done(); + + DecodeResponse response = codec.decode(data, listener); + + expect(response.complete, false); + expect(listener.commands, [ + OnImage( + imageId, + 0, + const [0, 1, 3, 4, 5], + ), + OnLinearGradient( + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + colors: Int32List.fromList([0, 1]), + offsets: Float32List.fromList([0, 1]), + tileMode: 1, + id: shaderId, + ), + OnPaintObject( + color: 23, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: fillId, + shaderId: shaderId, + ), + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: strokeId, + shaderId: shaderId, + ), + OnPathStart(pathId, 0), + const OnPathMoveTo(1, 2), + const OnPathLineTo(2, 3), + const OnPathClose(), + const OnPathFinished(), + ]); + + response = codec.decode(data, listener, response: response); + + expect(response.complete, true); + expect(listener.commands, [ + OnImage( + imageId, + 0, + const [0, 1, 3, 4, 5], + ), + OnLinearGradient( + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + colors: Int32List.fromList([0, 1]), + offsets: Float32List.fromList([0, 1]), + tileMode: 1, + id: shaderId, + ), + OnPaintObject( + color: 23, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: fillId, + shaderId: shaderId, + ), + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: strokeId, + shaderId: shaderId, + ), + OnPathStart(pathId, 0), + const OnPathMoveTo(1, 2), + const OnPathLineTo(2, 3), + const OnPathClose(), + const OnPathFinished(), + OnDrawPath(pathId, fillId, null), + OnDrawPath(pathId, strokeId, null), + OnDrawImage(imageId, 1, 2, 100, 100, null), + ]); + }); + + test('Basic message encode and decode with half precision path', () { + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + final TestListener listener = TestListener(); + + final int fillId = codec.writeFill(buffer, 23, 0); + final int strokeId = codec.writeStroke(buffer, 44, 1, 2, 3, 4.0, 6.0); + final int pathId = codec.writePath( + buffer, + Uint8List.fromList([ + ControlPointTypes.moveTo, + ControlPointTypes.lineTo, + ControlPointTypes.lineTo, + ControlPointTypes.close + ]), + Float32List.fromList([1.25, 24.5, 200.10, -32.4, -10000, 2500.2]), + 0, + half: true, + ); + codec.writeDrawPath(buffer, pathId, fillId, null); + codec.writeDrawPath(buffer, pathId, strokeId, null); + + final ByteData data = buffer.done(); + + final DecodeResponse response = codec.decode(data, listener); + + expect(response.complete, true); + expect(listener.commands, [ + OnPaintObject( + color: 23, + strokeCap: null, + strokeJoin: null, + blendMode: 0, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: fillId, + shaderId: null, + ), + OnPaintObject( + color: 44, + strokeCap: 1, + strokeJoin: 2, + blendMode: 3, + strokeMiterLimit: 4.0, + strokeWidth: 6.0, + paintStyle: 1, + id: strokeId, + shaderId: null, + ), + OnPathStart(pathId, 0), + const OnPathMoveTo(1.25, 24.5), + const OnPathLineTo(200.125, -32.40625), + const OnPathLineTo(-10000, 2500.0), + const OnPathClose(), + const OnPathFinished(), + const OnDrawPath(0, 0, null), + const OnDrawPath(0, 1, null), + ]); + }); +} + +class TestListener extends VectorGraphicsCodecListener { + final List commands = []; + + @override + void onDrawPath(int pathId, int? paintId, int? patternId) { + commands.add(OnDrawPath(pathId, paintId, patternId)); + } + + @override + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) { + commands.add(OnDrawVertices(vertices, indices, paintId)); + } + + @override + void onTextPosition(int textPositionId, double? x, double? y, double? dx, + double? dy, bool reset, Float64List? transform) { + commands.add(OnTextPosition( + id: textPositionId, + x: x, + y: y, + dx: dx, + dy: dy, + reset: reset, + transform: transform, + )); + } + + @override + void onUpdateTextPosition(int textPositionId) { + commands.add(OnUpdateTextPosition(textPositionId)); + } + + @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, + }) { + commands.add( + OnPaintObject( + color: color, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + blendMode: blendMode, + strokeMiterLimit: strokeMiterLimit, + strokeWidth: strokeWidth, + paintStyle: paintStyle, + id: id, + shaderId: shaderId, + ), + ); + } + + @override + void onPathClose() { + commands.add(const OnPathClose()); + } + + @override + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + commands.add(OnPathCubicTo(x1, y1, x2, y2, x3, y3)); + } + + @override + void onPathFinished() { + commands.add(const OnPathFinished()); + } + + @override + void onPathLineTo(double x, double y) { + commands.add(OnPathLineTo(x, y)); + } + + @override + void onPathMoveTo(double x, double y) { + commands.add(OnPathMoveTo(x, y)); + } + + @override + void onPathStart(int id, int fillType) { + commands.add(OnPathStart(id, fillType)); + } + + @override + void onRestoreLayer() { + commands.add(const OnRestoreLayer()); + } + + @override + void onMask() { + commands.add(const OnMask()); + } + + @override + void onSaveLayer(int id) { + commands.add(OnSaveLayer(id)); + } + + @override + void onClipPath(int pathId) { + commands.add(OnClipPath(pathId)); + } + + @override + void onRadialGradient( + double centerX, + double centerY, + double radius, + double? focalX, + double? focalY, + Int32List colors, + Float32List? offsets, + Float64List? transform, + int tileMode, + int id, + ) { + commands.add( + OnRadialGradient( + centerX: centerX, + centerY: centerY, + radius: radius, + focalX: focalX, + focalY: focalY, + colors: colors, + offsets: offsets, + transform: transform, + tileMode: tileMode, + id: id, + ), + ); + } + + @override + void onLinearGradient( + double fromX, + double fromY, + double toX, + double toY, + Int32List colors, + Float32List? offsets, + int tileMode, + int id, + ) { + commands.add(OnLinearGradient( + fromX: fromX, + fromY: fromY, + toX: toX, + toY: toY, + colors: colors, + offsets: offsets, + tileMode: tileMode, + id: id, + )); + } + + @override + void onSize(double width, double height) { + commands.add(OnSize(width, height)); + } + + @override + void onTextConfig( + String text, + String? fontFamily, + double xAnchorMultiplier, + int fontWeight, + double fontSize, + int decoration, + int decorationStyle, + int decorationColor, + int id, + ) { + commands.add(OnTextConfig( + text, + xAnchorMultiplier, + fontSize, + fontFamily, + fontWeight, + decoration, + decorationStyle, + decorationColor, + id, + )); + } + + @override + void onDrawText(int textId, int? fillId, int? strokeId, int? patternId) { + commands.add(OnDrawText(textId, fillId, strokeId, patternId)); + } + + @override + void onImage( + int imageId, + int format, + Uint8List data, { + VectorGraphicsErrorListener? onError, + }) { + commands.add(OnImage( + imageId, + format, + data, + onError: onError, + )); + } + + @override + void onDrawImage( + int imageId, + double x, + double y, + double width, + double height, + Float64List? transform, + ) { + commands.add(OnDrawImage(imageId, x, y, width, height, transform)); + } + + @override + void onPatternStart(int patternId, double x, double y, double width, + double height, Float64List transform) { + commands.add(OnPatternStart(patternId, x, y, width, height, transform)); + } +} + +@immutable +@immutable +class OnTextPosition { + const OnTextPosition({ + required this.id, + this.x, + this.y, + this.dx, + this.dy, + required this.reset, + required this.transform, + }); + + final int id; + final double? x; + final double? y; + final double? dx; + final double? dy; + final bool reset; + final Float64List? transform; + + @override + int get hashCode => Object.hash( + id, + x, + y, + dx, + dy, + reset, + Object.hashAll(transform ?? []), + ); + + @override + bool operator ==(Object other) { + return other is OnTextPosition && + other.id == id && + other.x == x && + other.y == y && + other.dx == dx && + other.dy == dy && + _listEquals(other.transform, transform); + } +} + +@immutable +class OnMask { + const OnMask(); +} + +@immutable +@immutable +class OnLinearGradient { + const OnLinearGradient({ + required this.fromX, + required this.fromY, + required this.toX, + required this.toY, + required this.colors, + required this.offsets, + required this.tileMode, + required this.id, + }); + + final double fromX; + final double fromY; + final double toX; + final double toY; + final Int32List colors; + final Float32List? offsets; + final int tileMode; + final int id; + + @override + int get hashCode => Object.hash( + fromX, + fromY, + toX, + toY, + Object.hashAll(colors), + Object.hashAll(offsets ?? []), + tileMode, + id, + ); + + @override + bool operator ==(Object other) { + return other is OnLinearGradient && + other.fromX == fromX && + other.fromY == fromY && + other.toX == toX && + other.toY == toY && + _listEquals(other.colors, colors) && + _listEquals(other.offsets, offsets) && + other.tileMode == tileMode && + other.id == id; + } + + @override + String toString() { + return 'OnLinearGradient(' + 'fromX: $fromX, ' + 'toX: $toX, ' + 'fromY: $fromY, ' + 'toY: $toY, ' + 'colors: Int32List.fromList($colors), ' + 'offsets: Float32List.fromList($offsets), ' + 'tileMode: $tileMode, ' + 'id: $id)'; + } +} + +@immutable +class OnRadialGradient { + const OnRadialGradient({ + required this.centerX, + required this.centerY, + required this.radius, + required this.focalX, + required this.focalY, + required this.colors, + required this.offsets, + required this.transform, + required this.tileMode, + required this.id, + }); + + final double centerX; + final double centerY; + final double radius; + final double? focalX; + final double? focalY; + final Int32List colors; + final Float32List? offsets; + final Float64List? transform; + final int tileMode; + final int id; + + @override + int get hashCode => Object.hash( + centerX, + centerY, + radius, + focalX, + focalY, + Object.hashAll(colors), + Object.hashAll(offsets ?? []), + Object.hashAll(transform ?? []), + tileMode, + id, + ); + + @override + bool operator ==(Object other) { + return other is OnRadialGradient && + other.centerX == centerX && + other.centerY == centerY && + other.radius == radius && + other.focalX == focalX && + other.focalX == focalY && + _listEquals(other.colors, colors) && + _listEquals(other.offsets, offsets) && + _listEquals(other.transform, transform) && + other.tileMode == tileMode && + other.id == id; + } +} + +@immutable +class OnSaveLayer { + const OnSaveLayer(this.id); + + final int id; + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(Object other) => other is OnSaveLayer && other.id == id; +} + +@immutable +class OnClipPath { + const OnClipPath(this.id); + + final int id; + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(Object other) => other is OnClipPath && other.id == id; +} + +@immutable +class OnRestoreLayer { + const OnRestoreLayer(); +} + +@immutable +class OnDrawPath { + const OnDrawPath(this.pathId, this.paintId, this.patternId); + + final int pathId; + final int? paintId; + final int? patternId; + + @override + int get hashCode => Object.hash(pathId, paintId, patternId); + + @override + bool operator ==(Object other) => + other is OnDrawPath && + other.pathId == pathId && + other.paintId == paintId && + other.patternId == patternId; + + @override + String toString() => 'OnDrawPath($pathId, $paintId, $patternId)'; +} + +@immutable +class OnDrawVertices { + const OnDrawVertices(this.vertices, this.indices, this.paintId); + + final List vertices; + final List? indices; + final int? paintId; + + @override + int get hashCode => Object.hash(Object.hashAll(vertices), + Object.hashAll(indices ?? []), paintId); + + @override + bool operator ==(Object other) => + other is OnDrawVertices && + _listEquals(vertices, other.vertices) && + _listEquals(indices, other.indices) && + other.paintId == paintId; + + @override + String toString() => 'OnDrawVertices($vertices, $indices, $paintId)'; +} + +@immutable +class OnPaintObject { + const OnPaintObject({ + required this.color, + required this.strokeCap, + required this.strokeJoin, + required this.blendMode, + required this.strokeMiterLimit, + required this.strokeWidth, + required this.paintStyle, + required this.id, + required this.shaderId, + }); + + final int color; + final int? strokeCap; + final int? strokeJoin; + final int blendMode; + final double? strokeMiterLimit; + final double? strokeWidth; + final int paintStyle; + final int id; + final int? shaderId; + + @override + int get hashCode => Object.hash(color, strokeCap, strokeJoin, blendMode, + strokeMiterLimit, strokeWidth, paintStyle, id, shaderId); + + @override + bool operator ==(Object other) => + other is OnPaintObject && + other.color == color && + other.strokeCap == strokeCap && + other.strokeJoin == strokeJoin && + other.blendMode == blendMode && + other.strokeMiterLimit == strokeMiterLimit && + other.strokeWidth == strokeWidth && + other.paintStyle == paintStyle && + other.id == id && + other.shaderId == shaderId; + + @override + String toString() => + 'OnPaintObject(color: $color, strokeCap: $strokeCap, strokeJoin: $strokeJoin, ' + 'blendMode: $blendMode, strokeMiterLimit: $strokeMiterLimit, strokeWidth: $strokeWidth, ' + 'paintStyle: $paintStyle, id: $id, shaderId: $shaderId)'; +} + +@immutable +class OnPathClose { + const OnPathClose(); + + @override + int get hashCode => 44221; + + @override + bool operator ==(Object other) => other is OnPathClose; + + @override + String toString() => 'OnPathClose'; +} + +@immutable +class OnPathCubicTo { + const OnPathCubicTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double x2; + final double x3; + final double y1; + final double y2; + final double y3; + + @override + int get hashCode => Object.hash(x1, y1, x2, y2, x3, y3); + + @override + bool operator ==(Object other) => + other is OnPathCubicTo && + other.x1 == x1 && + other.y1 == y1 && + other.x2 == x2 && + other.y2 == y2 && + other.x3 == x3 && + other.y3 == y3; + + @override + String toString() => 'OnPathCubicTo($x1, $y1, $x2, $y2, $x3, $y3)'; +} + +@immutable +class OnPathFinished { + const OnPathFinished(); + + @override + int get hashCode => 1223; + + @override + bool operator ==(Object other) => other is OnPathFinished; + + @override + String toString() => 'OnPathFinished'; +} + +@immutable +class OnPathLineTo { + const OnPathLineTo(this.x, this.y); + + final double x; + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) => + other is OnPathLineTo && other.x == x && other.y == y; + + @override + String toString() => 'OnPathLineTo($x, $y)'; +} + +@immutable +class OnPathMoveTo { + const OnPathMoveTo(this.x, this.y); + + final double x; + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) => + other is OnPathMoveTo && other.x == x && other.y == y; + + @override + String toString() => 'OnPathMoveTo($x, $y)'; +} + +@immutable +class OnPathStart { + const OnPathStart(this.id, this.fillType); + + final int id; + final int fillType; + + @override + int get hashCode => Object.hash(id, fillType); + + @override + bool operator ==(Object other) => + other is OnPathStart && other.id == id && other.fillType == fillType; + + @override + String toString() => 'OnPathStart($id, $fillType)'; +} + +@immutable +class OnSize { + const OnSize(this.width, this.height); + + final double width; + final double height; + + @override + int get hashCode => Object.hash(width, height); + + @override + bool operator ==(Object other) => + other is OnSize && other.width == width && other.height == height; + + @override + String toString() => 'OnSize($width, $height)'; +} + +@immutable +class OnTextConfig { + const OnTextConfig( + this.text, + this.xAnchorMultiplier, + this.fontSize, + this.fontFamily, + this.fontWeight, + this.decoration, + this.decorationStyle, + this.decorationColor, + this.id, + ); + + final String text; + final double xAnchorMultiplier; + final double fontSize; + final String? fontFamily; + final int fontWeight; + final int decoration; + final int decorationStyle; + final int decorationColor; + final int id; + + @override + int get hashCode => Object.hash( + text, + xAnchorMultiplier, + fontSize, + fontFamily, + fontWeight, + decoration, + decorationStyle, + decorationColor, + id, + ); + + @override + bool operator ==(Object other) => + other is OnTextConfig && + other.text == text && + other.xAnchorMultiplier == xAnchorMultiplier && + other.fontSize == fontSize && + other.fontFamily == fontFamily && + other.fontWeight == fontWeight && + other.decoration == decoration && + other.decorationStyle == decorationStyle && + other.decorationColor == decorationColor && + other.id == id; + + @override + String toString() => + 'OnTextConfig($text, $fontSize, $fontFamily, $fontWeight, $decoration, $decorationStyle, $decorationColor, $id)'; +} + +@immutable +class OnDrawText { + const OnDrawText(this.textId, this.fillId, this.strokeId, this.patternId); + + final int textId; + final int? fillId; + final int? strokeId; + final int? patternId; + + @override + int get hashCode => Object.hash(textId, fillId, strokeId, patternId); + + @override + bool operator ==(Object other) => + other is OnDrawText && + other.textId == textId && + other.fillId == fillId && + other.strokeId == strokeId && + other.patternId == patternId; + + @override + String toString() => 'OnDrawText($textId, $fillId, $strokeId, $patternId)'; +} + +@immutable +class OnImage { + const OnImage(this.id, this.format, this.data, {this.onError}); + + final int id; + final int format; + final List data; + final VectorGraphicsErrorListener? onError; + + @override + int get hashCode => Object.hash(id, format, data, onError); + + @override + bool operator ==(Object other) => + other is OnImage && + other.id == id && + other.format == format && + other.onError == onError && + _listEquals(other.data, data); + + @override + String toString() => 'OnImage($id, $format, data:${data.length} bytes)'; +} + +@immutable +class OnDrawImage { + const OnDrawImage( + this.id, this.x, this.y, this.width, this.height, this.transform); + + final int id; + final double x; + final double y; + final double width; + final double height; + final Float64List? transform; + + @override + int get hashCode => Object.hash( + id, x, y, width, height, Object.hashAll(transform ?? const [])); + + @override + bool operator ==(Object other) { + return other is OnDrawImage && + other.id == id && + other.x == x && + other.y == y && + other.width == width && + other.height == height && + _listEquals(other.transform, transform); + } + + @override + String toString() => 'OnDrawImage($id, $x, $y, $width, $height, $transform)'; +} + +@immutable +class OnPatternStart { + const OnPatternStart( + this.patternId, this.x, this.y, this.width, this.height, this.transform); + + final int patternId; + final double x; + final double y; + final double width; + final double height; + final Float64List transform; + + @override + int get hashCode => + Object.hash(patternId, x, y, width, height, Object.hashAll(transform)); + + @override + bool operator ==(Object other) => + other is OnPatternStart && + other.patternId == patternId && + other.x == x && + other.y == y && + other.width == width && + other.height == height && + _listEquals(other.transform, transform); + + @override + String toString() => + 'OnPatternStart($patternId, $x, $y, $width, $height, $transform)'; +} + +bool _listEquals(List? left, List? right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + if (left.length != right.length) { + return false; + } + for (int i = 0; i < left.length; i++) { + if (left[i] != right[i]) { + return false; + } + } + return true; +} + +@immutable +class OnUpdateTextPosition { + const OnUpdateTextPosition(this.id); + + final int id; + + @override + int get hashCode => id; + + @override + bool operator ==(Object other) => + other is OnUpdateTextPosition && other.id == id; +} diff --git a/packages/vector_graphics_compiler/.gitignore b/packages/vector_graphics_compiler/.gitignore new file mode 100644 index 00000000000..3c8a157278c --- /dev/null +++ b/packages/vector_graphics_compiler/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/packages/vector_graphics_compiler/AUTHORS b/packages/vector_graphics_compiler/AUTHORS new file mode 100644 index 00000000000..557dff97933 --- /dev/null +++ b/packages/vector_graphics_compiler/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/vector_graphics_compiler/CHANGELOG.md b/packages/vector_graphics_compiler/CHANGELOG.md new file mode 100644 index 00000000000..51e080adfa8 --- /dev/null +++ b/packages/vector_graphics_compiler/CHANGELOG.md @@ -0,0 +1,120 @@ +## 1.1.12 + +* Transfers the package source from https://github.com/dnfield/vector_graphics + to https://github.com/flutter/packages. + +## 1.1.11+1 + +* Relax package:http constraint. + +## 1.1.11 + +* Use package:http to drop dependency on dart:html. + +## 1.1.10+1 + +* Add missing save before clip. + +## 1.1.10 + +* Add missing clip before saveLayer. + +## 1.1.9+2 + +* Fix case sensitivity on scientific notation parsing. + +## 1.1.9+1 + +* Fix publication error that did not have latest source code. + +## 1.1.9 + +* Fix handling of invalid XML `@id` attributes. +* Fix handling of self-referential `` elements. +* Add `--out-dir` option to compiler. +* Tweak warning message for unhandled eleemnts. + +## 1.1.8 + +* Fix bugs in transform parsing. + +## 1.1.7 + +* Support for matching the ambient text direction. + +## 1.1.6 + +* Fix bug in text position computation when transforms are involved. + +## 1.1.5+1 + +* Remove/update some invalid assertions related to image formats. + +## 1.1.5 + +* Support for encoding path control points as IEEE 754-2008 half precision + floating point values using the option `--use-half-precision-control-points`. +* Added an error builder property to provide a fallback widget on exceptions. + +## 1.1.4 + +* Support more image formats and malformed MIME types. +* Fix inheritence for `fill-rule`s. + +## 1.1.3 + +* Further improvements to whitespace handling for text. + +## 1.1.2 + +* Fix handling and inheritence of `none`. + +## 1.1.1 + +* Multiple text positioning bug fixes. +* Preserve stroke-opacity when specified. + +## 1.1.0 + +* Fix a number of inheritence related bugs: + * Inheritence of properties specified on the root element now work. + * Opacity inheritence is more correct now. + * Inheritence of `use` elements is more correctly handled. +* Make `currentColor` non-null on SVG theme, and fix how it is applied. +* Remove the opacity peephole optimizer, which was incorrectly applying + optimizations in a few cases. A future release may add this back. +* Add clipBehavior to the widget. +* Fix patterns when multiple patterns are specified and applied within the + graphic. + +## 1.0.1 + +* Fix handling of unspecified fill colors on use/group elements. + +## 1.0.0+1 + +* Fix issue in pattern decoding. +* Fix issue in matrix parsing for some combinations of matrices. + +## 1.0.0 + +* Initial stable release. +* Parsing is now synchronous, and is easier to work with in tests. +* Correctly handle images with `id`s and defined in `defs` blocks. +* Compile time color remapping support. + +## 0.0.3 + +* Better concurrency support +* Pattern support. +* Bug fixes around image handling. +* Bug fix for when optimizers are used on non-default fill types. +* Support for SVG theme related properties (currentColor, font-size, x-height). + +## 0.0.2 + +* Add optimizations for masks, clipping, and overdraw. + +## 0.0.1 + +* Create repository diff --git a/packages/vector_graphics_compiler/LICENSE b/packages/vector_graphics_compiler/LICENSE new file mode 100644 index 00000000000..c6823b81eb8 --- /dev/null +++ b/packages/vector_graphics_compiler/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/vector_graphics_compiler/README.md b/packages/vector_graphics_compiler/README.md new file mode 100644 index 00000000000..8997314b22a --- /dev/null +++ b/packages/vector_graphics_compiler/README.md @@ -0,0 +1,39 @@ +# vector_graphics_compiler + +A compiler for `package:vector_graphics`. + +This package parses SVG files into a format that the vector_graphics runtime +can render. + +## Features + +Supported SVG features: + +- Groups, paths, and basic shapes are all supported. +- References, including out of order references. +- Linear and radial gradients, including radial gradients with focal points. +- Text +- Symbols +- Images +- Patterns + +Unsupported SVG features: + +- Filters +- Some text processing attributes + +Optimizations: + +- Opacity peepholing +- Transformation inlining (except for text and radial gradients) +- Group collapsing +- Mask and clip elimination + +## Commemoration + +This package was originally authored by +[Dan Field](https://github.com/dnfield) and has been forked here +from [dnfield/vector_graphics](https://github.com/dnfield/vector_graphics). +Dan was a member of the Flutter team at Google from 2018 until his death +in 2024. Dan’s impact and contributions to Flutter were immeasurable, and we +honor his memory by continuing to publish and maintain this package. diff --git a/packages/vector_graphics_compiler/bin/util/isolate_processor.dart b/packages/vector_graphics_compiler/bin/util/isolate_processor.dart new file mode 100644 index 00000000000..eae0eae999e --- /dev/null +++ b/packages/vector_graphics_compiler/bin/util/isolate_processor.dart @@ -0,0 +1,185 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:vector_graphics_compiler/src/debug_format.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +/// The isolate processor distributes SVG compilation across multiple isolates. +class IsolateProcessor { + /// Create a new [IsolateProcessor]. + IsolateProcessor(this._libpathops, this._libtessellator, int concurrency) + : _pool = Pool(concurrency); + + final String? _libpathops; + final String? _libtessellator; + final Pool _pool; + + int _total = 0; + int _current = 0; + + /// Process the provided input/output [Pair] objects into vector graphics. + /// + /// Returns whether all requests were successful. + Future process( + List pairs, { + SvgTheme theme = const SvgTheme(), + required bool maskingOptimizerEnabled, + required bool clippingOptimizerEnabled, + required bool overdrawOptimizerEnabled, + required bool tessellate, + required bool dumpDebug, + required bool useHalfPrecisionControlPoints, + }) async { + _total = pairs.length; + _current = 0; + bool failure = false; + await Future.wait(eagerError: true, >[ + for (final Pair pair in pairs) + _process( + pair, + theme: theme, + maskingOptimizerEnabled: maskingOptimizerEnabled, + clippingOptimizerEnabled: clippingOptimizerEnabled, + overdrawOptimizerEnabled: overdrawOptimizerEnabled, + tessellate: tessellate, + dumpDebug: dumpDebug, + useHalfPrecisionControlPoints: useHalfPrecisionControlPoints, + libpathops: _libpathops, + libtessellator: _libtessellator, + ).catchError((dynamic error, [StackTrace? stackTrace]) { + failure = true; + print('XXXXXXXXXXX ${pair.inputPath} XXXXXXXXXXXXX'); + print(error); + print(stackTrace); + }), + ]); + if (failure) { + print('Some targets failed.'); + } + return !failure; + } + + static void _loadPathOps(String? libpathops) { + if (libpathops != null && libpathops.isNotEmpty) { + initializeLibPathOps(libpathops); + } else if (!initializePathOpsFromFlutterCache()) { + throw StateError('Could not find libpathops binary'); + } + } + + static void _loadTessellator(String? libtessellator) { + if (libtessellator != null && libtessellator.isNotEmpty) { + initializeLibTesselator(libtessellator); + } else if (!initializeTessellatorFromFlutterCache()) { + throw StateError('Could not find libtessellator binary'); + } + } + + Future _process( + Pair pair, { + required bool maskingOptimizerEnabled, + required bool clippingOptimizerEnabled, + required bool overdrawOptimizerEnabled, + required bool tessellate, + required bool dumpDebug, + required bool useHalfPrecisionControlPoints, + required String? libpathops, + required String? libtessellator, + SvgTheme theme = const SvgTheme(), + }) async { + PoolHandle? resource; + try { + resource = await _pool.request(); + await Isolate.run(() { + if (maskingOptimizerEnabled || + clippingOptimizerEnabled || + overdrawOptimizerEnabled) { + _loadPathOps(libpathops); + } + if (tessellate) { + _loadTessellator(libtessellator); + } + + final Uint8List bytes = encodeSvg( + xml: File(pair.inputPath).readAsStringSync(), + debugName: pair.inputPath, + theme: theme, + enableMaskingOptimizer: maskingOptimizerEnabled, + enableClippingOptimizer: clippingOptimizerEnabled, + enableOverdrawOptimizer: overdrawOptimizerEnabled, + useHalfPrecisionControlPoints: useHalfPrecisionControlPoints, + ); + File(pair.outputPath).writeAsBytesSync(bytes); + if (dumpDebug) { + final Uint8List debugBytes = dumpToDebugFormat(bytes); + File('${pair.outputPath}.debug').writeAsBytesSync(debugBytes); + } + }); + _current++; + print('Progress: $_current/$_total'); + } finally { + resource?.release(); + } + } +} + +/// A combination of an input file and its output file. +class Pair { + /// Create a new [Pair]. + const Pair(this.inputPath, this.outputPath); + + /// The path the SVG should be read from. + final String inputPath; + + /// The path the vector graphic will be written to. + final String outputPath; +} + +class Pool { + Pool(this.concurrency); + + final int concurrency; + final List active = []; + final List> pending = >[]; + + Future request() async { + if (active.length < concurrency) { + final PoolHandle handle = PoolHandle(this); + active.add(handle); + return handle; + } + final Completer completer = Completer(); + pending.add(completer); + return completer.future; + } + + void _clearAndCheckPending(PoolHandle oldHandle) { + assert(active.contains(oldHandle)); + active.remove(oldHandle); + while (active.length < concurrency && pending.isNotEmpty) { + final Completer completer = pending.removeAt(0); + final PoolHandle handle = PoolHandle(this); + active.add(handle); + completer.complete(handle); + } + } +} + +class PoolHandle { + PoolHandle(this.pool); + + Pool? pool; + + void release() { + assert(pool != null); + pool?._clearAndCheckPending(this); + } +} diff --git a/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart b/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart new file mode 100644 index 00000000000..ff857b4eaf5 --- /dev/null +++ b/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart @@ -0,0 +1,208 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; +import 'package:vector_graphics_compiler/src/svg/colors.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'util/isolate_processor.dart'; + +final ArgParser argParser = ArgParser() + ..addOption( + 'current-color', + help: 'The value (in ARGB format or a named SVG color) of the ' + '"currentColor" attribute.', + valueHelp: '0xFF000000', + defaultsTo: '0xFF000000', + ) + ..addOption( + 'font-size', + help: 'The basis for font size based values (i.e. em, ex).', + valueHelp: '14', + defaultsTo: '14', + ) + ..addOption( + 'x-height', + help: 'The x-height or corpus size of the font. If unspecified, defaults ' + 'to half of font-size.', + valueHelp: '7', + ) + ..addOption( + 'libtessellator', + help: 'The path to a libtessellator dynamic library', + valueHelp: 'path/to/libtessellator.dylib', + hide: true, + ) + ..addOption( + 'libpathops', + help: 'The path to a libpathops dynamic library', + valueHelp: 'path/to/libpath_ops.dylib', + hide: true, + ) + ..addFlag( + 'tessellate', + help: 'Convert path fills into a tessellated shape. This will improve ' + 'raster times at the cost of slightly larger file sizes.', + ) + ..addFlag( + 'optimize-masks', + help: 'Allows for masking optimizer to be enabled or disabled', + defaultsTo: true, + ) + ..addFlag( + 'optimize-clips', + help: 'Allows for clipping optimizer to be enabled or disabled', + defaultsTo: true, + ) + ..addFlag( + 'optimize-overdraw', + help: 'Allows for overdraw optimizer to be enabled or disabled', + defaultsTo: true, + ) + ..addOption( + 'input-dir', + help: 'The path to a directory containing one or more SVGs. ' + 'Only includes files that end with .svg. ' + 'Cannot be combined with --input or --output.', + ) + ..addOption( + 'out-dir', + help: 'The output directory path ' + 'use it with --input-dir to specific the output dirictory', + ) + ..addOption( + 'input', + abbr: 'i', + help: 'The path to a file containing a single SVG', + ) + ..addOption('concurrency', + abbr: 'k', + help: 'The maximum number of SVG processing isolates to spawn at once. ' + 'If not provided, defaults to the number of cores.') + ..addFlag('dump-debug', + help: + 'Dump a human readable debugging format alongside the compiled asset', + hide: true) + ..addOption( + 'output', + abbr: 'o', + help: + 'The path to a file where the resulting vector_graphic will be written.\n' + 'If not provided, defaults to .vec', + ) + ..addFlag('use-half-precision-control-points', + help: + 'Convert path control points into IEEE 754-2008 half precision floating point values.\n' + 'This reduces file size at the cost of lost precision at larger values.'); + +void validateOptions(ArgResults results) { + if (results.wasParsed('input-dir') && + (results.wasParsed('input') || results.wasParsed('output'))) { + print( + '--input-dir cannot be combined with --input and/or --output options.'); + exit(1); + } + if (!results.wasParsed('input') && !results.wasParsed('input-dir')) { + print('One of --input or --input-dir must be specified.'); + exit(1); + } +} + +SvgTheme _parseTheme(ArgResults results) { + Color? currentColor = namedColors[results['current-color']]; + if (currentColor == null) { + final int? argbValue = int.tryParse(results['current-color'] as String); + currentColor = Color(argbValue ?? 0xFF000000); + } + return SvgTheme( + currentColor: currentColor, + fontSize: double.tryParse(results['font-size'] as String) ?? 14, + xHeight: results.wasParsed('x-height') + ? double.tryParse(results['x-height'] as String) + : null, + ); +} + +Future main(List args) async { + final ArgResults results; + try { + results = argParser.parse(args); + } on FormatException catch (err) { + print(err.message); + print(argParser.usage); + exit(1); + } + validateOptions(results); + + final List pairs = []; + if (results.wasParsed('input-dir')) { + final Directory directory = Directory(results['input-dir'] as String); + if (!directory.existsSync()) { + print('input-dir ${directory.path} does not exist.'); + exit(1); + } + for (final File file + in directory.listSync(recursive: true).whereType()) { + if (!file.path.endsWith('.svg')) { + continue; + } + + String outputPath = '${file.path}.vec'; + + // to specfic the output directory when parse multi svg + if (results.wasParsed('out-dir')) { + final Directory outDir = Directory(results['out-dir'] as String); + //to add the output dirctory if it exist + if (!outDir.existsSync()) { + outDir.createSync(); + } + outputPath = p.join(outDir.path, '${p.basename(file.path)}.vec'); + } + + pairs.add(Pair(file.path, outputPath)); + } + } else { + final String inputFilePath = results['input'] as String; + final String outputFilePath = + results['output'] as String? ?? '$inputFilePath.vec'; + pairs.add(Pair(inputFilePath, outputFilePath)); + } + + final bool maskingOptimizerEnabled = results['optimize-masks'] == true; + final bool clippingOptimizerEnabled = results['optimize-clips'] == true; + final bool overdrawOptimizerEnabled = results['optimize-overdraw'] == true; + final bool tessellate = results['tessellate'] == true; + final bool dumpDebug = results['dump-debug'] == true; + final bool useHalfPrecisionControlPoints = + results['use-half-precision-control-points'] == true; + final int concurrency; + if (results.wasParsed('concurrency')) { + concurrency = int.parse(results['concurrency'] as String); + } else { + concurrency = Platform.numberOfProcessors; + } + + final IsolateProcessor processor = IsolateProcessor( + results['libpathops'] as String?, + results['libtessellator'] as String?, + concurrency, + ); + if (!await processor.process( + pairs, + theme: _parseTheme(results), + maskingOptimizerEnabled: maskingOptimizerEnabled, + clippingOptimizerEnabled: clippingOptimizerEnabled, + overdrawOptimizerEnabled: overdrawOptimizerEnabled, + tessellate: tessellate, + dumpDebug: dumpDebug, + useHalfPrecisionControlPoints: useHalfPrecisionControlPoints, + )) { + exit(1); + } +} diff --git a/packages/vector_graphics_compiler/dart_test.yaml b/packages/vector_graphics_compiler/dart_test.yaml new file mode 100644 index 00000000000..91ec220b8e2 --- /dev/null +++ b/packages/vector_graphics_compiler/dart_test.yaml @@ -0,0 +1 @@ +test_on: vm diff --git a/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_io.dart b/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_io.dart new file mode 100644 index 00000000000..0e4dad963c7 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_io.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'dart:io'; +import 'svg/path_ops.dart'; + +/// Look up the location of the pathops from flutter's artifact cache. +bool initializePathOpsFromFlutterCache() { + final Directory cacheRoot; + if (Platform.resolvedExecutable.contains('flutter_tester')) { + cacheRoot = File(Platform.resolvedExecutable).parent.parent.parent.parent; + } else if (Platform.resolvedExecutable.contains('dart')) { + cacheRoot = File(Platform.resolvedExecutable).parent.parent.parent; + } else { + print('Unknown executable: ${Platform.resolvedExecutable}'); + return false; + } + + final String platform; + final String executable; + if (Platform.isWindows) { + platform = 'windows-x64'; + executable = 'path_ops.dll'; + } else if (Platform.isMacOS) { + platform = 'darwin-x64'; + executable = 'libpath_ops.dylib'; + } else if (Platform.isLinux) { + platform = 'linux-x64'; + executable = 'libpath_ops.so'; + } else { + print('path_ops not supported on ${Platform.localeName}'); + return false; + } + final String pathops = + '${cacheRoot.path}/artifacts/engine/$platform/$executable'; + if (!File(pathops).existsSync()) { + print('Could not locate libpathops at $pathops.'); + print('Ensure you are on a supported version of flutter and then run '); + print('"flutter precache".'); + return false; + } + initializeLibPathOps(pathops); + return true; +} diff --git a/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_web.dart b/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_web.dart new file mode 100644 index 00000000000..ba823d879b4 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/_initialize_path_ops_web.dart @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +/// Look up the location of the pathops from flutter's artifact cache. +bool initializePathOpsFromFlutterCache() { + print('PathOps not supported on web.'); + return false; +} diff --git a/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_io.dart b/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_io.dart new file mode 100644 index 00000000000..9c4fa596aa3 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_io.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'dart:io'; +import 'svg/tessellator.dart'; + +/// Look up the location of the tessellator from flutter's artifact cache. +bool initializeTessellatorFromFlutterCache() { + final Directory cacheRoot; + if (Platform.resolvedExecutable.contains('flutter_tester')) { + cacheRoot = File(Platform.resolvedExecutable).parent.parent.parent.parent; + } else if (Platform.resolvedExecutable.contains('dart')) { + cacheRoot = File(Platform.resolvedExecutable).parent.parent.parent; + } else { + print('Unknown executable: ${Platform.resolvedExecutable}'); + return false; + } + + final String platform; + final String executable; + if (Platform.isWindows) { + platform = 'windows-x64'; + executable = 'libtessellator.dll'; + } else if (Platform.isMacOS) { + platform = 'darwin-x64'; + executable = 'libtessellator.dylib'; + } else if (Platform.isLinux) { + platform = 'linux-x64'; + executable = 'libtessellator.so'; + } else { + print('Tesselation not supported on ${Platform.localeName}'); + return false; + } + final String tessellator = + '${cacheRoot.path}/artifacts/engine/$platform/$executable'; + if (!File(tessellator).existsSync()) { + print('Could not locate libtessellator at $tessellator.'); + print('Ensure you are on a supported version of flutter and then run '); + print('"flutter precache".'); + return false; + } + initializeLibTesselator(tessellator); + return true; +} diff --git a/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_web.dart b/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_web.dart new file mode 100644 index 00000000000..c232520d2cc --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/_initialize_tessellator_web.dart @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +/// Look up the location of the tessellator from flutter's artifact cache. +bool initializeTessellatorFromFlutterCache() { + print('Tesselation not supported on web'); + return false; +} diff --git a/packages/vector_graphics_compiler/lib/src/debug_format.dart b/packages/vector_graphics_compiler/lib/src/debug_format.dart new file mode 100644 index 00000000000..e065cfbdf7d --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/debug_format.dart @@ -0,0 +1,229 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'paint.dart'; + +/// Write an unstable but human readable form of the vector graphics binary +/// package intended to be used for debugging and development. +Uint8List dumpToDebugFormat(Uint8List bytes) { + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final _DebugVectorGraphicsListener listener = _DebugVectorGraphicsListener(); + final DecodeResponse response = + codec.decode(bytes.buffer.asByteData(), listener); + if (!response.complete) { + codec.decode(bytes.buffer.asByteData(), listener, response: response); + } + // Newer versions of Dart will make this a Uint8List and not require the cast. + // ignore: unnecessary_cast + return utf8.encode(listener.buffer.toString()) as Uint8List; +} + +String _intToColor(int value) { + return 'Color(0x${(value & 0xFFFFFFFF).toRadixString(16).padLeft(8, '0')})'; +} + +class _DebugVectorGraphicsListener extends VectorGraphicsCodecListener { + final StringBuffer buffer = StringBuffer(); + + @override + void onClipPath(int pathId) { + buffer.writeln('DrawClip: id:$pathId'); + } + + @override + void onDrawImage(int imageId, double x, double y, double width, double height, + Float64List? transform) { + buffer.writeln( + 'DrawImage: id:$imageId (Rect.fromLTWH($x, $y, $width, $height), transform: $transform)'); + } + + @override + void onDrawPath(int pathId, int? paintId, int? patternId) { + final String patternContext = + patternId != null ? ', patternId:$patternId' : ''; + buffer.writeln('DrawPath: id:$pathId (paintId:$paintId$patternContext)'); + } + + @override + void onDrawText(int textId, int? fillId, int? strokeId, int? patternId) { + buffer.writeln( + 'DrawText: id:$textId (fill: $fillId, stroke: $strokeId, pattern: $patternId)'); + } + + @override + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) { + buffer.writeln('DrawVertices: $vertices ($indices, paintId: $paintId)'); + } + + @override + void onImage( + int imageId, + int format, + Uint8List data, { + VectorGraphicsErrorListener? onError, + }) { + buffer.writeln( + 'StoreImage: id:$imageId (format:$format, byteLength:${data.lengthInBytes}'); + } + + @override + void onLinearGradient(double fromX, double fromY, double toX, double toY, + Int32List colors, Float32List? offsets, int tileMode, int id) { + buffer.writeln( + 'StoreGradient: id:$id Linear(\n' + ' from: ($fromX, $fromY)\n' + ' to: ($toX, $toY)\n' + ' colors: [${colors.map(_intToColor).join(',')}]\n' + ' offsets: $offsets\n' + ' tileMode: ${TileMode.values[tileMode].name}', + ); + } + + @override + void onMask() { + buffer.writeln('BeginMask:'); + } + + @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, + }) { + // Fill + if (paintStyle == 0) { + buffer.writeln( + 'StorePaint: id:$id Fill(${_intToColor(color)}, blendMode: ${BlendMode.values[blendMode].name}, shader: $shaderId)'); + } else { + buffer.writeln( + 'StorePaint: id:$id Stroke(${_intToColor(color)}, strokeCap: $strokeCap, $strokeJoin: $strokeJoin, ' + 'blendMode: ${BlendMode.values[blendMode].name}, strokeMiterLimit: $strokeMiterLimit, strokeWidth: $strokeWidth, shader: $shaderId)'); + } + } + + @override + void onPathClose() { + buffer.writeln(' close()'); + } + + @override + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + buffer.writeln(' cubicTo(($x1, $y1), ($x2, $y2), ($x3, $y3)'); + } + + @override + void onPathFinished() { + buffer.writeln('EndPath:'); + } + + @override + void onPathLineTo(double x, double y) { + buffer.writeln(' lineTo($x, $y)'); + } + + @override + void onPathMoveTo(double x, double y) { + buffer.writeln(' moveTo($x, $y)'); + } + + @override + void onPathStart(int id, int fillType) { + buffer + .writeln('PathStart: id:$id ${fillType == 0 ? 'nonZero' : 'evenOdd'}'); + } + + @override + void onPatternStart(int patternId, double x, double y, double width, + double height, Float64List transform) { + buffer.writeln( + 'StorePattern: $patternId (Rect.fromLTWH($x, $y, $width, $height), transform: $transform)'); + } + + @override + void onRadialGradient( + double centerX, + double centerY, + double radius, + double? focalX, + double? focalY, + Int32List colors, + Float32List? offsets, + Float64List? transform, + int tileMode, + int id) { + final bool hasFocal = focalX != null; + buffer.writeln( + 'StoreGradient: id:$id Radial(\n' + 'center: ($centerX, $centerY)\n' + 'radius: $radius\n' + '${hasFocal ? 'focal: ($focalX, $focalY)\n' : ''}' + 'colors: [${colors.map(_intToColor).join(',')}]\n' + 'offsets: $offsets\n' + 'transform: $transform\n' + 'tileMode: ${TileMode.values[tileMode].name}', + ); + } + + @override + void onRestoreLayer() { + buffer.writeln('Restore:'); + } + + @override + void onSaveLayer(int paintId) { + buffer.writeln('SaveLayer: $paintId'); + } + + @override + void onSize(double width, double height) { + buffer.writeln('RecordSize: Size($width, $height)'); + } + + @override + void onTextConfig( + String text, + String? fontFamily, + double xAnchorMultiplier, + int fontWeight, + double fontSize, + int decoration, + int decorationStyle, + int decorationColor, + int id, + ) { + buffer.writeln( + 'RecordText: id:$id ($text, ($xAnchorMultiplier x-anchoring), weight: $fontWeight, size: $fontSize, decoration: $decoration, decorationStyle: $decorationStyle, decorationColor: 0x${decorationColor.toRadixString(16)}, family: $fontFamily)'); + } + + @override + void onTextPosition( + int id, + double? x, + double? y, + double? dx, + double? dy, + bool reset, + Float64List? transform, + ) { + buffer.writeln( + 'StoreTextPosition: id:$id (($x, $y) d($dx, $dy), reset: $reset, transform: $transform)'); + } + + @override + void onUpdateTextPosition(int id) { + buffer.writeln('UpdateTextPosition: id:$id'); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/draw_command_builder.dart b/packages/vector_graphics_compiler/lib/src/draw_command_builder.dart new file mode 100644 index 00000000000..3d012129d1b --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/draw_command_builder.dart @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'geometry/image.dart'; +import 'geometry/matrix.dart'; +import 'geometry/path.dart'; +import 'geometry/pattern.dart'; +import 'geometry/vertices.dart'; +import 'paint.dart'; +import 'svg/resolver.dart'; +import 'vector_instructions.dart'; + +/// An interface for building up a stack of vector commands. +class DrawCommandBuilder { + final Map _paints = {}; + final Map _paths = {}; + final Map _text = {}; + final Map _images = {}; + final Map _drawImages = {}; + final Map _vertices = {}; + final List _commands = []; + final Map _patterns = {}; + final Map _patternData = {}; + final Map _textPositions = {}; + + int _getOrGenerateId(T object, Map map) => + map.putIfAbsent(object, () => map.length); + + /// Add a vertices to the command stack. + void addVertices(IndexedVertices vertices, Paint paint) { + final int paintId = _getOrGenerateId(paint, _paints); + final int verticesId = _getOrGenerateId(vertices, _vertices); + _commands.add(DrawCommand( + DrawCommandType.vertices, + paintId: paintId, + objectId: verticesId, + )); + } + + /// Add a save layer to the command stack. + void addSaveLayer(Paint paint) { + final int paintId = _getOrGenerateId(paint, _paints); + _commands.add(DrawCommand( + DrawCommandType.saveLayer, + paintId: paintId, + )); + } + + /// Add a restore to the command stack. + void restore() { + _commands.add(const DrawCommand(DrawCommandType.restore)); + } + + /// Adds a clip to the command stack. + void addClip(Path path) { + final int pathId = _getOrGenerateId(path, _paths); + _commands.add(DrawCommand(DrawCommandType.clip, objectId: pathId)); + } + + /// Adds a mask to the command stack. + void addMask() { + _commands.add(const DrawCommand(DrawCommandType.mask)); + } + + /// Adds a pattern to the command stack. + void addPattern( + Object id, { + required double x, + required double y, + required double width, + required double height, + required AffineMatrix transform, + }) { + final int patternId = _getOrGenerateId(id, _patterns); + final int patternDataId = _getOrGenerateId( + PatternData(x, y, width, height, transform), + _patternData, + ); + _commands.add(DrawCommand( + DrawCommandType.pattern, + objectId: patternId, + patternDataId: patternDataId, + )); + } + + /// Updates the current text position to [position]. + void updateTextPosition(TextPosition position) { + final int positionId = _getOrGenerateId(position, _textPositions); + _commands.add(DrawCommand( + DrawCommandType.textPosition, + objectId: positionId, + )); + } + + /// Add a path to the current draw command stack + void addPath(Path path, Paint paint, String? debugString, Object? patternId) { + if (path.isEmpty) { + return; + } + final int pathId = _getOrGenerateId(path, _paths); + final int paintId = _getOrGenerateId(paint, _paints); + + _commands.add(DrawCommand(DrawCommandType.path, + objectId: pathId, + paintId: paintId, + debugString: debugString, + patternId: patternId != null ? _patterns[patternId] : null)); + } + + /// Adds a text to the current draw command stack. + void addText( + TextConfig textConfig, + Paint paint, + String? debugString, + Object? patternId, + ) { + final int paintId = _getOrGenerateId(paint, _paints); + final int styleId = _getOrGenerateId(textConfig, _text); + _commands.add(DrawCommand( + DrawCommandType.text, + objectId: styleId, + paintId: paintId, + debugString: debugString, + patternId: patternId != null ? _patterns[patternId] : null, + patternDataId: patternId != null ? _patternData[patternId] : null, + )); + } + + /// Add an image to the current draw command stack. + void addImage(ResolvedImageNode node, String? debugString) { + final ImageData imageData = ImageData(node.data, node.format.index); + final int imageId = _getOrGenerateId(imageData, _images); + final DrawImageData drawImageData = DrawImageData( + imageId, + node.rect, + node.transform, + ); + + final int drawImageId = _getOrGenerateId(drawImageData, _drawImages); + _commands.add(DrawCommand( + DrawCommandType.image, + objectId: drawImageId, + debugString: debugString, + )); + } + + /// Create a new [VectorInstructions] with the given width and height. + VectorInstructions toInstructions(double width, double height) { + return VectorInstructions( + width: width, + height: height, + paints: _paints.keys.toList(), + paths: _paths.keys.toList(), + text: _text.keys.toList(), + vertices: _vertices.keys.toList(), + images: _images.keys.toList(), + drawImages: _drawImages.keys.toList(), + commands: _commands, + patternData: _patternData.keys.toList(), + textPositions: _textPositions.keys.toList(), + ); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart b/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart new file mode 100644 index 00000000000..25820084fd9 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart @@ -0,0 +1,165 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; +import 'package:meta/meta.dart'; + +import '../util.dart'; + +/// An immutable position in two-dimensional space. +/// +/// This class is roughly compatible with dart:ui's Offset. +@immutable +class Point { + /// Creates a point object with x,y coordinates. + const Point(this.x, this.y); + + /// The point at the origin of coordinate space. + static const Point zero = Point(0, 0); + + /// Linearly interpolate between two points. + /// + /// The [t] argument represents a position on the timeline, with 0.0 meaning + /// interpolation has not started and 1.0 meaning interpolation has finished. + /// + /// At the start the returned value equals [a], and at the end it equals [b]. As + /// the number advances from 0 to 1 it returns a value closer to a or b + /// respectively. + static Point lerp(Point a, Point b, double t) { + return Point( + lerpDouble(a.x, b.x, t), + lerpDouble(a.y, b.y, t), + ); + } + + /// The distance between points [a] and [b]. + static double distance(Point a, Point b) { + final double x = a.x - b.x; + final double y = a.y - b.y; + return math.sqrt((x * x) + (y * y)); + } + + /// The offset along the x-axis of this point. + final double x; + + /// The offset along the y-axis of this point. + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) { + return other is Point && other.x == x && other.y == y; + } + + /// Returns a point whose coordinates are the coordinates of the + /// left-hand-side operand (a Point) divided by the scalar right-hand-side + /// operand (a double). + Point operator /(double divisor) { + return Point(x / divisor, y / divisor); + } + + /// Returns a point whose coordinates are the coordinates of the + /// left-hand-side operand (a Point) multiplied by the scalar right-hand-side + /// operand (a double). + Point operator *(double multiplicand) { + return Point(x * multiplicand, y * multiplicand); + } + + /// Returns a point whose coordinates are the coordinates of the + /// left-hand-side operand (a Point) added to the right-hand-side + /// coordinates (a Point). + Point operator +(Point other) { + return Point(x + other.x, y + other.y); + } + + @override + String toString() => 'Point($x, $y)'; +} + +/// An immutable, 2D, axis-aligned, floating-point rectangle whose coordinates +/// are relative to a given origin. +@immutable +class Rect { + /// Creates a rectangle from the specified left, top, right, and bottom + /// positions. + const Rect.fromLTRB(this.left, this.top, this.right, this.bottom); + + /// Creates a rectangle from the specified left and top positions with width + /// and height dimensions. + const Rect.fromLTWH(double left, double top, double width, double height) + : this.fromLTRB(left, top, left + width, top + height); + + /// Creates a rectangle representing a circle with centerpoint `x,`y` and + /// radius `r`. + const Rect.fromCircle(double x, double y, double r) + : this.fromLTRB(x - r, y - r, x + r, y + r); + + /// A rectangle covering the entire coordinate space, equal to dart:ui's + /// definition. + static const Rect largest = Rect.fromLTRB(-1e9, -1e9, 1e9, 1e9); + + /// A rectangle with the top, left, right, and bottom edges all at zero. + static const Rect zero = Rect.fromLTRB(0, 0, 0, 0); + + /// The x-axis offset of left edge. + final double left; + + /// The y-axis offset of the top edge. + final double top; + + /// The x-axis offset of the right edge. + final double right; + + /// The y-axis offset of the bottom edge. + final double bottom; + + /// The width of the rectangle. + double get width => right - left; + + /// The height of the rectangle. + double get height => bottom - top; + + /// Creates the smallest rectangle that covers the edges of this and `other`. + Rect expanded(Rect other) { + return Rect.fromLTRB( + math.min(left, other.left), + math.min(top, other.top), + math.max(right, other.right), + math.max(bottom, other.bottom), + ); + } + + /// Whether or not the rect has any contents. + bool get isEmpty => width == 0 || height == 0; + + /// Whether or not [other] intersect this rectangle. + /// + /// This only works for sorted rectangles. + bool intersects(Rect other) { + assert(other.left <= other.right && other.top <= other.bottom); + assert(left <= right && top <= bottom); + if (isEmpty || other.isEmpty) { + return false; + } + return math.max(left, other.left) < math.min(right, other.right) && + math.max(top, other.top) < math.min(bottom, other.bottom); + } + + @override + int get hashCode => Object.hash(left, top, right, bottom); + + @override + bool operator ==(Object other) { + return other is Rect && + other.left == left && + other.top == top && + other.right == right && + other.bottom == bottom; + } + + @override + String toString() => 'Rect.fromLTRB($left, $top, $right, $bottom)'; +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/image.dart b/packages/vector_graphics_compiler/lib/src/geometry/image.dart new file mode 100644 index 00000000000..77e05171a85 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/image.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'basic_types.dart'; +import 'matrix.dart'; + +/// The encoded image data and its format. +class ImageData { + /// Create a new [ImageData]. + const ImageData( + this.data, + this.format, + ); + + /// An encoded image. + final Uint8List data; + + /// The encoding format of the [data]. + final int format; +} + +/// A command to draw an image at a particular location. +class DrawImageData { + /// Create a new [DrawImageData]. + const DrawImageData( + this.id, + this.rect, + this.transform, + ); + + /// The corresponding encoding image to draw. + final int id; + + /// The x position of the image in pixels. + final Rect rect; + + /// An optional transform, if the position cannot be fully described + /// by [rect]. + final AffineMatrix? transform; +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart b/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart new file mode 100644 index 00000000000..ff800343622 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart @@ -0,0 +1,446 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import '../../vector_graphics_compiler.dart'; + +import 'basic_types.dart'; + +/// An immutable affine matrix, a 3x3 column-major-order matrix in which the +/// last row is always set to the identity values, i.e. `0 0 1`. +@immutable +class AffineMatrix { + /// Creates an immutable affine matrix. To work with the identity matrix, use + /// the [identity] property. + const AffineMatrix( + this.a, + this.b, + this.c, + this.d, + this.e, + this.f, [ + double? m4_10, + ]) : _m4_10 = m4_10 ?? (1.0 * a); + + /// The identity affine matrix. + static const AffineMatrix identity = AffineMatrix(1, 0, 0, 1, 0, 0); + + /// The 0,0 position of the matrix. + final double a; + + /// The 0,1 position of the matrix. + final double b; + + /// The 1,0 position of the matrix. + final double c; + + /// The 1,1 position of the matrix. + final double d; + + /// The 2,0 position of the matrix. + final double e; + + /// The 1,2 position of the matrix. + final double f; + + /// Translations can affect this value, so we have to track it. + final double _m4_10; + + /// Calculates the scale for a stroke width based on the average of the x- and + /// y-axis scales of this matrix. + double? scaleStrokeWidth(double? width) { + if (width == null || (a == 1 && d == 1)) { + return width; + } + + final double xScale = math.sqrt(a * a + c * c); + final double yScale = math.sqrt(b * b + d * d); + + return (xScale + yScale) / 2 * width; + } + + /// Creates a new affine matrix rotated by `radians`. + AffineMatrix rotated(double radians) { + if (radians == 0) { + return this; + } + final double cosAngle = math.cos(radians); + final double sinAngle = math.sin(radians); + return AffineMatrix( + (a * cosAngle) + (c * sinAngle), + (b * cosAngle) + (d * sinAngle), + (a * -sinAngle) + (c * cosAngle), + (b * -sinAngle) + (d * cosAngle), + e, + f, + _m4_10, + ); + } + + /// Whether this matrix can be expressed be applied to a rect without any loss + /// of inforamtion. + /// + /// In other words, if this matrix is a simple translate and/or non-negative + /// scale with no rotation or skew, this property is true. Otherwise, it is + /// false. + bool get encodableInRect { + return a > 0 && b == 0 && c == 0 && d > 0 && _m4_10 == a; + } + + /// Creates a new affine matrix rotated by `x` and `y`. + /// + /// If `y` is not specified, it is defaulted to the same value as `x`. + AffineMatrix scaled(double x, [double? y]) { + y ??= x; + if (x == 1 && y == 1) { + return this; + } + return AffineMatrix( + a * x, + b * x, + c * y, + d * y, + e, + f, + _m4_10 * x, + ); + } + + /// Creates a new affine matrix, translated along the x and y axis. + AffineMatrix translated(double x, double y) { + return AffineMatrix( + a, + b, + c, + d, + (a * x) + (c * y) + e, + (b * x) + (d * y) + f, + _m4_10, + ); + } + + /// Creates a new affine matrix, skewed along the x axis. + AffineMatrix xSkewed(double x) { + return multiplied(AffineMatrix( + identity.a, + identity.b, + math.tan(x), + identity.d, + identity.e, + identity.f, + identity._m4_10, + )); + } + + /// Creates a new affine matrix, skewed along the y axis. + AffineMatrix ySkewed(double y) { + return multiplied(AffineMatrix( + identity.a, + math.tan(y), + identity.c, + identity.d, + identity.e, + identity.f, + identity._m4_10, + )); + } + + /// Creates a new affine matrix of this concatenated with `other`. + AffineMatrix multiplied(AffineMatrix other) { + return AffineMatrix( + (a * other.a) + (c * other.b), + (b * other.a) + (d * other.b), + (a * other.c) + (c * other.d), + (b * other.c) + (d * other.d), + (a * other.e) + (c * other.f) + e, + (b * other.e) + (d * other.f) + f, + _m4_10 * other._m4_10, + ); + } + + /// Maps `point` using the values of this matrix. + Point transformPoint(Point point) { + return Point( + (a * point.x) + (c * point.y) + e, + (b * point.x) + (d * point.y) + f, + ); + } + + /// Maps `rect` using the values of this matrix. + Rect transformRect(Rect rect) { + return _transformRect(toMatrix4(), rect); + } + + /// Creates a typed data representatino of this matrix suitable for use with + /// `package:vector_math_64` (and, by extension, Flutter/dart:ui). + Float64List toMatrix4() { + return Float64List.fromList([ + a, b, 0, 0, // + c, d, 0, 0, // + 0, 0, _m4_10, 0, // + e, f, 0, 1.0, // + ]); + } + + @override + int get hashCode => Object.hash(a, b, c, d, e, f, _m4_10); + + @override + bool operator ==(Object other) { + return other is AffineMatrix && + other.a == a && + other.b == b && + other.c == c && + other.d == d && + other.e == e && + other.f == f && + other._m4_10 == _m4_10; + } + + @override + String toString() => ''' +[ $a, $c, $e ] +[ $b, $d, $f ] +[ 0.0, 0.0, 1.0 ] // _m4_10 = $_m4_10 +'''; +} + +// transformRect implementation from package:flutter. + +/// Returns a rect that bounds the result of applying the given matrix as a +/// perspective transform to the given rect. +/// +/// This function assumes the given rect is in the plane with z equals 0.0. +/// The transformed rect is then projected back into the plane with z equals +/// 0.0 before computing its bounding rect. +Rect _transformRect(Float64List transform, Rect rect) { + final Float64List storage = transform; + final double x = rect.left; + final double y = rect.top; + final double w = rect.right - x; + final double h = rect.bottom - y; + + // We want to avoid turning a finite rect into an infinite one if we can. + assert(w.isFinite && h.isFinite, '($w, $h)'); + + // Transforming the 4 corners of a rectangle the straightforward way + // incurs the cost of transforming 4 points using vector math which + // involves 48 multiplications and 48 adds and then normalizing + // the points using 4 inversions of the homogeneous weight factor + // and then 12 multiplies. Once we have transformed all of the points + // we then need to turn them into a bounding box using 4 min/max + // operations each on 4 values yielding 12 total comparisons. + // + // On top of all of those operations, using the vector_math package to + // do the work for us involves allocating several objects in order to + // communicate the values back and forth - 4 allocating getters to extract + // the [Offset] objects for the corners of the [Rect], 4 conversions to + // a [Vector3] to use [Matrix4.perspectiveTransform()], and then 4 new + // [Offset] objects allocated to hold those results, yielding 8 [Offset] + // and 4 [Vector3] object allocations per rectangle transformed. + // + // But the math we really need to get our answer is actually much less + // than that. + // + // First, consider that a full point transform using the vector math + // package involves expanding it out into a vector3 with a Z coordinate + // of 0.0 and then performing 3 multiplies and 3 adds per coordinate: + // ``` + // xt = x*m00 + y*m10 + z*m20 + m30; + // yt = x*m01 + y*m11 + z*m21 + m31; + // zt = x*m02 + y*m12 + z*m22 + m32; + // wt = x*m03 + y*m13 + z*m23 + m33; + // ``` + // Immediately we see that we can get rid of the 3rd column of multiplies + // since we know that Z=0.0. We can also get rid of the 3rd row because + // we ignore the resulting Z coordinate. Finally we can get rid of the + // last row if we don't have a perspective transform since we can verify + // that the results are 1.0 for all points. This gets us down to 16 + // multiplies and 16 adds in the non-perspective case and 24 of each for + // the perspective case. (Plus the 12 comparisons to turn them back into + // a bounding box.) + // + // But we can do even better than that. + // + // Under optimal conditions of no perspective transformation, + // which is actually a very common condition, we can transform + // a rectangle in as little as 3 operations: + // + // (rx,ry) = transform of upper left corner of rectangle + // (wx,wy) = delta transform of the (w, 0) width relative vector + // (hx,hy) = delta transform of the (0, h) height relative vector + // + // A delta transform is a transform of all elements of the matrix except + // for the translation components. The translation components are added + // in at the end of each transform computation so they represent a + // constant offset for each point transformed. A delta transform of + // a horizontal or vertical vector involves a single multiplication due + // to the fact that it only has one non-zero coordinate and no addition + // of the translation component. + // + // In the absence of a perspective transform, the transformed + // rectangle will be mapped into a parallelogram with corners at: + // corner1 = (rx, ry) + // corner2 = corner1 + dTransformed width vector = (rx+wx, ry+wy) + // corner3 = corner1 + dTransformed height vector = (rx+hx, ry+hy) + // corner4 = corner1 + both dTransformed vectors = (rx+wx+hx, ry+wy+hy) + // In all, this method of transforming the rectangle requires only + // 8 multiplies and 12 additions (which we can reduce to 8 additions if + // we only need a bounding box, see below). + // + // In the presence of a perspective transform, the above conditions + // continue to hold with respect to the non-normalized coordinates so + // we can still save a lot of multiplications by computing the 4 + // non-normalized coordinates using relative additions before we normalize + // them and they lose their "pseudo-parallelogram" relationships. We still + // have to do the normalization divisions and min/max all 4 points to + // get the resulting transformed bounding box, but we save a lot of + // calculations over blindly transforming all 4 coordinates independently. + // In all, we need 12 multiplies and 22 additions to construct the + // non-normalized vectors and then 8 divisions (or 4 inversions and 8 + // multiplies) for normalization (plus the standard set of 12 comparisons + // for the min/max bounds operations). + // + // Back to the non-perspective case, the optimization that lets us get + // away with fewer additions if we only need a bounding box comes from + // analyzing the impact of the relative vectors on expanding the + // bounding box of the parallelogram. First, the bounding box always + // contains the transformed upper-left corner of the rectangle. Next, + // each relative vector either pushes on the left or right side of the + // bounding box and also either the top or bottom side, depending on + // whether it is positive or negative. Finally, you can consider the + // impact of each vector on the bounding box independently. If, say, + // wx and hx have the same sign, then the limiting point in the bounding + // box will be the one that involves adding both of them to the origin + // point. If they have opposite signs, then one will push one wall one + // way and the other will push the opposite wall the other way and when + // you combine both of them, the resulting "opposite corner" will + // actually be between the limits they established by pushing the walls + // away from each other, as below: + // ``` + // +---------(originx,originy)--------------+ + // | -----^---- | + // | ----- ---- | + // | ----- ---- | + // (+hx,+hy)< ---- | + // | ---- ---- | + // | ---- >(+wx,+wy) + // | ---- ----- | + // | ---- ----- | + // | ---- ----- | + // | v | + // +---------------(+wx+hx,+wy+hy)----------+ + // ``` + // In this diagram, consider that: + // ``` + // wx would be a positive number + // hx would be a negative number + // wy and hy would both be positive numbers + // ``` + // As a result, wx pushes out the right wall, hx pushes out the left wall, + // and both wy and hy push down the bottom wall of the bounding box. The + // wx,hx pair (of opposite signs) worked on opposite walls and the final + // opposite corner had an X coordinate between the limits they established. + // The wy,hy pair (of the same sign) both worked together to push the + // bottom wall down by their sum. + // + // This relationship allows us to simply start with the point computed by + // transforming the upper left corner of the rectangle, and then + // conditionally adding wx, wy, hx, and hy to either the left or top + // or right or bottom of the bounding box independently depending on sign. + // In that case we only need 4 comparisons and 4 additions total to + // compute the bounding box, combined with the 8 multiplications and + // 4 additions to compute the transformed point and relative vectors + // for a total of 8 multiplies, 8 adds, and 4 comparisons. + // + // An astute observer will note that we do need to do 2 subtractions at + // the top of the method to compute the width and height. Add those to + // all of the relative solutions listed above. The test for perspective + // also adds 3 compares to the affine case and up to 3 compares to the + // perspective case (depending on which test fails, the rest are omitted). + // + // The final tally: + // basic method = 60 mul + 48 add + 12 compare + // optimized perspective = 12 mul + 22 add + 15 compare + 2 sub + // optimized affine = 8 mul + 8 add + 7 compare + 2 sub + // + // Since compares are essentially subtractions and subtractions are + // the same cost as adds, we end up with: + // basic method = 60 mul + 60 add/sub/compare + // optimized perspective = 12 mul + 39 add/sub/compare + // optimized affine = 8 mul + 17 add/sub/compare + + final double wx = storage[0] * w; + final double hx = storage[4] * h; + final double rx = storage[0] * x + storage[4] * y + storage[12]; + + final double wy = storage[1] * w; + final double hy = storage[5] * h; + final double ry = storage[1] * x + storage[5] * y + storage[13]; + + if (storage[3] == 0.0 && storage[7] == 0.0 && storage[15] == 1.0) { + double left = rx; + double right = rx; + if (wx < 0) { + left += wx; + } else { + right += wx; + } + if (hx < 0) { + left += hx; + } else { + right += hx; + } + + double top = ry; + double bottom = ry; + if (wy < 0) { + top += wy; + } else { + bottom += wy; + } + if (hy < 0) { + top += hy; + } else { + bottom += hy; + } + + return Rect.fromLTRB(left, top, right, bottom); + } else { + final double ww = storage[3] * w; + final double hw = storage[7] * h; + final double rw = storage[3] * x + storage[7] * y + storage[15]; + + final double ulx = rx / rw; + final double uly = ry / rw; + final double urx = (rx + wx) / (rw + ww); + final double ury = (ry + wy) / (rw + ww); + final double llx = (rx + hx) / (rw + hw); + final double lly = (ry + hy) / (rw + hw); + final double lrx = (rx + wx + hx) / (rw + ww + hw); + final double lry = (ry + wy + hy) / (rw + ww + hw); + + return Rect.fromLTRB( + _min4(ulx, urx, llx, lrx), + _min4(uly, ury, lly, lry), + _max4(ulx, urx, llx, lrx), + _max4(uly, ury, lly, lry), + ); + } +} + +double _min4(double a, double b, double c, double d) { + final double e = (a < b) ? a : b; + final double f = (c < d) ? c : d; + return (e < f) ? e : f; +} + +double _max4(double a, double b, double c, double d) { + final double e = (a > b) ? a : b; + final double f = (c > d) ? c : d; + return (e > f) ? e : f; +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/path.dart b/packages/vector_graphics_compiler/lib/src/geometry/path.dart new file mode 100644 index 00000000000..4f5a5e2151f --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/path.dart @@ -0,0 +1,782 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:meta/meta.dart'; +import 'package:path_parsing/path_parsing.dart'; + +import '../util.dart'; +import 'basic_types.dart'; +import 'matrix.dart'; + +// This is a magic number used by impeller for radius approximation: +// https://github.com/flutter/impeller/blob/a2478aa4939a9a08c6c3810f72e0db42e7383a07/geometry/path_builder.cc#L9 +// See https://spencermortensen.com/articles/bezier-circle/ for more information. +const double _kArcApproximationMagic = 0.551915024494; + +/// Specifies the winding rule that decies how the interior of a [Path] is +/// calculated. +/// +/// This enum is used by the [Path.fillType] property. +/// +/// It is compatible with the same enum in `dart:ui`. +enum PathFillType { + /// The interior is defined by a non-zero sum of signed edge crossings. + /// + /// For a given point, the point is considered to be on the inside of the path + /// if a line drawn from the point to infinity crosses lines going clockwise + /// around the point a different number of times than it crosses lines going + /// counter-clockwise around that point. + nonZero, + + /// The interior is defined by an odd number of edge crossings. + /// + /// For a given point, the point is considered to be on the inside of the path + /// if a line drawn from the point to infinity crosses an odd number of lines. + evenOdd, +} + +/// The available types of path verbs. +/// +/// Used by [PathCommand.type]. +enum PathCommandType { + /// A path verb that picks up the pen to move it to another coordinate, + /// starting a new contour. + move, + + /// A path verb that draws a line from the current point to a specified + /// coordinate. + line, + + /// A path verb that draws a Bezier curve from the current point to a + /// specified point using two control points. + cubic, + + /// A path verb that draws a line from the current point to the starting + /// point of the current contour. + close, +} + +/// An abstract, immutable representation of a path verb and its associated +/// points. +/// +/// [Path] objects are collections of [PathCommand]s. To create a path object, +/// use a [PathBuilder]. To create a path object from an SVG path definition, +/// use [parseSvgPathData]. +@immutable +abstract class PathCommand { + const PathCommand._(this.type); + + /// The type of this path command. + final PathCommandType type; + + /// Returns a new path command transformed by `matrix`. + PathCommand transformed(AffineMatrix matrix); + + /// A representation of this path command for dart:ui. + String toFlutterString(); +} + +/// A straight line from the current point to x,y. +class LineToCommand extends PathCommand { + /// Creates a straight line command from the current point to x,y. + const LineToCommand(this.x, this.y) : super._(PathCommandType.line); + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y; + + @override + LineToCommand transformed(AffineMatrix matrix) { + final Point xy = matrix.transformPoint(Point(x, y)); + return LineToCommand(xy.x, xy.y); + } + + @override + int get hashCode => Object.hash(type, x, y); + + @override + bool operator ==(Object other) { + return other is LineToCommand && other.x == x && other.y == y; + } + + @override + String toFlutterString() => '..lineTo($x, $y)'; + + @override + String toString() => 'LineToCommand($x, $y)'; +} + +/// Moves the current point to x,y as if picking up the pen. +class MoveToCommand extends PathCommand { + /// Creates a new command that moves the current point to x,y without drawing. + const MoveToCommand(this.x, this.y) : super._(PathCommandType.move); + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y; + + @override + MoveToCommand transformed(AffineMatrix matrix) { + final Point xy = matrix.transformPoint(Point(x, y)); + return MoveToCommand(xy.x, xy.y); + } + + @override + int get hashCode => Object.hash(type, x, y); + + @override + bool operator ==(Object other) { + return other is MoveToCommand && other.x == x && other.y == y; + } + + @override + String toFlutterString() => '..moveTo($x, $y)'; + + @override + String toString() => 'MoveToCommand($x, $y)'; +} + +/// A command describing a cubic Bezier command from the current point to +/// x3,y3 using control points x1,y1 and x2,y2. +class CubicToCommand extends PathCommand { + /// Creates a new cubic Bezier command from the current point to x3,y3 using + /// control points x1,y1 and x2,y2. + const CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3) + : super._(PathCommandType.cubic); + + /// Creates a cubic command from the current point to [end] using [control1] + /// and [control2] as control points. + CubicToCommand.fromPoints(Point control1, Point control2, Point end) + : this(control1.x, control1.y, control2.x, control2.y, end.x, end.y); + + factory CubicToCommand._fromIterablePoints(Iterable points) { + final List list = points.toList(); + assert(list.length == 3); + return CubicToCommand.fromPoints(list[0], list[1], list[2]); + } + + /// The absolute offset of the first control point for this path from the x + /// axis. + final double x1; + + /// The absolute offset of the first control point for this path from the y + /// axis. + final double y1; + + /// A [Point] representation of [x1],[y1], the first control point. + Point get controlPoint1 => Point(x1, y1); + + /// The absolute offset of the second control point for this path from the x + /// axis. + final double x2; + + /// The absolute offset of the second control point for this path from the x + /// axis. + final double y2; + + /// A [Point] representation of [x2],[y2], the second control point. + Point get controlPoint2 => Point(x2, y2); + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x3; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y3; + + /// A [Point] representation of [x3],[y3], the end point of the curve. + Point get endPoint => Point(x3, y3); + + /// Subdivides the cubic curve described by [start], [control1], [control2], + /// [end]. + /// + /// The returned list describes two cubics, where elements `0, 1, 2, 3` are + /// the start, cp1, cp2, and end points of the first cubic and `3, 4, 5, 6` + /// are the start, cp1, cp2, and end points of the second cubic. + static List subdivide( + Point start, + Point control1, + Point control2, + Point end, + double t, + ) { + final Point ab = Point.lerp(start, control1, t); + final Point bc = Point.lerp(control1, control2, t); + final Point cd = Point.lerp(control2, end, t); + final Point abc = Point.lerp(ab, bc, t); + final Point bcd = Point.lerp(bc, cd, t); + final Point abcd = Point.lerp(abc, bcd, t); + return [ + start, + ab, + abc, + abcd, + bcd, + cd, + end, + ]; + } + + /// Computes an approximation of the arc length of this cubic starting + /// from [start]. + double computeLength(Point start) { + // Mike Reed just made this up! The nerve of him. + // One difference from Skia is just setting a default tolerance of 3. This + // is good enough for a particular test SVG that has this curve: + // M65 33c0 17.673-14.326 32-32 32S1 50.673 1 33C1 15.327 15.326 1 33 1s32 14.327 32 32z + // Lower values end up getting the end points wrong when dashing a path. + const double tolerance = 1 / 2 * 3; + + double compute( + Point p1, + Point cp1, + Point cp2, + Point p2, + double distance, + ) { + // If it's "too curvy," cut it in half + if (Point.distance(cp1, Point.lerp(p1, p2, 1 / 3)) > tolerance || + Point.distance(cp2, Point.lerp(p1, p2, 2 / 3)) > tolerance) { + final List points = subdivide(p1, cp1, cp2, p2, .5); + distance = compute( + points[0], + points[1], + points[2], + points[3], + distance, + ); + distance = compute( + points[3], + points[4], + points[5], + points[6], + distance, + ); + } else { + // It's collinear enough to just treat as a line. + distance += Point.distance(p1, p2); + } + return distance; + } + + return compute(start, Point(x1, y1), Point(x2, y2), Point(x3, y3), 0); + } + + @override + CubicToCommand transformed(AffineMatrix matrix) { + final Point xy1 = matrix.transformPoint(Point(x1, y1)); + final Point xy2 = matrix.transformPoint(Point(x2, y2)); + final Point xy3 = matrix.transformPoint(Point(x3, y3)); + return CubicToCommand(xy1.x, xy1.y, xy2.x, xy2.y, xy3.x, xy3.y); + } + + @override + int get hashCode => Object.hash(type, x1, y1, x2, y2, x3, y3); + + @override + bool operator ==(Object other) { + return other is CubicToCommand && + other.x1 == x1 && + other.y1 == y1 && + other.x2 == x2 && + other.y2 == y2 && + other.x3 == x3 && + other.y3 == y3; + } + + @override + String toFlutterString() => '..cubicTo($x1, $y1, $x2, $y2, $x3, $y3)'; + + @override + String toString() => 'CubicToCommand($x1, $y1, $x2, $y2, $x3, $y3)'; +} + +/// A straight line from the current point to the current contour start point. +class CloseCommand extends PathCommand { + /// Creates a new straight line from the current point to the current contour + /// start point. + const CloseCommand() : super._(PathCommandType.close); + + @override + CloseCommand transformed(AffineMatrix matrix) { + return this; + } + + @override + int get hashCode => type.hashCode; + + @override + bool operator ==(Object other) { + return other is CloseCommand; + } + + @override + String toFlutterString() => '..close()'; + @override + String toString() => 'CloseCommand()'; +} + +/// Creates a new builder of [Path] objects. +class PathBuilder implements PathProxy { + /// Creates a new path builder for paths of the specified fill type. + /// + /// By default, will create non-zero filled paths. + PathBuilder([PathFillType? fillType]) + : fillType = fillType ?? PathFillType.nonZero; + + /// Creates a new mutable path builder object from an existing [Path]. + PathBuilder.fromPath(Path path) { + addPath(path); + fillType = path.fillType; + } + + final List _commands = []; + + @override + PathBuilder close() { + _commands.add(const CloseCommand()); + return this; + } + + @override + PathBuilder cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + _commands.add(CubicToCommand(x1, y1, x2, y2, x3, y3)); + return this; + } + + @override + PathBuilder lineTo(double x, double y) { + _commands.add(LineToCommand(x, y)); + return this; + } + + @override + PathBuilder moveTo(double x, double y) { + _commands.add(MoveToCommand(x, y)); + return this; + } + + /// Adds the commands of an existing path to the new path being created. + PathBuilder addPath(Path other) { + _commands.addAll(other._commands); + return this; + } + + /// Adds an oval command to new path. + PathBuilder addOval(Rect oval) { + final Point r = Point(oval.width * 0.5, oval.height * 0.5); + final Point c = Point( + oval.left + (oval.width * 0.5), + oval.top + (oval.height * 0.5), + ); + final Point m = Point( + _kArcApproximationMagic * r.x, + _kArcApproximationMagic * r.y, + ); + + moveTo(c.x, c.y - r.y); + + // Top right arc. + cubicTo(c.x + m.x, c.y - r.y, c.x + r.x, c.y - m.y, c.x + r.x, c.y); + + // Bottom right arc. + cubicTo(c.x + r.x, c.y + m.y, c.x + m.x, c.y + r.y, c.x, c.y + r.y); + + // Bottom left arc. + cubicTo(c.x - m.x, c.y + r.y, c.x - r.x, c.y + m.y, c.x - r.x, c.y); + + // Top left arc. + cubicTo(c.x - r.x, c.y - m.y, c.x - m.x, c.y - r.y, c.x, c.y - r.y); + + close(); + return this; + } + + /// Adds a rectangle to the new path. + PathBuilder addRect(Rect rect) { + moveTo(rect.left, rect.top); + lineTo(rect.right, rect.top); + lineTo(rect.right, rect.bottom); + lineTo(rect.left, rect.bottom); + close(); + return this; + } + + /// Adds a rounded rectangle to the new path. + PathBuilder addRRect(Rect rect, double rx, double ry) { + if (rx == 0 && ry == 0) { + return addRect(rect); + } + + final Point magicRadius = Point(rx, ry) * _kArcApproximationMagic; + + moveTo(rect.left + rx, rect.top); + + // Top line. + lineTo(rect.left + rect.width - rx, rect.top); + + // Top right arc. + // + cubicTo( + rect.left + rect.width - rx + magicRadius.x, + rect.top, + rect.left + rect.width, + rect.top + ry - magicRadius.y, + rect.left + rect.width, + rect.top + ry, + ); + + // Right line. + lineTo(rect.left + rect.width, rect.top + rect.height - ry); + + // Bottom right arc. + cubicTo( + rect.left + rect.width, + rect.top + rect.height - ry + magicRadius.y, + rect.left + rect.width - rx + magicRadius.x, + rect.top + rect.height, + rect.left + rect.width - rx, + rect.top + rect.height, + ); + + // Bottom line. + lineTo(rect.left + rx, rect.top + rect.height); + + // Bottom left arc. + cubicTo( + rect.left + rx - magicRadius.x, + rect.top + rect.height, + rect.left, + rect.top + rect.height - ry + magicRadius.y, + rect.left, + rect.top + rect.height - ry); + + // Left line. + lineTo(rect.left, rect.top + ry); + + // Top left arc. + cubicTo( + rect.left, + rect.top + ry - magicRadius.y, + rect.left + rx - magicRadius.x, + rect.top, + rect.left + rx, + rect.top, + ); + + close(); + return this; + } + + /// The fill type to use for the new path. + late PathFillType fillType; + + /// Creates a new [Path] object from the commands in this path. + /// + /// If `reset` is set to false, this builder can be used to create multiple + /// path objects with the same commands. By default, the builder will reset + /// to an initial state. + Path toPath({bool reset = true}) { + final Path path = Path( + commands: _commands, + fillType: fillType, + ); + + if (reset) { + _commands.clear(); + } + return path; + } +} + +/// An immutable collection of [PathCommand]s. +@immutable +class Path { + /// Creates a new immutable collection of [PathCommand]s. + Path({ + List commands = const [], + this.fillType = PathFillType.nonZero, + }) { + _commands.addAll(commands); + } + + /// Creates a copy of this path, replacing the current [fillType] with [type]. + Path withFillType(PathFillType type) { + if (type == fillType) { + return this; + } + return Path(fillType: type, commands: _commands); + } + + /// Whether this path has any commands. + bool get isEmpty => _commands.isEmpty; + + /// The commands this path contains. + Iterable get commands => _commands; + + final List _commands = []; + + /// The fill type of this path, defaulting to [PathFillType.nonZero]. + final PathFillType fillType; + + /// Creates a new path whose commands and points are transformed by `matrix`. + Path transformed(AffineMatrix matrix) { + final List commands = []; + for (final PathCommand command in _commands) { + commands.add(command.transformed(matrix)); + } + return Path( + commands: commands, + fillType: fillType, + ); + } + + @override + int get hashCode => Object.hash(Object.hashAll(_commands), fillType); + + @override + bool operator ==(Object other) { + return other is Path && + listEquals(_commands, other._commands) && + other.fillType == fillType; + } + + /// Creates a dashed version of this path. + /// + /// The interval list is read in a circular fashion, such that the first + /// interval is used to dash and the second to move. If the list is an odd + /// number of elements, it is effectively the same as if it were repeated + /// twice. + /// + /// Callers are responsible for not passing interval lists consisting entirely + /// of `0`. + Path dashed(List intervals) { + if (intervals.isEmpty) { + return this; + } + final _PathDasher dasher = _PathDasher(intervals); + return dasher.dash(this); + } + + /// Compute the bounding box for the given path segment. + Rect bounds() { + if (_commands.isEmpty) { + return Rect.zero; + } + double smallestX = double.maxFinite; + double smallestY = double.maxFinite; + double largestX = -double.maxFinite; + double largestY = -double.maxFinite; + for (final PathCommand command in _commands) { + switch (command.type) { + case PathCommandType.move: + final MoveToCommand move = command as MoveToCommand; + smallestX = math.min(move.x, smallestX); + smallestY = math.min(move.y, smallestY); + largestX = math.max(move.x, largestX); + largestY = math.max(move.y, largestY); + case PathCommandType.line: + final LineToCommand move = command as LineToCommand; + smallestX = math.min(move.x, smallestX); + smallestY = math.min(move.y, smallestY); + largestX = math.max(move.x, largestX); + largestY = math.max(move.y, largestY); + case PathCommandType.cubic: + final CubicToCommand cubic = command as CubicToCommand; + for (final List pair in >[ + [cubic.x1, cubic.y1], + [cubic.x2, cubic.y2], + [cubic.x3, cubic.y3], + ]) { + smallestX = math.min(pair[0], smallestX); + smallestY = math.min(pair[1], smallestY); + largestX = math.max(pair[0], largestX); + largestY = math.max(pair[1], largestY); + } + case PathCommandType.close: + break; + } + } + return Rect.fromLTRB(smallestX, smallestY, largestX, largestY); + } + + /// Returns a string that prints the dart:ui code to create this path. + String toFlutterString() { + final StringBuffer buffer = StringBuffer('Path()'); + if (fillType != PathFillType.nonZero) { + buffer.write('\n ..fillType = $fillType'); + } + for (final PathCommand command in commands) { + buffer.write('\n ${command.toFlutterString()}'); + } + buffer.write(';'); + return buffer.toString(); + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('Path('); + if (commands.isNotEmpty) { + buffer.write('\n commands: $commands,'); + } + if (fillType != PathFillType.nonZero) { + buffer.write('\n fillType: $fillType,'); + } + buffer.write('\n)'); + return buffer.toString(); + } +} + +/// Creates a new [Path] object from an SVG path data string. +Path parseSvgPathData(String svg, [PathFillType? type]) { + if (svg == '') { + return Path(fillType: type ?? PathFillType.nonZero); + } + + final SvgPathStringSource parser = SvgPathStringSource(svg); + final PathBuilder pathBuilder = PathBuilder(type); + final SvgPathNormalizer normalizer = SvgPathNormalizer(); + for (final PathSegmentData seg in parser.parseSegments()) { + normalizer.emitSegment(seg, pathBuilder); + } + return pathBuilder.toPath(); +} + +class _CircularIntervalList { + _CircularIntervalList(this._vals) + : assert(_vals.isNotEmpty), + assert(!_vals.every((double val) => val == 0)); + + final List _vals; + int _idx = 0; + + double get next { + if (_idx >= _vals.length) { + _idx = 0; + } + return _vals[_idx++]; + } +} + +class _PathDasher { + _PathDasher(List intervals) + : assert(!intervals.every((double interval) => interval == 0)), + _intervals = _CircularIntervalList(intervals); + + final _CircularIntervalList _intervals; + + late double length; + Point currentPoint = Point.zero; + Point currentSubpathPoint = Point.zero; + late bool draw; + + final List _dashedCommands = []; + + void _dashLineTo(Point target) { + double distance = Point.distance(currentPoint, target); + + if (distance <= 0 || length <= 0) { + return; + } + + while (distance >= length) { + final double t = length / distance; + currentPoint = Point.lerp(currentPoint, target, t); + length = _intervals.next; + + if (draw) { + _dashedCommands.add(LineToCommand(currentPoint.x, currentPoint.y)); + } else { + _dashedCommands.add(MoveToCommand(currentPoint.x, currentPoint.y)); + } + + distance = Point.distance(currentPoint, target); + draw = !draw; + } + if (distance > 0) { + length -= distance; + if (draw) { + _dashedCommands.add(LineToCommand(target.x, target.y)); + } + } + currentPoint = target; + } + + void _dashCubicTo(CubicToCommand cubic) { + double distance = cubic.computeLength(currentPoint); + while (distance >= length) { + final double t = length / distance; + final List dividedPoints = CubicToCommand.subdivide( + currentPoint, + cubic.controlPoint1, + cubic.controlPoint2, + cubic.endPoint, + t, + ); + currentPoint = dividedPoints[3]; + if (draw) { + _dashedCommands.add(CubicToCommand._fromIterablePoints( + dividedPoints.skip(1).take(3), + )); + } else { + _dashedCommands.add(MoveToCommand( + currentPoint.x, + currentPoint.y, + )); + } + cubic = CubicToCommand._fromIterablePoints( + dividedPoints.skip(4).take(3), + ); + length = _intervals.next; + distance = cubic.computeLength(currentPoint); + draw = !draw; + } + length -= distance; + currentPoint = cubic.endPoint; + if (draw) { + _dashedCommands.add(cubic); + } + } + + Path dash(Path path) { + length = _intervals.next; + draw = true; + for (final PathCommand command in path._commands) { + switch (command.type) { + case PathCommandType.move: + final MoveToCommand move = command as MoveToCommand; + currentPoint = Point(move.x, move.y); + currentSubpathPoint = currentPoint; + _dashedCommands.add(command); + case PathCommandType.line: + final LineToCommand line = command as LineToCommand; + _dashLineTo(Point(line.x, line.y)); + case PathCommandType.cubic: + _dashCubicTo(command as CubicToCommand); + case PathCommandType.close: + _dashLineTo(currentSubpathPoint); + currentPoint = currentSubpathPoint; + } + } + return Path(commands: _dashedCommands, fillType: path.fillType); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/pattern.dart b/packages/vector_graphics_compiler/lib/src/geometry/pattern.dart new file mode 100644 index 00000000000..5aacb80b52e --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/pattern.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'matrix.dart'; + +/// Pattern positioning and size information. +@immutable +class PatternData { + /// Constructs new [PatternData]. + const PatternData(this.x, this.y, this.width, this.height, this.transform); + + /// The x coordinate shift of the pattern tile in px. + final double x; + + /// The y coordinate shift of the pattern tile in px. + final double y; + + /// The width of the pattern's viewbox in px. + /// Values must be > = 1. + final double width; + + /// The height of the pattern's viewbox in px. + /// Values must be > = 1. + final double height; + + /// The transform of the pattern generated from its children. + final AffineMatrix transform; + + @override + int get hashCode => Object.hash(x, y, width, height, transform); + + @override + bool operator ==(Object other) { + return other is PatternData && + other.x == x && + other.y == y && + other.width == width && + other.height == height && + other.transform == transform; + } +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/vertices.dart b/packages/vector_graphics_compiler/lib/src/geometry/vertices.dart new file mode 100644 index 00000000000..e9c972c7279 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/vertices.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'basic_types.dart'; + +/// A description of vertex points for drawing triangles. +class Vertices { + /// Creates a new collection of triangle vertices at the specified points. + const Vertices(this.vertexPoints); + + /// Creates a new collection of triangle vertices from the specified + /// [Float32List], interpreted as x,y pairs. + factory Vertices.fromFloat32List(Float32List vertices) { + if (vertices.length.isOdd) { + throw ArgumentError( + 'must be an even number of vertex points', + 'vertices', + ); + } + final List vertexPoints = []; + for (int index = 0; index < vertices.length; index += 2) { + vertexPoints.add(Point(vertices[index], vertices[index + 1])); + } + return Vertices(vertexPoints); + } + + /// A list of vertex points descibing this triangular mesh. + /// + /// The vertex points are assumed to be in VertexMode.triangle. + final List vertexPoints; + + /// Creates an optimized version of [vertexPoints] where the points are + /// deduplicated via an index buffer. + IndexedVertices createIndex() { + final Map pointMap = {}; + int index = 0; + final List indices = []; + for (final Point point in vertexPoints) { + indices.add(pointMap.putIfAbsent(point, () => index++)); + } + + Float32List pointsToFloat32List(List points) { + final Float32List vertices = Float32List(points.length * 2); + int vertexIndex = 0; + for (final Point point in points) { + vertices[vertexIndex++] = point.x; + vertices[vertexIndex++] = point.y; + } + return vertices; + } + + final List compressedPoints = pointMap.keys.toList(); + if (compressedPoints.length * 2 + indices.length > + vertexPoints.length * 2) { + return IndexedVertices(pointsToFloat32List(vertexPoints), null); + } + + return IndexedVertices( + pointsToFloat32List(compressedPoints), + Uint16List.fromList(indices), + ); + } +} + +/// An optimized version of [Vertices] that uses an index buffer to specify +/// reused vertex points. +class IndexedVertices { + /// Creates a indexed set of vertices. + /// + /// Consider using [Vertices.createIndex]. + const IndexedVertices(this.vertices, this.indices); + + /// The raw vertex points. + final Float32List vertices; + + /// The order to use vertices from [vertices]. + /// + /// May be null if [vertices] was not compressable. + final Uint16List? indices; +} diff --git a/packages/vector_graphics_compiler/lib/src/image/image_info.dart b/packages/vector_graphics_compiler/lib/src/image/image_info.dart new file mode 100644 index 00000000000..c4e422b8aa6 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/image/image_info.dart @@ -0,0 +1,215 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +/// Image formats supported by Flutter. +enum ImageFormat { + /// A Portable Network Graphics format image. + png, + + /// A JPEG format image. + /// + /// This library does not support JPEG 2000. + jpeg, + + /// A WebP format image. + webp, + + /// A Graphics Interchange Format image. + gif, + + /// A Windows Bitmap format image. + bmp, +} + +/// Provides details about image format information for raw compressed bytes +/// of an image. +abstract class ImageSizeData { + /// Allows subclasses to be const. + const ImageSizeData({ + required this.format, + required this.width, + required this.height, + }) : assert(width >= 0), + assert(height >= 0); + + /// Creates an appropriate [ImageSizeData] for the source `bytes`, if possible. + /// + /// Only supports image formats supported by Flutter. + factory ImageSizeData.fromBytes(Uint8List bytes) { + if (bytes.isEmpty) { + throw ArgumentError('bytes was empty'); + } + if (PngImageSizeData.matches(bytes)) { + return PngImageSizeData._(bytes.buffer.asByteData()); + } + if (GifImageSizeData.matches(bytes)) { + return GifImageSizeData._(bytes.buffer.asByteData()); + } + if (JpegImageSizeData.matches(bytes)) { + return JpegImageSizeData._fromBytes(bytes.buffer.asByteData()); + } + if (WebPImageSizeData.matches(bytes)) { + return WebPImageSizeData._(bytes.buffer.asByteData()); + } + if (BmpImageSizeData.matches(bytes)) { + return BmpImageSizeData._(bytes.buffer.asByteData()); + } + throw ArgumentError('unknown image type'); + } + + /// The [ImageFormat] this instance represents. + final ImageFormat format; + + /// The width, in pixels, of the image. + /// + /// If the image is multi-frame, this is the width of the first frame. + final int width; + + /// The height, in pixels, of the image. + /// + /// If the image is multi-frame, this is the height of the first frame. + final int height; + + /// The esimated size of the image in bytes. + /// + /// The `withMipmapping` parameter controls whether to account for mipmapping + /// when decompressing the image. Flutter will use this when possible, at the + /// cost of slightly more memory usage. + int decodedSizeInBytes({bool withMipmapping = true}) { + if (withMipmapping) { + return (width * height * 4.3).ceil(); + } + return width * height * 4; + } +} + +/// The [ImageSizeData] for a PNG image. +class PngImageSizeData extends ImageSizeData { + PngImageSizeData._(ByteData data) + : super( + format: ImageFormat.png, + width: data.getUint32(16), + height: data.getUint32(20), + ); + + /// Returns true if `bytes` starts with the expected header for a PNG image. + static bool matches(Uint8List bytes) { + return bytes.lengthInBytes > 20 && + bytes[0] == 0x89 && + bytes[1] == 0x50 && + bytes[2] == 0x4E && + bytes[3] == 0x47 && + bytes[4] == 0x0D && + bytes[5] == 0x0A && + bytes[6] == 0x1A && + bytes[7] == 0x0A; + } +} + +/// The [ImageSizeData] for a GIF image. +class GifImageSizeData extends ImageSizeData { + GifImageSizeData._(ByteData data) + : super( + format: ImageFormat.gif, + width: data.getUint16(6, Endian.little), + height: data.getUint16(8, Endian.little), + ); + + /// Returns true if `bytes` starts with the expected header for a GIF image. + static bool matches(Uint8List bytes) { + return bytes.lengthInBytes > 8 && + bytes[0] == 0x47 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x38 && + (bytes[4] == 0x37 || bytes[4] == 0x39) // 7 or 9 + && + bytes[5] == 0x61; + } +} + +/// The [ImageSizeData] for a JPEG image. +/// +/// This library does not support JPEG2000 images. +class JpegImageSizeData extends ImageSizeData { + JpegImageSizeData._({required super.width, required super.height}) + : super( + format: ImageFormat.jpeg, + ); + + factory JpegImageSizeData._fromBytes(ByteData data) { + int index = 4; // Skip the first header bytes (already validated). + index += data.getUint16(index); + while (index < data.lengthInBytes) { + if (data.getUint8(index) != 0xFF) { + // Start of block + throw StateError('Invalid JPEG file'); + } + if (const [0xC0, 0xC1, 0xC2].contains(data.getUint8(index + 1))) { + // Start of frame 0 + return JpegImageSizeData._( + height: data.getUint16(index + 5), + width: data.getUint16(index + 7), + ); + } + index += 2; + index += data.getUint16(index); + } + throw StateError('Invalid JPEG'); + } + + /// Returns true if `bytes` starts with the expected header for a JPEG image. + static bool matches(Uint8List bytes) { + return bytes.lengthInBytes > 12 && + bytes[0] == 0xFF && + bytes[1] == 0xD8 && + bytes[2] == 0xFF; + } +} + +/// The [ImageSizeData] for a WebP image. +class WebPImageSizeData extends ImageSizeData { + WebPImageSizeData._(ByteData data) + : super( + format: ImageFormat.webp, + width: data.getUint16(26, Endian.little), + height: data.getUint16(28, Endian.little), + ); + + /// Returns true if `bytes` starts with the expected header for a WebP image. + static bool matches(Uint8List bytes) { + return bytes.lengthInBytes > 28 && + bytes[0] == 0x52 // R + && + bytes[1] == 0x49 // I + && + bytes[2] == 0x46 // F + && + bytes[3] == 0x46 // F + && + bytes[8] == 0x57 // W + && + bytes[9] == 0x45 // E + && + bytes[10] == 0x42 // B + && + bytes[11] == 0x50; // P + } +} + +/// The [ImageSizeData] for a BMP image. +class BmpImageSizeData extends ImageSizeData { + BmpImageSizeData._(ByteData data) + : super( + format: ImageFormat.bmp, + width: data.getInt32(18, Endian.little), + height: data.getInt32(22, Endian.little)); + + /// Returns true if `bytes` starts with the expected header for a WebP image. + static bool matches(Uint8List bytes) { + return bytes.lengthInBytes > 22 && bytes[0] == 0x42 && bytes[1] == 0x4D; + } +} diff --git a/packages/vector_graphics_compiler/lib/src/paint.dart b/packages/vector_graphics_compiler/lib/src/paint.dart new file mode 100644 index 00000000000..5085f47526a --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/paint.dart @@ -0,0 +1,1518 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'geometry/basic_types.dart'; +import 'geometry/matrix.dart'; +import 'util.dart'; + +// The enumerations in this file must match the ordering and index valuing of +// the similarly named enumerations in dart:ui. + +/// An immutable representation of a 32 bit color. +@immutable +class Color { + /// Creates an immutable representation of a 32 bit color. + /// + /// The first 8 bits are the alpha value, the next 8 red, the next 8 green, + /// and the last 8 blue. + const Color(this.value); + + /// Creates an immutable representation of color from its red, green, blue, + /// and 0..1 opacity parts. + const Color.fromRGBO(int r, int g, int b, double opacity) + : value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) | + ((r & 0xff) << 16) | + ((g & 0xff) << 8) | + ((b & 0xff) << 0)) & + 0xFFFFFFFF; + + /// Creates an immutable representation of color from its alpha, red, green, + /// and blue parts. + /// + /// Each part is represented by an integer from 0..255. + const Color.fromARGB(int a, int r, int g, int b) + : value = (((a & 0xff) << 24) | + ((r & 0xff) << 16) | + ((g & 0xff) << 8) | + ((b & 0xff) << 0)) & + 0xFFFFFFFF; + + /// Fully opaque black. + static const Color opaqueBlack = Color(0xFF000000); + + /// Creates a new color based on this color with the specified opacity, + /// unpremultiplied. + Color withOpacity(double opacity) { + return Color.fromRGBO(r, g, b, opacity); + } + + /// The raw 32 bit color value. + /// + /// The first 8 bits are the alpha value, the next 8 red, the next 8 green, + /// and the last 8 blue. + final int value; + + /// The red channel value from 0..255. + int get r => (0x00ff0000 & value) >> 16; + + /// The green channel value from 0..255. + int get g => (0x0000ff00 & value) >> 8; + + /// The blue channel value from 0..255. + int get b => (0x000000ff & value) >> 0; + + /// The opacity channel value from 0..255. + int get a => value >> 24; + + @override + String toString() => 'Color(0x${value.toRadixString(16).padLeft(8, '0')})'; + + @override + int get hashCode => value; + + @override + bool operator ==(Object other) { + return other is Color && other.value == value; + } +} + +/// A shading program to apply to a [Paint]. Implemented in [LinearGradient] and +/// [RadialGradient]. +@immutable +abstract class Gradient { + /// Allows subclasses to be const. + const Gradient._( + this.id, + this.colors, + this.offsets, + this.tileMode, + this.unitMode, + this.transform, + ); + + /// The reference identifier for this gradient. + final String id; + + /// The colors to blend from the start to end points. + final List? colors; + + /// The positions to apply [colors] to. Must be the same length as [colors]. + final List? offsets; + + /// Specifies the meaning of [from] and [to]. + final TileMode? tileMode; + + /// Whether the coordinates in this gradient should be transformed by the + /// space this object occupies or by the root bounds. + final GradientUnitMode? unitMode; + + /// The transform, if any, to apply to the gradient. + final AffineMatrix? transform; + + /// Apply the bounds and transform the shader. + Gradient applyBounds(Rect bounds, AffineMatrix transform); + + /// Creates a new gradient + Gradient applyProperties(Gradient ref); +} + +/// A [Gradient] that describes a linear gradient from [from] to [to]. +/// +/// If [offsets] is provided, `offsets[i]` is a number from 0.0 to 1.0 +/// that specifies where `offsets[i]` begins in the gradient. If [offsets] is +/// not provided, then only two stops, at 0.0 and 1.0, are implied (and +/// [colors] must therefore only have two entries). +/// +/// The behavior before [from] and after [to] is described by the [tileMode] +/// argument. For details, see the [TileMode] enum. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_linear.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) +/// +/// If [transform] is provided, the gradient fill will be transformed by the +/// specified affine matrix relative to the local coordinate system. +class LinearGradient extends Gradient { + /// Creates a new linear gradient shader. + const LinearGradient({ + required String id, + required this.from, + required this.to, + List? colors, + List? offsets, + TileMode? tileMode, + GradientUnitMode? unitMode, + AffineMatrix? transform, + }) : super._(id, colors, offsets, tileMode, unitMode, transform); + + /// The start point of the gradient, as specified by [tileMode]. + final Point from; + + /// The end point of the gradient, as specified by [tileMode]. + final Point to; + + @override + LinearGradient applyBounds(Rect bounds, AffineMatrix transform) { + assert(offsets != null); + assert(colors != null); + AffineMatrix accumulatedTransform = this.transform ?? AffineMatrix.identity; + switch (unitMode ?? GradientUnitMode.objectBoundingBox) { + case GradientUnitMode.objectBoundingBox: + accumulatedTransform = transform + .translated(bounds.left, bounds.top) + .scaled(bounds.width, bounds.height) + .multiplied(accumulatedTransform); + case GradientUnitMode.userSpaceOnUse: + accumulatedTransform = transform.multiplied(accumulatedTransform); + case GradientUnitMode.transformed: + break; + } + + return LinearGradient( + id: id, + from: accumulatedTransform.transformPoint(from), + to: accumulatedTransform.transformPoint(to), + colors: colors, + offsets: offsets, + tileMode: tileMode ?? TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ); + } + + @override + LinearGradient applyProperties(Gradient ref) { + return LinearGradient( + id: id, + from: from, + to: to, + colors: colors ?? ref.colors, + offsets: offsets ?? ref.offsets, + tileMode: tileMode ?? ref.tileMode, + unitMode: unitMode ?? ref.unitMode, + transform: transform ?? ref.transform, + ); + } + + @override + int get hashCode => Object.hash( + id, + from, + to, + Object.hashAll(colors ?? []), + Object.hashAll(offsets ?? []), + tileMode, + unitMode); + + @override + bool operator ==(Object other) { + return other is LinearGradient && + other.id == id && + other.from == from && + other.to == to && + listEquals(other.colors, colors) && + listEquals(other.offsets, offsets) && + other.tileMode == tileMode && + other.unitMode == unitMode; + } + + @override + String toString() { + return 'LinearGradient(' + "id: '$id', " + 'from: $from, ' + 'to: $to, ' + 'colors: $colors, ' + 'offsets: $offsets, ' + 'tileMode: $tileMode, ' + '${transform == null ? '' : 'Float64List.fromList(${transform!.toMatrix4()}), '}' + 'unitMode: $unitMode)'; + } +} + +/// Determines how to transform the points given for a gradient. +enum GradientUnitMode { + /// The gradient vector(s) are transformed by the space in the object + /// containing the gradient. + objectBoundingBox, + + /// The gradient vector(s) are transformed by the root bounds of the drawing. + userSpaceOnUse, + + /// The gradient vectors are already transformed. + transformed, +} + +/// Creates a radial gradient centered at [center] that ends at [radius] +/// distance from the center. +/// +/// If [offsets] is provided, `offsets[i]` is a number from 0.0 to 1.0 +/// that specifies where `colors[i]` begins in the gradient. If [offsets] is +/// not provided, then only two stops, at 0.0 and 1.0, are implied (and +/// [colors] must therefore only have two entries). +/// +/// The behavior before and after the radius is described by the [tileMode] +/// argument. For details, see the [TileMode] enum. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radial.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) +/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) +/// +/// If [transform] is provided, the gradient fill will be transformed by the +/// specified affine matrix relative to the local coordinate system. +/// +/// If [focalPoint] is provided and not equal to [center] and [focalRadius] +/// is provided and not equal to 0.0, the generated shader will be a two point +/// conical radial gradient, with [focalPoint] being the center of the focal +/// circle. If [focalPoint] is provided and not equal to [center], at least one +/// of the two offsets must not be equal to [Point.zero]. +class RadialGradient extends Gradient { + /// Creates a new radial gradient object with the specified properties. + /// + /// See [RadialGradient]. + const RadialGradient({ + required String id, + required this.center, + required this.radius, + List? colors, + List? offsets, + TileMode? tileMode, + AffineMatrix? transform, + this.focalPoint, + GradientUnitMode? unitMode, + }) : super._(id, colors, offsets, tileMode, unitMode, transform); + + /// The central point of the gradient. + final Point center; + + /// The colors to blend from the start to end points. + final double radius; + + /// If specified, creates a two-point conical gradient using [center] and the + /// [focalPoint]. + final Point? focalPoint; + + @override + RadialGradient applyBounds(Rect bounds, AffineMatrix transform) { + assert(offsets != null); + assert(colors != null); + AffineMatrix accumulatedTransform = this.transform ?? AffineMatrix.identity; + switch (unitMode ?? GradientUnitMode.objectBoundingBox) { + case GradientUnitMode.objectBoundingBox: + accumulatedTransform = transform + .translated(bounds.left, bounds.top) + .scaled(bounds.width, bounds.height) + .multiplied(accumulatedTransform); + case GradientUnitMode.userSpaceOnUse: + accumulatedTransform = transform.multiplied(accumulatedTransform); + case GradientUnitMode.transformed: + break; + } + + return RadialGradient( + id: id, + center: center, + radius: radius, + colors: colors, + offsets: offsets, + tileMode: tileMode ?? TileMode.clamp, + transform: accumulatedTransform, + focalPoint: focalPoint, + unitMode: GradientUnitMode.transformed, + ); + } + + @override + RadialGradient applyProperties(Gradient ref) { + return RadialGradient( + id: id, + center: center, + radius: radius, + focalPoint: focalPoint, + colors: colors ?? ref.colors, + offsets: offsets ?? ref.offsets, + transform: transform ?? ref.transform, + unitMode: unitMode ?? ref.unitMode, + tileMode: tileMode ?? ref.tileMode, + ); + } + + @override + int get hashCode => Object.hash( + id, + center, + radius, + Object.hashAll(colors ?? []), + Object.hashAll(offsets ?? []), + tileMode, + transform, + focalPoint, + unitMode); + + @override + bool operator ==(Object other) { + return other is RadialGradient && + other.id == id && + other.center == center && + other.radius == radius && + other.focalPoint == focalPoint && + listEquals(other.colors, colors) && + listEquals(other.offsets, offsets) && + other.transform == transform && + other.tileMode == tileMode && + other.unitMode == unitMode; + } + + @override + String toString() { + return 'RadialGradient(' + "id: '$id', " + 'center: $center, ' + 'radius: $radius, ' + 'colors: $colors, ' + 'offsets: $offsets, ' + 'tileMode: $tileMode, ' + '${transform == null ? '' : 'transform: Float64List.fromList(${transform!.toMatrix4()}) ,'}' + 'focalPoint: $focalPoint, ' + 'unitMode: $unitMode)'; + } +} + +/// An immutable collection of painting attributes. +/// +/// Null attribute values indicate that a value is expected to inherit from +/// parent or accept a child's painting value. +/// +/// Leaf nodes in a painting graph must have a non-null [fill] or a non-null +/// [stroke]. If both [stroke] and [fill] are not null, the expected painting +/// order is [fill] followed by [stroke]. +@immutable +class Paint { + /// Creates a new collection of painting attributes. + /// + /// See [Paint]. + const Paint({ + BlendMode? blendMode, + this.stroke, + this.fill, + }) : blendMode = blendMode ?? BlendMode.srcOver; + + /// The Porter-Duff algorithm to use when compositing this painting object + /// with any objects painted under it. + /// + /// Defaults to [BlendMode.srcOver]. + final BlendMode blendMode; + + /// The stroke properties, if any, to apply to shapes drawn with this paint. + /// + /// If both stroke and [fill] are non-null, the fill is painted first, + /// followed by stroke. + final Stroke? stroke; + + /// The fill properties, if any, to apply to shapes drawn with this paint. + /// + /// If both [stroke] and fill are non-null, the fill is painted first, + /// followed by stroke. + final Fill? fill; + + @override + int get hashCode => Object.hash(blendMode, stroke, fill); + + @override + bool operator ==(Object other) { + return other is Paint && + other.blendMode == blendMode && + other.stroke == stroke && + other.fill == fill; + } + + /// Apply the bounds to the given paint. + /// + /// May be a no-op if no properties of the paint are impacted by + /// the bounds. + Paint applyBounds(Rect bounds, AffineMatrix transform) { + final Gradient? shader = fill?.shader; + if (shader == null) { + return this; + } + final Gradient newShader = shader.applyBounds(bounds, transform); + return Paint( + blendMode: blendMode, + stroke: stroke, + fill: Fill( + color: fill!.color, + shader: newShader, + ), + ); + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('Paint(blendMode: $blendMode'); + const String leading = ', '; + if (stroke != null) { + buffer.write('${leading}stroke: $stroke'); + } + if (fill != null) { + buffer.write('${leading}fill: $fill'); + } + buffer.write(')'); + return buffer.toString(); + } +} + +/// An immutable collection of stroking properties for a [Paint]. +/// +/// See also [Paint.stroke]. +@immutable +class Stroke { + /// Creates a new collection of stroking properties. + const Stroke({ + Color? color, + this.shader, + this.cap, + this.join, + this.miterLimit, + this.width, + }) : color = color ?? Color.opaqueBlack; + + /// The color to use for this stroke. + /// + /// Defaults to [Color.opaqueBlack]. + /// + /// If [shader] is not null, only the opacity is used. + final Color color; + + /// The [Gradient] to use when stroking. + final Gradient? shader; + + /// The cap style to use for strokes. + /// + /// Defaults to [StrokeCap.butt]. + final StrokeCap? cap; + + /// The join style to use for strokes. + /// + /// Defaults to [StrokeJoin.miter]. + final StrokeJoin? join; + + /// The limit where stroke joins drawn with [StrokeJoin.miter] switch to being + /// drawn as [StrokeJoin.bevel]. + final double? miterLimit; + + /// The width of the stroke, if [style] is [PaintingStyle.stroke]. + final double? width; + + @override + int get hashCode => Object.hash( + PaintingStyle.stroke, color, shader, cap, join, miterLimit, width); + + @override + bool operator ==(Object other) { + return other is Stroke && + other.color == color && + other.shader == shader && + other.cap == cap && + other.join == join && + other.miterLimit == miterLimit && + other.width == width; + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('Stroke(color: $color'); + const String leading = ', '; + if (shader != null) { + buffer.write('${leading}shader: $shader'); + } + if (cap != null) { + buffer.write('${leading}cap: $cap'); + } + if (join != null) { + buffer.write('${leading}join: $join'); + } + if (miterLimit != null) { + buffer.write('${leading}miterLimit: $miterLimit'); + } + if (width != null) { + buffer.write('${leading}width: $width'); + } + buffer.write(')'); + return buffer.toString(); + } +} + +/// An immutable representation of filling attributes for a [Paint]. +/// +/// See also [Paint.fill]. +@immutable +class Fill { + /// Creates a new immutable set of drawing attributes for a [Paint]. + const Fill({ + Color? color, + this.shader, + }) : color = color ?? Color.opaqueBlack; + + /// The color to use for this stroke. + /// + /// Defaults to [Color.opaqueBlack]. + /// + /// If [shader] is not null, only the opacity is used. + final Color color; + + /// The [Gradient] to use when filling. + final Gradient? shader; + + @override + int get hashCode => Object.hash(PaintingStyle.fill, color, shader); + + @override + bool operator ==(Object other) { + return other is Fill && other.color == color && other.shader == shader; + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('Fill(color: $color'); + const String leading = ', '; + + if (shader != null) { + buffer.write('${leading}shader: $shader'); + } + buffer.write(')'); + return buffer.toString(); + } +} + +/// The Porter-Duff algorithm to use for blending. +/// +/// The values in this enum are expected to match exactly the values of the +/// similarly named enum from dart:ui. They must not be removed even if they +/// are unused. +enum BlendMode { + // This list comes from Skia's SkXfermode.h and the values (order) should be + // kept in sync. + // See: https://skia.org/docs/user/api/skpaint_overview/#SkXfermode + + /// Drop both the source and destination images, leaving nothing. + /// + /// This corresponds to the "clear" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_clear.png) + clear, + + /// Drop the destination image, only paint the source image. + /// + /// Conceptually, the destination is first cleared, then the source image is + /// painted. + /// + /// This corresponds to the "Copy" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_src.png) + src, + + /// Drop the source image, only paint the destination image. + /// + /// Conceptually, the source image is discarded, leaving the destination + /// untouched. + /// + /// This corresponds to the "Destination" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dst.png) + dst, + + /// Composite the source image over the destination image. + /// + /// This is the default value. It represents the most intuitive case, where + /// shapes are painted on top of what is below, with transparent areas showing + /// the destination layer. + /// + /// This corresponds to the "Source over Destination" Porter-Duff operator, + /// also known as the Painter's Algorithm. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOver.png) + srcOver, + + /// Composite the source image under the destination image. + /// + /// This is the opposite of [srcOver]. + /// + /// This corresponds to the "Destination over Source" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOver.png) + /// + /// This is useful when the source image should have been painted before the + /// destination image, but could not be. + dstOver, + + /// Show the source image, but only where the two images overlap. The + /// destination image is not rendered, it is treated merely as a mask. The + /// color channels of the destination are ignored, only the opacity has an + /// effect. + /// + /// To show the destination image instead, consider [dstIn]. + /// + /// To reverse the semantic of the mask (only showing the source where the + /// destination is absent, rather than where it is present), consider + /// [srcOut]. + /// + /// This corresponds to the "Source in Destination" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcIn.png) + srcIn, + + /// Show the destination image, but only where the two images overlap. The + /// source image is not rendered, it is treated merely as a mask. The color + /// channels of the source are ignored, only the opacity has an effect. + /// + /// To show the source image instead, consider [srcIn]. + /// + /// To reverse the semantic of the mask (only showing the source where the + /// destination is present, rather than where it is absent), consider [dstOut]. + /// + /// This corresponds to the "Destination in Source" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstIn.png) + dstIn, + + /// Show the source image, but only where the two images do not overlap. The + /// destination image is not rendered, it is treated merely as a mask. The color + /// channels of the destination are ignored, only the opacity has an effect. + /// + /// To show the destination image instead, consider [dstOut]. + /// + /// To reverse the semantic of the mask (only showing the source where the + /// destination is present, rather than where it is absent), consider [srcIn]. + /// + /// This corresponds to the "Source out Destination" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcOut.png) + srcOut, + + /// Show the destination image, but only where the two images do not overlap. The + /// source image is not rendered, it is treated merely as a mask. The color + /// channels of the source are ignored, only the opacity has an effect. + /// + /// To show the source image instead, consider [srcOut]. + /// + /// To reverse the semantic of the mask (only showing the destination where the + /// source is present, rather than where it is absent), consider [dstIn]. + /// + /// This corresponds to the "Destination out Source" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstOut.png) + dstOut, + + /// Composite the source image over the destination image, but only where it + /// overlaps the destination. + /// + /// This corresponds to the "Source atop Destination" Porter-Duff operator. + /// + /// This is essentially the [srcOver] operator, but with the output's opacity + /// channel being set to that of the destination image instead of being a + /// combination of both image's opacity channels. + /// + /// For a variant with the destination on top instead of the source, see + /// [dstATop]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_srcATop.png) + srcATop, + + /// Composite the destination image over the source image, but only where it + /// overlaps the source. + /// + /// This corresponds to the "Destination atop Source" Porter-Duff operator. + /// + /// This is essentially the [dstOver] operator, but with the output's opacity + /// channel being set to that of the source image instead of being a + /// combination of both image's opacity channels. + /// + /// For a variant with the source on top instead of the destination, see + /// [srcATop]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_dstATop.png) + dstATop, + + /// Apply a bitwise `xor` operator to the source and destination images. This + /// leaves transparency where they would overlap. + /// + /// This corresponds to the "Source xor Destination" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_xor.png) + xor, + + /// Sum the components of the source and destination images. + /// + /// Transparency in a pixel of one of the images reduces the contribution of + /// that image to the corresponding output pixel, as if the color of that + /// pixel in that image was darker. + /// + /// This corresponds to the "Source plus Destination" Porter-Duff operator. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_plus.png) + plus, + + /// Multiply the color components of the source and destination images. + /// + /// This can only result in the same or darker colors (multiplying by white, + /// 1.0, results in no change; multiplying by black, 0.0, results in black). + /// + /// When compositing two opaque images, this has similar effect to overlapping + /// two transparencies on a projector. + /// + /// For a variant that also multiplies the alpha channel, consider [multiply]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_modulate.png) + /// + /// See also: + /// + /// * [screen], which does a similar computation but inverted. + /// * [overlay], which combines [modulate] and [screen] to favor the + /// destination image. + /// * [hardLight], which combines [modulate] and [screen] to favor the + /// source image. + modulate, + + // Following blend modes are defined in the CSS Compositing standard. + + /// Multiply the inverse of the components of the source and destination + /// images, and inverse the result. + /// + /// Inverting the components means that a fully saturated channel (opaque + /// white) is treated as the value 0.0, and values normally treated as 0.0 + /// (black, transparent) are treated as 1.0. + /// + /// This is essentially the same as [modulate] blend mode, but with the values + /// of the colors inverted before the multiplication and the result being + /// inverted back before rendering. + /// + /// This can only result in the same or lighter colors (multiplying by black, + /// 1.0, results in no change; multiplying by white, 0.0, results in white). + /// Similarly, in the alpha channel, it can only result in more opaque colors. + /// + /// This has similar effect to two projectors displaying their images on the + /// same screen simultaneously. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_screen.png) + /// + /// See also: + /// + /// * [modulate], which does a similar computation but without inverting the + /// values. + /// * [overlay], which combines [modulate] and [screen] to favor the + /// destination image. + /// * [hardLight], which combines [modulate] and [screen] to favor the + /// source image. + screen, // The last coeff mode. + + /// Multiply the components of the source and destination images after + /// adjusting them to favor the destination. + /// + /// Specifically, if the destination value is smaller, this multiplies it with + /// the source value, whereas is the source value is smaller, it multiplies + /// the inverse of the source value with the inverse of the destination value, + /// then inverts the result. + /// + /// Inverting the components means that a fully saturated channel (opaque + /// white) is treated as the value 0.0, and values normally treated as 0.0 + /// (black, transparent) are treated as 1.0. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_overlay.png) + /// + /// See also: + /// + /// * [modulate], which always multiplies the values. + /// * [screen], which always multiplies the inverses of the values. + /// * [hardLight], which is similar to [overlay] but favors the source image + /// instead of the destination image. + overlay, + + /// Composite the source and destination image by choosing the lowest value + /// from each color channel. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_darken.png) + darken, + + /// Composite the source and destination image by choosing the highest value + /// from each color channel. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_lighten.png) + lighten, + + /// Divide the destination by the inverse of the source. + /// + /// Inverting the components means that a fully saturated channel (opaque + /// white) is treated as the value 0.0, and values normally treated as 0.0 + /// (black, transparent) are treated as 1.0. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorDodge.png) + colorDodge, + + /// Divide the inverse of the destination by the source, and inverse the result. + /// + /// Inverting the components means that a fully saturated channel (opaque + /// white) is treated as the value 0.0, and values normally treated as 0.0 + /// (black, transparent) are treated as 1.0. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_colorBurn.png) + colorBurn, + + /// Multiply the components of the source and destination images after + /// adjusting them to favor the source. + /// + /// Specifically, if the source value is smaller, this multiplies it with the + /// destination value, whereas is the destination value is smaller, it + /// multiplies the inverse of the destination value with the inverse of the + /// source value, then inverts the result. + /// + /// Inverting the components means that a fully saturated channel (opaque + /// white) is treated as the value 0.0, and values normally treated as 0.0 + /// (black, transparent) are treated as 1.0. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hardLight.png) + /// + /// See also: + /// + /// * [modulate], which always multiplies the values. + /// * [screen], which always multiplies the inverses of the values. + /// * [overlay], which is similar to [hardLight] but favors the destination + /// image instead of the source image. + hardLight, + + /// Use [colorDodge] for source values below 0.5 and [colorBurn] for source + /// values above 0.5. + /// + /// This results in a similar but softer effect than [overlay]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_softLight.png) + /// + /// See also: + /// + /// * [color], which is a more subtle tinting effect. + softLight, + + /// Subtract the smaller value from the bigger value for each channel. + /// + /// Compositing black has no effect; compositing white inverts the colors of + /// the other image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. + /// + /// The effect is similar to [exclusion] but harsher. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_difference.png) + difference, + + /// Subtract double the product of the two images from the sum of the two + /// images. + /// + /// Compositing black has no effect; compositing white inverts the colors of + /// the other image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. + /// + /// The effect is similar to [difference] but softer. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_exclusion.png) + exclusion, + + /// Multiply the components of the source and destination images, including + /// the alpha channel. + /// + /// This can only result in the same or darker colors (multiplying by white, + /// 1.0, results in no change; multiplying by black, 0.0, results in black). + /// + /// Since the alpha channel is also multiplied, a fully-transparent pixel + /// (opacity 0.0) in one image results in a fully transparent pixel in the + /// output. This is similar to [dstIn], but with the colors combined. + /// + /// For a variant that multiplies the colors but does not multiply the alpha + /// channel, consider [modulate]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_multiply.png) + multiply, // The last separable mode. + + /// Take the hue of the source image, and the saturation and luminosity of the + /// destination image. + /// + /// The effect is to tint the destination image with the source image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. Regions that are entirely transparent in the source image take + /// their hue from the destination. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) + /// + /// See also: + /// + /// * [color], which is a similar but stronger effect as it also applies the + /// saturation of the source image. + /// * [HSVColor], which allows colors to be expressed using Hue rather than + /// the red/green/blue channels of [Color]. + hue, + + /// Take the saturation of the source image, and the hue and luminosity of the + /// destination image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. Regions that are entirely transparent in the source image take + /// their saturation from the destination. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_hue.png) + /// + /// See also: + /// + /// * [color], which also applies the hue of the source image. + /// * [luminosity], which applies the luminosity of the source image to the + /// destination. + saturation, + + /// Take the hue and saturation of the source image, and the luminosity of the + /// destination image. + /// + /// The effect is to tint the destination image with the source image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. Regions that are entirely transparent in the source image take + /// their hue and saturation from the destination. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_color.png) + /// + /// See also: + /// + /// * [hue], which is a similar but weaker effect. + /// * [softLight], which is a similar tinting effect but also tints white. + /// * [saturation], which only applies the saturation of the source image. + color, + + /// Take the luminosity of the source image, and the hue and saturation of the + /// destination image. + /// + /// The opacity of the output image is computed in the same way as for + /// [srcOver]. Regions that are entirely transparent in the source image take + /// their luminosity from the destination. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/blend_mode_luminosity.png) + /// + /// See also: + /// + /// * [saturation], which applies the saturation of the source image to the + /// destination. + /// * [ImageFilter.blur], which can be used with [BackdropFilter] for a + /// related effect. + luminosity, +} + +/// Strategies for painting shapes and paths on a canvas. +/// +/// See [Paint.style]. +// These enum values must be kept in sync with SkPaint::Style. +enum PaintingStyle { + // This list comes from Skia's SkPaint.h and the values (order) should be kept + // in sync. + + /// Apply the [Paint] to the inside of the shape. For example, when + /// applied to the [Canvas.drawCircle] call, this results in a disc + /// of the given size being painted. + fill, + + /// Apply the [Paint] to the edge of the shape. For example, when + /// applied to the [Canvas.drawCircle] call, this results is a hoop + /// of the given size being painted. The line drawn on the edge will + /// be the width given by the [Paint.width] property. + stroke, +} + +/// Styles to use for line endings. +/// +/// See also: +/// +/// * [Paint.strokeCap] for how this value is used. +/// * [StrokeJoin] for the different kinds of line segment joins. +// These enum values must be kept in sync with SkPaint::Cap. +enum StrokeCap { + /// Begin and end contours with a flat edge and no extension. + /// + /// ![A butt cap ends line segments with a square end that stops at the end of + /// the line segment.](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/butt_cap.png) + /// + /// Compare to the [square] cap, which has the same shape, but extends past + /// the end of the line by half a stroke width. + butt, + + /// Begin and end contours with a semi-circle extension. + /// + /// ![A round cap adds a rounded end to the line segment that protrudes + /// by one half of the thickness of the line (which is the radius of the cap) + /// past the end of the segment.](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_cap.png) + /// + /// The cap is colored in the diagram above to highlight it: in normal use it + /// is the same color as the line. + round, + + /// Begin and end contours with a half square extension. This is + /// similar to extending each contour by half the stroke width (as + /// given by [Paint.width]). + /// + /// ![A square cap has a square end that effectively extends the line length + /// by half of the stroke width.](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/square_cap.png) + /// + /// The cap is colored in the diagram above to highlight it: in normal use it + /// is the same color as the line. + /// + /// Compare to the [butt] cap, which has the same shape, but doesn't extend + /// past the end of the line. + square, +} + +/// Styles to use for line segment joins. +/// +/// This only affects line joins for polygons drawn by [Canvas.drawPath] and +/// rectangles, not points drawn as lines with [Canvas.drawPoints]. +/// +/// See also: +/// +/// * [Paint.join] and [Paint.miterLimit] for how this value is +/// used. +/// * [StrokeCap] for the different kinds of line endings. +// These enum values must be kept in sync with SkPaint::Join. +enum StrokeJoin { + /// Joins between line segments form sharp corners. + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} + /// + /// The center of the line segment is colored in the diagram above to + /// highlight the join, but in normal usage the join is the same color as the + /// line. + /// + /// See also: + /// + /// * [Paint.join], used to set the line segment join style to this + /// value. + /// * [Paint.miterLimit], used to define when a miter is drawn instead + /// of a bevel when the join is set to this value. + miter, + + /// Joins between line segments are semi-circular. + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4} + /// + /// The center of the line segment is colored in the diagram above to + /// highlight the join, but in normal usage the join is the same color as the + /// line. + /// + /// See also: + /// + /// * [Paint.join], used to set the line segment join style to this + /// value. + round, + + /// Joins between line segments connect the corners of the butt ends of the + /// line segments to give a beveled appearance. + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4} + /// + /// The center of the line segment is colored in the diagram above to + /// highlight the join, but in normal usage the join is the same color as the + /// line. + /// + /// See also: + /// + /// * [Paint.join], used to set the line segment join style to this + /// value. + bevel, +} + +/// Defines what happens at the edge of a gradient or the sampling of a source image +/// in an [ImageFilter]. +/// +/// A gradient is defined along a finite inner area. In the case of a linear +/// gradient, it's between the parallel lines that are orthogonal to the line +/// drawn between two points. In the case of radial gradients, it's the disc +/// that covers the circle centered on a particular point up to a given radius. +/// +/// An image filter reads source samples from a source image and performs operations +/// on those samples to produce a result image. An image defines color samples only +/// for pixels within the bounds of the image but some filter operations, such as a blur +/// filter, read samples over a wide area to compute the output for a given pixel. Such +/// a filter would need to combine samples from inside the image with hypothetical +/// color values from outside the image. +/// +/// This enum is used to define how the gradient or image filter should treat the regions +/// outside that defined inner area. +/// +/// See also: +/// +/// * [painting.Gradient], the superclass for [LinearGradient] and +/// [RadialGradient], as used by [BoxDecoration] et al, which works in +/// relative coordinates and can create a [Shader] representing the gradient +/// for a particular [Rect] on demand. +/// * [dart:ui.Gradient], the low-level class used when dealing with the +/// [Paint.shader] property directly, with its [Gradient.linear] and +/// [Gradient.radial] constructors. +/// * [dart:ui.ImageFilter.blur], an ImageFilter that may sometimes need to +/// read samples from outside an image to combine with the pixels near the +/// edge of the image. +// These enum values must be kept in sync with SkTileMode. +enum TileMode { + /// Samples beyond the edge are clamped to the nearest color in the defined inner area. + /// + /// A gradient will paint all the regions outside the inner area with the + /// color at the end of the color stop list closest to that region. + /// + /// An image filter will substitute the nearest edge pixel for any samples taken from + /// outside its source image. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png) + clamp, + + /// Samples beyond the edge are repeated from the far end of the defined area. + /// + /// For a gradient, this technique is as if the stop points from 0.0 to 1.0 were then + /// repeated from 1.0 to 2.0, 2.0 to 3.0, and so forth (and for linear gradients, similarly + /// from -1.0 to 0.0, -2.0 to -1.0, etc). + /// + /// An image filter will treat its source image as if it were tiled across the enlarged + /// sample space from which it reads, each tile in the same orientation as the base image. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png) + repeated, + + /// Samples beyond the edge are mirrored back and forth across the defined area. + /// + /// For a gradient, this technique is as if the stop points from 0.0 to 1.0 were then + /// repeated backwards from 2.0 to 1.0, then forwards from 2.0 to 3.0, then backwards + /// again from 4.0 to 3.0, and so forth (and for linear gradients, similarly in the + /// negative direction). + /// + /// An image filter will treat its source image as tiled in an alternating forwards and + /// backwards or upwards and downwards direction across the sample space from which + /// it is reading. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png) + mirror, + + /// Samples beyond the edge are treated as transparent black. + /// + /// A gradient will render transparency over any region that is outside the circle of a + /// radial gradient or outside the parallel lines that define the inner area of a linear + /// gradient. + /// + /// An image filter will substitute transparent black for any sample it must read from + /// outside its source image. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_linear.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radial.png) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_sweep.png) + decal, +} + +/// A description of how to update the current text position. +/// +/// If [reset] is true, this update discards the previous current text position. +/// Otherwise, it appends to the previous text position. +@immutable +class TextPosition { + /// See [TextPosition]. + const TextPosition({ + this.x, + this.y, + this.dx, + this.dy, + this.reset = false, + this.transform, + }); + + /// The horizontal axis coordinate for the current text position. + /// + /// If null, use the current text position accumulated since the last [reset], + /// or 0 if this represents a reset. + final double? x; + + /// The horizontal axis coordinate to add to the current text position. + /// + /// If null, use the current text position accumulated since the last [reset], + /// or 0 if this represents a reset. + final double? dx; + + /// The vertical axis coordinate for the current text position. + /// + /// If null, use the current text position accumulated since the last [reset], + /// or 0 if this represents a reset. + final double? y; + + /// The vertical axis coordinate to add to the current text position. + /// + /// If null, use the current text position accumulated since the last [reset], + /// or 0 if this represents a reset. + final double? dy; + + /// If true, reset the current text position using [x] and [y]. + final bool reset; + + /// A transform applied to the rendered font. + /// + /// If `null` this implies no transform. + final AffineMatrix? transform; + + @override + int get hashCode => Object.hash(x, y, dx, dy, reset, transform); + + @override + bool operator ==(Object other) { + return other is TextPosition && + other.x == x && + other.y == y && + other.dx == dx && + other.dy == dy && + other.reset == reset && + other.transform == transform; + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer(); + buffer.write('TextPosition(reset: $reset'); + if (x != null) { + buffer.write(', x: $x'); + } + if (y != null) { + buffer.write(', y: $y'); + } + if (dx != null) { + buffer.write(', dx: $dx'); + } + if (dy != null) { + buffer.write(', dy: $dy'); + } + if (transform != null) { + buffer.write(', transform: $transform'); + } + buffer.write(')'); + return buffer.toString(); + } +} + +/// Additional text specific configuration that is added to the encoding. +@immutable +class TextConfig { + /// Create a new [TextStyle] object. + const TextConfig( + this.text, + this.xAnchorMultiplier, + this.fontFamily, + this.fontWeight, + this.fontSize, + this.decoration, + this.decorationStyle, + this.decorationColor, + ); + + /// The text to be rendered. + final String text; + + /// A multiplier for text anchoring. + /// + /// This value should be multiplied by the length of the longest line in the + /// text and subtracted from x coordinate of the current [TextPosition]. + final double xAnchorMultiplier; + + /// The size of the font, only supported as absolute size. + final double fontSize; + + /// The name of the font family to select for rendering. + final String? fontFamily; + + /// The font weight, converted to a weight constant. + final FontWeight fontWeight; + + /// The decoration to apply to the text. + final TextDecoration decoration; + + /// The decoration style to apply to the text. + final TextDecorationStyle decorationStyle; + + /// The color to use for the decoration, if any. + final Color decorationColor; + + @override + int get hashCode => Object.hash( + text, + xAnchorMultiplier, + fontSize, + fontFamily, + fontWeight, + decoration, + decorationStyle, + decorationColor, + ); + + @override + bool operator ==(Object other) { + return other is TextConfig && + other.text == text && + other.xAnchorMultiplier == xAnchorMultiplier && + other.fontSize == fontSize && + other.fontFamily == fontFamily && + other.fontWeight == fontWeight && + other.decoration == decoration && + other.decorationStyle == decorationStyle && + other.decorationColor == decorationColor; + } + + @override + String toString() { + return 'TextConfig(' + "'$text', " + '$xAnchorMultiplier, ' + "'$fontFamily', " + '$fontWeight, ' + '$fontSize, ' + '$decoration, ' + '$decorationStyle, ' + '$decorationColor,)'; + } +} + +/// The value of the font weight. +/// +/// This matches the enum values defined in dart:ui. +enum FontWeight { + /// A font weight of 100, + w100, + + /// A font weight of 200, + w200, + + /// A font weight of 300, + w300, + + /// A font weight of 400, + w400, + + /// A font weight of 500, + w500, + + /// A font weight of 600, + w600, + + /// A font weight of 700, + w700, + + /// A font weight of 800, + w800, + + /// A font weight of 900, + w900, +} + +/// The style in which to draw a text decoration +/// +/// This matches the enum values defined in dart:ui. +enum TextDecorationStyle { + /// Draw a solid line + solid, + + /// Draw two lines + double, + + /// Draw a dotted line + dotted, + + /// Draw a dashed line + dashed, + + /// Draw a sinusoidal line + wavy +} + +/// A linear decoration to draw near the text. +/// +/// This matches the enum values defined in dart:ui. +@immutable +class TextDecoration { + const TextDecoration._(this.mask); + + /// Creates a decoration that paints the union of all the given decorations. + factory TextDecoration.combine(List decorations) { + int mask = 0; + for (final TextDecoration decoration in decorations) { + mask |= decoration.mask; + } + return TextDecoration._(mask); + } + + /// The raw mask for serialization. + final int mask; + + /// Whether this decoration will paint at least as much decoration as the given decoration. + bool contains(TextDecoration other) { + return (mask | other.mask) == mask; + } + + /// Do not draw a decoration + static const TextDecoration none = TextDecoration._(kNoTextDecorationMask); + + /// Draw a line underneath each line of text + static const TextDecoration underline = TextDecoration._(kUnderlineMask); + + /// Draw a line above each line of text + static const TextDecoration overline = TextDecoration._(kOverlineMask); + + /// Draw a line through each line of text + static const TextDecoration lineThrough = TextDecoration._(kLineThroughMask); + + @override + bool operator ==(Object other) { + return other is TextDecoration && other.mask == mask; + } + + @override + int get hashCode => mask.hashCode; + + @override + String toString() { + if (mask == 0) { + return 'TextDecoration.none'; + } + final List values = []; + if (mask & underline.mask != 0) { + values.add('underline'); + } + if (mask & overline.mask != 0) { + values.add('overline'); + } + if (mask & lineThrough.mask != 0) { + values.add('lineThrough'); + } + if (values.length == 1) { + return 'TextDecoration.${values[0]}'; + } + return 'TextDecoration.combine([${values.join(", ")}])'; + } +} + +/// The default font weight. +const FontWeight normalFontWeight = FontWeight.w400; + +/// A commonly used font weight that is heavier than normal. +const FontWeight boldFontWeight = FontWeight.w700; diff --git a/packages/vector_graphics_compiler/lib/src/svg/_path_ops_ffi.dart b/packages/vector_graphics_compiler/lib/src/svg/_path_ops_ffi.dart new file mode 100644 index 00000000000..fd1eb3bed3e --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/_path_ops_ffi.dart @@ -0,0 +1,301 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: camel_case_types +import 'dart:ffi' as ffi; +import 'dart:typed_data'; + +import 'path_ops.dart'; + +// TODO(dnfield): Figure out where to put this. +// https://github.com/flutter/flutter/issues/99563 +final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open(_dylibPath); +late final String _dylibPath; + +/// Creates a path object to operate on. +/// +/// First, build up the path contours with the [moveTo], [lineTo], [cubicTo], +/// and [close] methods. All methods expect absolute coordinates. +/// +/// Finally, use the [dispose] method to clean up native resources. After +/// [dispose] has been called, this class must not be used again. +class Path implements PathProxy { + /// Creates an empty path object with the specified fill type. + Path([FillType fillType = FillType.nonZero]) + : _path = _createPathFn(fillType.index); + + /// Creates a copy of this path. + factory Path.from(Path other) { + final Path result = Path(other.fillType); + other.replay(result); + return result; + } + + /// The [FillType] of this path. + FillType get fillType { + assert(_path != null); + return FillType.values[_getFillTypeFn(_path!)]; + } + + ffi.Pointer<_SkPath>? _path; + ffi.Pointer<_PathData>? _pathData; + + /// The number of points used by each [PathVerb]. + static const Map pointsPerVerb = { + PathVerb.moveTo: 2, + PathVerb.lineTo: 2, + PathVerb.cubicTo: 6, + PathVerb.close: 0, + }; + + /// Makes the appropriate calls using [verbs] and [points] to replay this path + /// on [proxy]. + /// + /// Calls [PathProxy.reset] first if [reset] is true. + void replay(PathProxy proxy, {bool reset = true}) { + if (reset) { + proxy.reset(); + } + int index = 0; + for (final PathVerb verb in verbs.toList()) { + switch (verb) { + case PathVerb.moveTo: + proxy.moveTo(points[index++], points[index++]); + case PathVerb.lineTo: + proxy.lineTo(points[index++], points[index++]); + case PathVerb.quadTo: + // TODO(dnfield): Avoid degree elevation? + // The binary format only supports cubics. Skia might have + // used a quad when combining paths somewhere though. + final double cpX = points[index++]; + final double cpY = points[index++]; + proxy.cubicTo( + cpX, + cpY, + cpX, + cpY, + points[index++], + points[index++], + ); + case PathVerb.cubicTo: + proxy.cubicTo( + points[index++], + points[index++], + points[index++], + points[index++], + points[index++], + points[index++], + ); + case PathVerb.close: + proxy.close(); + } + } + assert(index == points.length); + } + + /// The list of path verbs in this path. + /// + /// This may not match the verbs supplied by calls to [moveTo], [lineTo], + /// [cubicTo], and [close] after [applyOp] is invoked. + /// + /// This list determines the meaning of the [points] array. + + static const Map pathVerbDict = { + 0: PathVerb.moveTo, + 1: PathVerb.lineTo, + 2: PathVerb.quadTo, + 4: PathVerb.cubicTo, + 5: PathVerb.close + }; + + /// Retrieves PathVerbs. + Iterable get verbs { + _updatePathData(); + final int count = _pathData!.ref.verbCount; + return List.generate(count, (int index) { + return pathVerbDict[_pathData!.ref.verbs[index]]!; + }, growable: false); + } + + /// The list of points to use with [verbs]. + /// + /// Each verb uses a specific number of points, specified by the + /// [pointsPerVerb] map. + Float32List get points { + _updatePathData(); + return _pathData!.ref.points.asTypedList(_pathData!.ref.pointCount); + } + + void _updatePathData() { + assert(_path != null); + _pathData ??= _dataFn(_path!); + } + + void _resetPathData() { + if (_pathData != null) { + _destroyDataFn(_pathData!); + } + _pathData = null; + } + + @override + void moveTo(double x, double y) { + assert(_path != null); + _resetPathData(); + _moveToFn(_path!, x, y); + } + + @override + void lineTo(double x, double y) { + assert(_path != null); + _resetPathData(); + _lineToFn(_path!, x, y); + } + + @override + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + assert(_path != null); + _resetPathData(); + _cubicToFn(_path!, x1, y1, x2, y2, x3, y3); + } + + @override + void close() { + assert(_path != null); + _resetPathData(); + _closeFn(_path!, true); + } + + @override + void reset() { + assert(_path != null); + _resetPathData(); + _resetFn(_path!); + } + + /// Releases native resources. + /// + /// After calling dispose, this class must not be used again. + void dispose() { + assert(_path != null); + _resetPathData(); + _destroyFn(_path!); + _path = null; + } + + /// Applies the operation described by [op] to this path using [other]. + Path applyOp(Path other, PathOp op) { + assert(_path != null); + assert(other._path != null); + final Path result = Path.from(this); + _opFn(result._path!, other._path!, op.index); + return result; + } +} + +/// Whether or not PathOps should be used. +bool get isPathOpsInitialized => _isPathOpsInitialized; +bool _isPathOpsInitialized = false; + +/// Initialize the libpathops dynamic library. +void initializeLibPathOps(String path) { + _dylibPath = path; + _isPathOpsInitialized = true; +} + +base class _SkPath extends ffi.Opaque {} + +base class _PathData extends ffi.Struct { + external ffi.Pointer verbs; + + @ffi.Size() + external int verbCount; + + external ffi.Pointer points; + + @ffi.Size() + external int pointCount; +} + +typedef _CreatePathType = ffi.Pointer<_SkPath> Function(int); +typedef _create_path_type = ffi.Pointer<_SkPath> Function(ffi.Int); + +final _CreatePathType _createPathFn = + _dylib.lookupFunction<_create_path_type, _CreatePathType>( + 'CreatePath', +); + +typedef _MoveToType = void Function(ffi.Pointer<_SkPath>, double, double); +typedef _move_to_type = ffi.Void Function( + ffi.Pointer<_SkPath>, ffi.Float, ffi.Float); + +final _MoveToType _moveToFn = _dylib.lookupFunction<_move_to_type, _MoveToType>( + 'MoveTo', +); + +typedef _LineToType = void Function(ffi.Pointer<_SkPath>, double, double); +typedef _line_to_type = ffi.Void Function( + ffi.Pointer<_SkPath>, ffi.Float, ffi.Float); + +final _LineToType _lineToFn = _dylib.lookupFunction<_line_to_type, _LineToType>( + 'LineTo', +); + +typedef _CubicToType = void Function( + ffi.Pointer<_SkPath>, double, double, double, double, double, double); +typedef _cubic_to_type = ffi.Void Function(ffi.Pointer<_SkPath>, ffi.Float, + ffi.Float, ffi.Float, ffi.Float, ffi.Float, ffi.Float); + +final _CubicToType _cubicToFn = + _dylib.lookupFunction<_cubic_to_type, _CubicToType>('CubicTo'); + +typedef _CloseType = void Function(ffi.Pointer<_SkPath>, bool); +typedef _close_type = ffi.Void Function(ffi.Pointer<_SkPath>, ffi.Bool); + +final _CloseType _closeFn = + _dylib.lookupFunction<_close_type, _CloseType>('Close'); + +typedef _ResetType = void Function(ffi.Pointer<_SkPath>); +typedef _reset_type = ffi.Void Function(ffi.Pointer<_SkPath>); + +final _ResetType _resetFn = + _dylib.lookupFunction<_reset_type, _ResetType>('Reset'); + +typedef _DestroyType = void Function(ffi.Pointer<_SkPath>); +typedef _destroy_type = ffi.Void Function(ffi.Pointer<_SkPath>); + +final _DestroyType _destroyFn = + _dylib.lookupFunction<_destroy_type, _DestroyType>('DestroyPath'); + +typedef _OpType = void Function( + ffi.Pointer<_SkPath>, ffi.Pointer<_SkPath>, int); +typedef _op_type = ffi.Void Function( + ffi.Pointer<_SkPath>, ffi.Pointer<_SkPath>, ffi.Int); + +final _OpType _opFn = _dylib.lookupFunction<_op_type, _OpType>('Op'); + +typedef _PathDataType = ffi.Pointer<_PathData> Function(ffi.Pointer<_SkPath>); +typedef _path_data_type = ffi.Pointer<_PathData> Function(ffi.Pointer<_SkPath>); + +final _PathDataType _dataFn = + _dylib.lookupFunction<_path_data_type, _PathDataType>('Data'); + +typedef _DestroyDataType = void Function(ffi.Pointer<_PathData>); +typedef _destroy_data_type = ffi.Void Function(ffi.Pointer<_PathData>); + +final _DestroyDataType _destroyDataFn = + _dylib.lookupFunction<_destroy_data_type, _DestroyDataType>('DestroyData'); + +typedef _GetFillTypeType = int Function(ffi.Pointer<_SkPath>); +typedef _get_fill_type_type = ffi.Int32 Function(ffi.Pointer<_SkPath>); + +final _GetFillTypeType _getFillTypeFn = + _dylib.lookupFunction<_get_fill_type_type, _GetFillTypeType>('GetFillType'); diff --git a/packages/vector_graphics_compiler/lib/src/svg/_path_ops_unsupported.dart b/packages/vector_graphics_compiler/lib/src/svg/_path_ops_unsupported.dart new file mode 100644 index 00000000000..773c9425fcf --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/_path_ops_unsupported.dart @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'path_ops.dart'; + +/// Whether or not tesselation should be used. +bool get isPathOpsInitialized => false; + +/// Initialize the libpathops dynamic library. +void initializeLibPathOps(String path) {} + +/// Creates a path object to operate on. +class Path implements PathProxy { + /// Creates an empty path object with the specified fill type. + Path([this.fillType = FillType.nonZero]); + + /// Creates a copy of this path. + factory Path.from(Path other) { + final Path result = Path(other.fillType); + other.replay(result); + return result; + } + + /// The [FillType] of this path. + final FillType fillType; + + /// Makes the appropriate calls using [verbs] and [points] to replay this path + /// on [proxy]. + /// + /// Calls [PathProxy.reset] first if [reset] is true. + void replay(PathProxy proxy, {bool reset = true}) { + throw UnsupportedError('PathOps not supported on the web'); + } + + @override + void close() { + throw UnsupportedError('PathOps not supported on the web'); + } + + @override + void cubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + throw UnsupportedError('PathOps not supported on the web'); + } + + @override + void lineTo(double x, double y) { + throw UnsupportedError('PathOps not supported on the web'); + } + + @override + void moveTo(double x, double y) { + throw UnsupportedError('PathOps not supported on the web'); + } + + @override + void reset() { + throw UnsupportedError('PathOps not supported on the web'); + } + + /// Applies the operation described by [op] to this path using [other]. + Path applyOp(Path other, PathOp op) { + throw UnsupportedError('PathOps not supported on the web'); + } + + /// Retrieves PathVerbs. + Iterable get verbs { + throw UnsupportedError('PathOps not supported on the web'); + } + + /// The list of points to use with [verbs]. + /// + /// Each verb uses a specific number of points, specified by the + /// [pointsPerVerb] map. + Float32List get points { + throw UnsupportedError('PathOps not supported on the web'); + } + + /// Releases native resources. + /// + /// After calling dispose, this class must not be used again. + void dispose() { + throw UnsupportedError('PathOps not supported on the web'); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/_tessellator_ffi.dart b/packages/vector_graphics_compiler/lib/src/svg/_tessellator_ffi.dart new file mode 100644 index 00000000000..4850e020a3d --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/_tessellator_ffi.dart @@ -0,0 +1,367 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: camel_case_types +import 'dart:ffi' as ffi; +import 'dart:typed_data'; + +import '../geometry/path.dart'; +import '../geometry/vertices.dart'; +import '../paint.dart'; +import 'node.dart'; +import 'parser.dart'; +import 'resolver.dart'; +import 'tessellator.dart' as api; +import 'visitor.dart'; + +// TODO(dnfield): Figure out where to put this. +// https://github.com/flutter/flutter/issues/99563 +final ffi.DynamicLibrary _dylib = ffi.DynamicLibrary.open(_dylibPath); +late final String _dylibPath; + +/// Whether or not tesselation should be used. +bool get isTesselatorInitialized => _isTesselatorInitialized; +bool _isTesselatorInitialized = false; + +/// Initialize the libtesselator dynamic library. +/// +/// This method must be called before [VerticesBuilder] can be used or +/// constructed. +void initializeLibTesselator(String path) { + _dylibPath = path; + _isTesselatorInitialized = true; +} + +/// A visitor that replaces fill paths with tesselated vertices. +class Tessellator extends Visitor + with ErrorOnUnResolvedNode + implements api.Tessellator { + @override + Node visitEmptyNode(Node node, void data) { + return node; + } + + @override + Node visitParentNode(ParentNode parentNode, void data) { + return ParentNode(SvgAttributes.empty, children: [ + for (final Node child in parentNode.children) child.accept(this, data) + ]); + } + + @override + Node visitResolvedClipNode(ResolvedClipNode clipNode, void data) { + return ResolvedClipNode( + clips: clipNode.clips, + child: clipNode.child.accept(this, data), + ); + } + + @override + Node visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + return ResolvedMaskNode( + child: maskNode.child.accept(this, data), + mask: maskNode.mask, + blendMode: maskNode.blendMode, + ); + } + + @override + Node visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + return ResolvedTextPositionNode( + textPositionNode.textPosition, + [ + for (final Node child in textPositionNode.children) + child.accept(this, data) + ], + ); + } + + @override + Node visitResolvedPath(ResolvedPathNode pathNode, void data) { + final Fill? fill = pathNode.paint.fill; + final Stroke? stroke = pathNode.paint.stroke; + if (fill == null && stroke != null) { + return pathNode; + } + + final List children = []; + if (fill != null) { + final VerticesBuilder builder = VerticesBuilder(); + for (final PathCommand command in pathNode.path.commands) { + switch (command.type) { + case PathCommandType.move: + final MoveToCommand move = command as MoveToCommand; + builder.moveTo(move.x, move.y); + case PathCommandType.line: + final LineToCommand line = command as LineToCommand; + builder.lineTo(line.x, line.y); + case PathCommandType.cubic: + final CubicToCommand cubic = command as CubicToCommand; + builder.cubicTo( + cubic.x1, + cubic.y1, + cubic.x2, + cubic.y2, + cubic.x3, + cubic.y3, + ); + case PathCommandType.close: + builder.close(); + } + } + final Float32List rawVertices = builder.tessellate( + fillType: pathNode.path.fillType, + ); + if (rawVertices.isNotEmpty) { + final Vertices vertices = Vertices.fromFloat32List(rawVertices); + final IndexedVertices indexedVertices = vertices.createIndex(); + children.add(ResolvedVerticesNode( + paint: Paint(blendMode: pathNode.paint.blendMode, fill: fill), + vertices: indexedVertices, + bounds: pathNode.bounds, + )); + } + } + if (stroke != null) { + children.add(ResolvedPathNode( + paint: Paint( + blendMode: pathNode.paint.blendMode, + stroke: stroke, + ), + bounds: pathNode.bounds, + path: pathNode.path)); + } + if (children.isEmpty) { + return Node.empty; + } + if (children.length > 1) { + return ParentNode(SvgAttributes.empty, children: children); + } + return children[0]; + } + + @override + Node visitResolvedText(ResolvedTextNode textNode, void data) { + return textNode; + } + + @override + Node visitSaveLayerNode(SaveLayerNode layerNode, void data) { + return SaveLayerNode(SvgAttributes.empty, + paint: layerNode.paint, + children: [ + for (final Node child in layerNode.children) child.accept(this, data), + ]); + } + + @override + Node visitViewportNode(ViewportNode viewportNode, void data) { + return ViewportNode( + SvgAttributes.empty, + width: viewportNode.width, + height: viewportNode.height, + transform: viewportNode.transform, + children: [ + for (final Node child in viewportNode.children) + child.accept(this, data), + ], + ); + } + + @override + Node visitResolvedVerticesNode(ResolvedVerticesNode verticesNode, void data) { + return verticesNode; + } + + @override + Node visitResolvedImageNode(ResolvedImageNode resolvedImageNode, void data) { + return resolvedImageNode; + } + + @override + Node visitResolvedPatternNode(ResolvedPatternNode patternNode, void data) { + return patternNode; + } +} + +/// Creates vertices from path commands. +/// +/// First, build up the path contours with the [moveTo], [lineTo], [cubicTo], +/// and [close] methods. All methods expect absolute coordinates. +/// +/// Then, use the [tessellate] method to create a [Float32List] of vertex pairs. +/// +/// Finally, use the [dispose] method to clean up native resources. After +/// [dispose] has been called, this class must not be used again. +class VerticesBuilder { + /// Create a new [VerticesBuilder]. + VerticesBuilder() : _builder = _createPathFn(); + + ffi.Pointer<_PathBuilder>? _builder; + final List> _vertices = >[]; + + /// Adds a move verb to the absolute coordinates x,y. + void moveTo(double x, double y) { + assert(_builder != null); + _moveToFn(_builder!, x, y); + } + + /// Adds a line verb to the absolute coordinates x,y. + void lineTo(double x, double y) { + assert(_builder != null); + _lineToFn(_builder!, x, y); + } + + /// Adds a cubic Bezier curve with x1,y1 as the first control point, x2,y2 as + /// the second control point, and end point x3,y3. + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + assert(_builder != null); + _cubicToFn(_builder!, x1, y1, x2, y2, x3, y3); + } + + /// Adds a close command to the start of the current contour. + void close() { + assert(_builder != null); + _closeFn(_builder!, true); + } + + /// Tessellates the path created by the previous method calls into a list of + /// vertices. + Float32List tessellate({ + PathFillType fillType = PathFillType.nonZero, + api.SmoothingApproximation smoothing = const api.SmoothingApproximation(), + }) { + assert(_vertices.isEmpty); + assert(_builder != null); + final ffi.Pointer<_Vertices> vertices = _tessellateFn( + _builder!, + fillType.index, + smoothing.scale, + smoothing.angleTolerance, + smoothing.cuspLimit, + ); + _vertices.add(vertices); + return vertices.ref.points.asTypedList(vertices.ref.size); + } + + /// Releases native resources. + /// + /// After calling dispose, this class must not be used again. + void dispose() { + assert(_builder != null); + _vertices.forEach(_destroyVerticesFn); + _destroyFn(_builder!); + _vertices.clear(); + _builder = null; + } +} + +base class _Vertices extends ffi.Struct { + external ffi.Pointer points; + + @ffi.Uint32() + external int size; +} + +base class _PathBuilder extends ffi.Opaque {} + +typedef _CreatePathBuilderType = ffi.Pointer<_PathBuilder> Function(); +typedef _create_path_builder_type = ffi.Pointer<_PathBuilder> Function(); + +final _CreatePathBuilderType _createPathFn = + _dylib.lookupFunction<_create_path_builder_type, _CreatePathBuilderType>( + 'CreatePathBuilder', +); + +typedef _MoveToType = void Function(ffi.Pointer<_PathBuilder>, double, double); +typedef _move_to_type = ffi.Void Function( + ffi.Pointer<_PathBuilder>, + ffi.Float, + ffi.Float, +); + +final _MoveToType _moveToFn = _dylib.lookupFunction<_move_to_type, _MoveToType>( + 'MoveTo', +); + +typedef _LineToType = void Function(ffi.Pointer<_PathBuilder>, double, double); +typedef _line_to_type = ffi.Void Function( + ffi.Pointer<_PathBuilder>, + ffi.Float, + ffi.Float, +); + +final _LineToType _lineToFn = _dylib.lookupFunction<_line_to_type, _LineToType>( + 'LineTo', +); + +typedef _CubicToType = void Function( + ffi.Pointer<_PathBuilder>, + double, + double, + double, + double, + double, + double, +); +typedef _cubic_to_type = ffi.Void Function( + ffi.Pointer<_PathBuilder>, + ffi.Float, + ffi.Float, + ffi.Float, + ffi.Float, + ffi.Float, + ffi.Float, +); + +final _CubicToType _cubicToFn = + _dylib.lookupFunction<_cubic_to_type, _CubicToType>('CubicTo'); + +typedef _CloseType = void Function(ffi.Pointer<_PathBuilder>, bool); +typedef _close_type = ffi.Void Function(ffi.Pointer<_PathBuilder>, ffi.Bool); + +final _CloseType _closeFn = + _dylib.lookupFunction<_close_type, _CloseType>('Close'); + +typedef _TessellateType = ffi.Pointer<_Vertices> Function( + ffi.Pointer<_PathBuilder>, + int, + double, + double, + double, +); +typedef _tessellate_type = ffi.Pointer<_Vertices> Function( + ffi.Pointer<_PathBuilder>, + ffi.Int, + ffi.Float, + ffi.Float, + ffi.Float, +); + +final _TessellateType _tessellateFn = + _dylib.lookupFunction<_tessellate_type, _TessellateType>('Tessellate'); + +typedef _DestroyType = void Function(ffi.Pointer<_PathBuilder>); +typedef _destroy_type = ffi.Void Function(ffi.Pointer<_PathBuilder>); + +final _DestroyType _destroyFn = + _dylib.lookupFunction<_destroy_type, _DestroyType>( + 'DestroyPathBuilder', +); + +typedef _DestroyVerticesType = void Function(ffi.Pointer<_Vertices>); +typedef _destroy_vertices_type = ffi.Void Function(ffi.Pointer<_Vertices>); + +final _DestroyVerticesType _destroyVerticesFn = + _dylib.lookupFunction<_destroy_vertices_type, _DestroyVerticesType>( + 'DestroyVertices', +); diff --git a/packages/vector_graphics_compiler/lib/src/svg/_tessellator_unsupported.dart b/packages/vector_graphics_compiler/lib/src/svg/_tessellator_unsupported.dart new file mode 100644 index 00000000000..cfee6dede51 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/_tessellator_unsupported.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'node.dart'; +import 'resolver.dart'; +import 'tessellator.dart' as api; +import 'visitor.dart'; + +/// Whether or not tesselation should be used. +bool get isTesselatorInitialized => false; + +/// Initialize the libtesselator dynamic library. +/// +/// This method must be called before [VerticesBuilder] can be used or +/// constructed. +void initializeLibTesselator(String path) {} + +/// A visitor that replaces fill paths with tesselated vertices. +class Tessellator extends Visitor + with ErrorOnUnResolvedNode + implements api.Tessellator { + @override + Node visitEmptyNode(Node node, void data) { + return node; + } + + @override + Node visitParentNode(ParentNode parentNode, void data) { + return parentNode; + } + + @override + Node visitResolvedClipNode(ResolvedClipNode clipNode, void data) { + return clipNode; + } + + @override + Node visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + return maskNode; + } + + @override + Node visitResolvedPath(ResolvedPathNode pathNode, void data) { + return pathNode; + } + + @override + Node visitResolvedText(ResolvedTextNode textNode, void data) { + return textNode; + } + + @override + Node visitSaveLayerNode(SaveLayerNode layerNode, void data) { + return layerNode; + } + + @override + Node visitViewportNode(ViewportNode viewportNode, void data) { + return viewportNode; + } + + @override + Node visitResolvedVerticesNode(ResolvedVerticesNode verticesNode, void data) { + return verticesNode; + } + + @override + Node visitResolvedImageNode(ResolvedImageNode resolvedImageNode, void data) { + return resolvedImageNode; + } + + @override + Node visitResolvedPatternNode(ResolvedPatternNode patternNode, void data) { + return patternNode; + } + + @override + Node visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + return textPositionNode; + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/clipping_optimizer.dart b/packages/vector_graphics_compiler/lib/src/svg/clipping_optimizer.dart new file mode 100644 index 00000000000..de827466f2f --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/clipping_optimizer.dart @@ -0,0 +1,269 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../vector_graphics_compiler.dart'; +import 'masking_optimizer.dart'; +import 'node.dart'; +import 'path_ops.dart' as path_ops; +import 'visitor.dart'; + +class _Result { + _Result(this.node); + + final Node node; + int childCount = 0; + List children = []; + Node parent = Node.empty; + bool deleteClipNode = true; +} + +/// Applies and removes trivial cases of clipping. +/// This will not optimize cases where 'stroke-width' is set, +/// there are multiple path nodes in ResolvedClipNode.clips +/// or cases where the intersection of the clip and the path +/// results in Path.commands being empty. +class ClippingOptimizer extends Visitor<_Result, Node> + with ErrorOnUnResolvedNode<_Result, Node> { + ///List of clips to apply. + final List clipsToApply = []; + + /// Applies visitor to given node. + Node apply(Node node) { + final Node newNode = node.accept(this, null).node; + return newNode; + } + + /// Applies clip to a path node, and returns resulting path node. + ResolvedPathNode applyClip(Node child, Path clipPath) { + final ResolvedPathNode pathNode = child as ResolvedPathNode; + final path_ops.Path clipPathOpsPath = toPathOpsPath(clipPath); + final path_ops.Path pathPathOpsPath = toPathOpsPath(pathNode.path); + final path_ops.Path intersection = + clipPathOpsPath.applyOp(pathPathOpsPath, path_ops.PathOp.intersect); + final Path newPath = toVectorGraphicsPath(intersection); + final ResolvedPathNode newPathNode = ResolvedPathNode( + paint: pathNode.paint, bounds: newPath.bounds(), path: newPath); + + clipPathOpsPath.dispose(); + pathPathOpsPath.dispose(); + intersection.dispose(); + + return newPathNode; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitEmptyNode(Node node, void data) { + final _Result result = _Result(node); + return result; + } + + /// Visits applies optimizer to all children of ResolvedClipNode. + // ignore: library_private_types_in_public_api + _Result visitChildren(Node node, _Result data) { + if (node is ResolvedClipNode) { + data = node.child.accept(this, data); + } + return data; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitParentNode(ParentNode parentNode, Node data) { + final List newChildren = []; + bool deleteClipNode = true; + + for (final Node child in parentNode.children) { + final _Result childResult = child.accept(this, parentNode); + newChildren.add(childResult.node); + if (!childResult.deleteClipNode) { + deleteClipNode = false; + } + } + + final ParentNode newParentNode = ParentNode(parentNode.attributes, + precalculatedTransform: parentNode.transform, children: newChildren); + + final _Result result = _Result(newParentNode); + + result.deleteClipNode = deleteClipNode; + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitMaskNode(MaskNode maskNode, Node data) { + final _Result result = _Result(maskNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitPathNode(PathNode pathNode, Node data) { + final _Result result = _Result(pathNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + final _Result childResult = maskNode.child.accept(this, maskNode); + final ResolvedMaskNode newMaskNode = ResolvedMaskNode( + child: childResult.node, + mask: maskNode.mask, + blendMode: maskNode.blendMode); + final _Result result = _Result(newMaskNode); + result.children.add(childResult.node); + result.childCount = 1; + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedClipNode(ResolvedClipNode clipNode, Node data) { + _Result result = _Result(clipNode); + + Path? singleClipPath; + if (clipNode.clips.length == 1) { + singleClipPath = clipNode.clips.single; + } + + if (singleClipPath != null) { + clipsToApply.add(singleClipPath); + final _Result childResult = clipNode.child.accept(this, clipNode); + clipsToApply.removeLast(); + + if (childResult.deleteClipNode) { + result = _Result(childResult.node); + } else { + final ResolvedClipNode newClipNode = + ResolvedClipNode(child: childResult.node, clips: clipNode.clips); + result = _Result(newClipNode); + } + } else { + final _Result childResult = clipNode.child.accept(this, clipNode); + final ResolvedClipNode newClipNode = + ResolvedClipNode(child: childResult.node, clips: clipNode.clips); + result = _Result(newClipNode); + } + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPath(ResolvedPathNode pathNode, Node data) { + _Result result = _Result(pathNode); + bool hasStrokeWidth = false; + bool deleteClipNode = true; + + if (pathNode.paint.stroke?.width != null) { + hasStrokeWidth = true; + result.deleteClipNode = false; + } + + if (clipsToApply.isNotEmpty && !hasStrokeWidth) { + ResolvedPathNode newPathNode = pathNode; + for (final Path clipPath in clipsToApply) { + final ResolvedPathNode intersection = applyClip(newPathNode, clipPath); + if (intersection.path.commands.isNotEmpty) { + newPathNode = intersection; + } else { + result = _Result(pathNode); + result.deleteClipNode = false; + deleteClipNode = false; + break; + } + } + result = _Result(newPathNode); + result.deleteClipNode = deleteClipNode; + } + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedText(ResolvedTextNode textNode, Node data) { + final _Result result = _Result(textNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedVerticesNode( + ResolvedVerticesNode verticesNode, Node data) { + final _Result result = _Result(verticesNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitSaveLayerNode(SaveLayerNode layerNode, Node data) { + final List newChildren = []; + for (final Node child in layerNode.children) { + final _Result childResult = child.accept(this, layerNode); + newChildren.add(childResult.node); + } + final SaveLayerNode newLayerNode = SaveLayerNode(layerNode.attributes, + paint: layerNode.paint, children: newChildren); + + final _Result result = _Result(newLayerNode); + result.children = newChildren; + result.childCount = newChildren.length; + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitViewportNode(ViewportNode viewportNode, void data) { + final List children = []; + for (final Node child in viewportNode.children) { + final _Result childNode = child.accept(this, viewportNode); + children.add(childNode.node); + } + + final ViewportNode node = ViewportNode( + viewportNode.attributes, + width: viewportNode.width, + height: viewportNode.height, + transform: viewportNode.transform, + children: children, + ); + + final _Result result = _Result(node); + result.children = children; + result.childCount = children.length; + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedImageNode( + ResolvedImageNode resolvedImageNode, Node data) { + final _Result result = _Result(resolvedImageNode); + result.deleteClipNode = false; + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPatternNode(ResolvedPatternNode patternNode, Node data) { + return _Result(patternNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + return _Result( + ResolvedTextPositionNode( + textPositionNode.textPosition, + [ + for (final Node child in textPositionNode.children) + child.accept(this, data).node + ], + ), + ); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/color_mapper.dart b/packages/vector_graphics_compiler/lib/src/svg/color_mapper.dart new file mode 100644 index 00000000000..983cda18528 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/color_mapper.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../paint.dart'; + +/// A class that transforms from one color to another during SVG parsing. +abstract class ColorMapper { + /// Returns a new color to use in place of [color] during SVG parsing. + /// + /// The SVG parser will call this method every time it parses a color + Color substitute( + String? id, + String elementName, + String attributeName, + Color color, + ); +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/colors.dart b/packages/vector_graphics_compiler/lib/src/svg/colors.dart new file mode 100644 index 00000000000..69d3f025f3c --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/colors.dart @@ -0,0 +1,159 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../paint.dart'; + +/// Named colors from the SVG standard. +/// +/// https://www.w3.org/TR/SVG11/types.html#ColorKeywords +const Map namedColors = { + 'aliceblue': Color.fromARGB(255, 240, 248, 255), + 'antiquewhite': Color.fromARGB(255, 250, 235, 215), + 'aqua': Color.fromARGB(255, 0, 255, 255), + 'aquamarine': Color.fromARGB(255, 127, 255, 212), + 'azure': Color.fromARGB(255, 240, 255, 255), + 'beige': Color.fromARGB(255, 245, 245, 220), + 'bisque': Color.fromARGB(255, 255, 228, 196), + 'black': Color.opaqueBlack, + 'blanchedalmond': Color.fromARGB(255, 255, 235, 205), + 'blue': Color.fromARGB(255, 0, 0, 255), + 'blueviolet': Color.fromARGB(255, 138, 43, 226), + 'brown': Color.fromARGB(255, 165, 42, 42), + 'burlywood': Color.fromARGB(255, 222, 184, 135), + 'cadetblue': Color.fromARGB(255, 95, 158, 160), + 'chartreuse': Color.fromARGB(255, 127, 255, 0), + 'chocolate': Color.fromARGB(255, 210, 105, 30), + 'coral': Color.fromARGB(255, 255, 127, 80), + 'cornflowerblue': Color.fromARGB(255, 100, 149, 237), + 'cornsilk': Color.fromARGB(255, 255, 248, 220), + 'crimson': Color.fromARGB(255, 220, 20, 60), + 'cyan': Color.fromARGB(255, 0, 255, 255), + 'darkblue': Color.fromARGB(255, 0, 0, 139), + 'darkcyan': Color.fromARGB(255, 0, 139, 139), + 'darkgoldenrod': Color.fromARGB(255, 184, 134, 11), + 'darkgray': Color.fromARGB(255, 169, 169, 169), + 'darkgreen': Color.fromARGB(255, 0, 100, 0), + 'darkgrey': Color.fromARGB(255, 169, 169, 169), + 'darkkhaki': Color.fromARGB(255, 189, 183, 107), + 'darkmagenta': Color.fromARGB(255, 139, 0, 139), + 'darkolivegreen': Color.fromARGB(255, 85, 107, 47), + 'darkorange': Color.fromARGB(255, 255, 140, 0), + 'darkorchid': Color.fromARGB(255, 153, 50, 204), + 'darkred': Color.fromARGB(255, 139, 0, 0), + 'darksalmon': Color.fromARGB(255, 233, 150, 122), + 'darkseagreen': Color.fromARGB(255, 143, 188, 143), + 'darkslateblue': Color.fromARGB(255, 72, 61, 139), + 'darkslategray': Color.fromARGB(255, 47, 79, 79), + 'darkslategrey': Color.fromARGB(255, 47, 79, 79), + 'darkturquoise': Color.fromARGB(255, 0, 206, 209), + 'darkviolet': Color.fromARGB(255, 148, 0, 211), + 'deeppink': Color.fromARGB(255, 255, 20, 147), + 'deepskyblue': Color.fromARGB(255, 0, 191, 255), + 'dimgray': Color.fromARGB(255, 105, 105, 105), + 'dimgrey': Color.fromARGB(255, 105, 105, 105), + 'dodgerblue': Color.fromARGB(255, 30, 144, 255), + 'firebrick': Color.fromARGB(255, 178, 34, 34), + 'floralwhite': Color.fromARGB(255, 255, 250, 240), + 'forestgreen': Color.fromARGB(255, 34, 139, 34), + 'fuchsia': Color.fromARGB(255, 255, 0, 255), + 'gainsboro': Color.fromARGB(255, 220, 220, 220), + 'ghostwhite': Color.fromARGB(255, 248, 248, 255), + 'gold': Color.fromARGB(255, 255, 215, 0), + 'goldenrod': Color.fromARGB(255, 218, 165, 32), + 'gray': Color.fromARGB(255, 128, 128, 128), + 'grey': Color.fromARGB(255, 128, 128, 128), + 'green': Color.fromARGB(255, 0, 128, 0), + 'greenyellow': Color.fromARGB(255, 173, 255, 47), + 'honeydew': Color.fromARGB(255, 240, 255, 240), + 'hotpink': Color.fromARGB(255, 255, 105, 180), + 'indianred': Color.fromARGB(255, 205, 92, 92), + 'indigo': Color.fromARGB(255, 75, 0, 130), + 'ivory': Color.fromARGB(255, 255, 255, 240), + 'khaki': Color.fromARGB(255, 240, 230, 140), + 'lavender': Color.fromARGB(255, 230, 230, 250), + 'lavenderblush': Color.fromARGB(255, 255, 240, 245), + 'lawngreen': Color.fromARGB(255, 124, 252, 0), + 'lemonchiffon': Color.fromARGB(255, 255, 250, 205), + 'lightblue': Color.fromARGB(255, 173, 216, 230), + 'lightcoral': Color.fromARGB(255, 240, 128, 128), + 'lightcyan': Color.fromARGB(255, 224, 255, 255), + 'lightgoldenrodyellow': Color.fromARGB(255, 250, 250, 210), + 'lightgray': Color.fromARGB(255, 211, 211, 211), + 'lightgreen': Color.fromARGB(255, 144, 238, 144), + 'lightgrey': Color.fromARGB(255, 211, 211, 211), + 'lightpink': Color.fromARGB(255, 255, 182, 193), + 'lightsalmon': Color.fromARGB(255, 255, 160, 122), + 'lightseagreen': Color.fromARGB(255, 32, 178, 170), + 'lightskyblue': Color.fromARGB(255, 135, 206, 250), + 'lightslategray': Color.fromARGB(255, 119, 136, 153), + 'lightslategrey': Color.fromARGB(255, 119, 136, 153), + 'lightsteelblue': Color.fromARGB(255, 176, 196, 222), + 'lightyellow': Color.fromARGB(255, 255, 255, 224), + 'lime': Color.fromARGB(255, 0, 255, 0), + 'limegreen': Color.fromARGB(255, 50, 205, 50), + 'linen': Color.fromARGB(255, 250, 240, 230), + 'magenta': Color.fromARGB(255, 255, 0, 255), + 'maroon': Color.fromARGB(255, 128, 0, 0), + 'mediumaquamarine': Color.fromARGB(255, 102, 205, 170), + 'mediumblue': Color.fromARGB(255, 0, 0, 205), + 'mediumorchid': Color.fromARGB(255, 186, 85, 211), + 'mediumpurple': Color.fromARGB(255, 147, 112, 219), + 'mediumseagreen': Color.fromARGB(255, 60, 179, 113), + 'mediumslateblue': Color.fromARGB(255, 123, 104, 238), + 'mediumspringgreen': Color.fromARGB(255, 0, 250, 154), + 'mediumturquoise': Color.fromARGB(255, 72, 209, 204), + 'mediumvioletred': Color.fromARGB(255, 199, 21, 133), + 'midnightblue': Color.fromARGB(255, 25, 25, 112), + 'mintcream': Color.fromARGB(255, 245, 255, 250), + 'mistyrose': Color.fromARGB(255, 255, 228, 225), + 'moccasin': Color.fromARGB(255, 255, 228, 181), + 'navajowhite': Color.fromARGB(255, 255, 222, 173), + 'navy': Color.fromARGB(255, 0, 0, 128), + 'oldlace': Color.fromARGB(255, 253, 245, 230), + 'olive': Color.fromARGB(255, 128, 128, 0), + 'olivedrab': Color.fromARGB(255, 107, 142, 35), + 'orange': Color.fromARGB(255, 255, 165, 0), + 'orangered': Color.fromARGB(255, 255, 69, 0), + 'orchid': Color.fromARGB(255, 218, 112, 214), + 'palegoldenrod': Color.fromARGB(255, 238, 232, 170), + 'palegreen': Color.fromARGB(255, 152, 251, 152), + 'paleturquoise': Color.fromARGB(255, 175, 238, 238), + 'palevioletred': Color.fromARGB(255, 219, 112, 147), + 'papayawhip': Color.fromARGB(255, 255, 239, 213), + 'peachpuff': Color.fromARGB(255, 255, 218, 185), + 'peru': Color.fromARGB(255, 205, 133, 63), + 'pink': Color.fromARGB(255, 255, 192, 203), + 'plum': Color.fromARGB(255, 221, 160, 221), + 'powderblue': Color.fromARGB(255, 176, 224, 230), + 'purple': Color.fromARGB(255, 128, 0, 128), + 'red': Color.fromARGB(255, 255, 0, 0), + 'rosybrown': Color.fromARGB(255, 188, 143, 143), + 'royalblue': Color.fromARGB(255, 65, 105, 225), + 'saddlebrown': Color.fromARGB(255, 139, 69, 19), + 'salmon': Color.fromARGB(255, 250, 128, 114), + 'sandybrown': Color.fromARGB(255, 244, 164, 96), + 'seagreen': Color.fromARGB(255, 46, 139, 87), + 'seashell': Color.fromARGB(255, 255, 245, 238), + 'sienna': Color.fromARGB(255, 160, 82, 45), + 'silver': Color.fromARGB(255, 192, 192, 192), + 'skyblue': Color.fromARGB(255, 135, 206, 235), + 'slateblue': Color.fromARGB(255, 106, 90, 205), + 'slategray': Color.fromARGB(255, 112, 128, 144), + 'slategrey': Color.fromARGB(255, 112, 128, 144), + 'snow': Color.fromARGB(255, 255, 250, 250), + 'springgreen': Color.fromARGB(255, 0, 255, 127), + 'steelblue': Color.fromARGB(255, 70, 130, 180), + 'tan': Color.fromARGB(255, 210, 180, 140), + 'teal': Color.fromARGB(255, 0, 128, 128), + 'thistle': Color.fromARGB(255, 216, 191, 216), + 'tomato': Color.fromARGB(255, 255, 99, 71), + 'transparent': Color.fromARGB(0, 255, 255, 255), + 'turquoise': Color.fromARGB(255, 64, 224, 208), + 'violet': Color.fromARGB(255, 238, 130, 238), + 'wheat': Color.fromARGB(255, 245, 222, 179), + 'white': Color.fromARGB(255, 255, 255, 255), + 'whitesmoke': Color.fromARGB(255, 245, 245, 245), + 'yellow': Color.fromARGB(255, 255, 255, 0), + 'yellowgreen': Color.fromARGB(255, 154, 205, 50), +}; diff --git a/packages/vector_graphics_compiler/lib/src/svg/masking_optimizer.dart b/packages/vector_graphics_compiler/lib/src/svg/masking_optimizer.dart new file mode 100644 index 00000000000..ca24c29ca58 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/masking_optimizer.dart @@ -0,0 +1,359 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import '../../vector_graphics_compiler.dart'; +import 'node.dart'; +import 'path_ops.dart' as path_ops; +import 'visitor.dart'; + +class _Result { + _Result(this.node, {this.deleteMaskNode = true}); + + final Node node; + final List children = []; + Node parent = Node.empty; + final bool deleteMaskNode; +} + +/// Converts a vector_graphics PathFillType to a path_ops FillType. +path_ops.FillType toPathOpsFillTyle(PathFillType fill) { + switch (fill) { + case PathFillType.evenOdd: + return path_ops.FillType.evenOdd; + + case PathFillType.nonZero: + return path_ops.FillType.nonZero; + } +} + +/// Converts a path_ops FillType to a vector_graphics PathFillType +PathFillType toVectorGraphicsFillType(path_ops.FillType fill) { + switch (fill) { + case path_ops.FillType.evenOdd: + return PathFillType.evenOdd; + + case path_ops.FillType.nonZero: + return PathFillType.nonZero; + } +} + +/// Converts vector_graphics Path to path_ops Path. +path_ops.Path toPathOpsPath(Path path) { + final path_ops.Path newPath = path_ops.Path(toPathOpsFillTyle(path.fillType)); + + for (final PathCommand command in path.commands) { + switch (command.type) { + case PathCommandType.line: + final LineToCommand lineToCommand = command as LineToCommand; + newPath.lineTo(lineToCommand.x, lineToCommand.y); + case PathCommandType.cubic: + final CubicToCommand cubicToCommand = command as CubicToCommand; + newPath.cubicTo(cubicToCommand.x1, cubicToCommand.y1, cubicToCommand.x2, + cubicToCommand.y2, cubicToCommand.x3, cubicToCommand.y3); + case PathCommandType.move: + final MoveToCommand moveToCommand = command as MoveToCommand; + newPath.moveTo(moveToCommand.x, moveToCommand.y); + case PathCommandType.close: + newPath.close(); + } + } + + return newPath; +} + +/// Converts path_ops Path to VectorGraphicsPath. +Path toVectorGraphicsPath(path_ops.Path path) { + final List newCommands = []; + + int index = 0; + final Float32List points = path.points; + for (final path_ops.PathVerb verb in path.verbs.toList()) { + switch (verb) { + case path_ops.PathVerb.moveTo: + newCommands.add(MoveToCommand(points[index++], points[index++])); + case path_ops.PathVerb.lineTo: + newCommands.add(LineToCommand(points[index++], points[index++])); + case path_ops.PathVerb.quadTo: + final double cpX = points[index++]; + final double cpY = points[index++]; + newCommands.add(CubicToCommand( + cpX, + cpY, + cpX, + cpY, + points[index++], + points[index++], + )); + case path_ops.PathVerb.cubicTo: + newCommands.add(CubicToCommand( + points[index++], + points[index++], + points[index++], + points[index++], + points[index++], + points[index++], + )); + case path_ops.PathVerb.close: + newCommands.add(const CloseCommand()); + } + } + + final Path newPath = Path( + commands: newCommands, fillType: toVectorGraphicsFillType(path.fillType)); + + return newPath; +} + +/// Gets the single child recursively, +/// returns null if there are 0 children or more than 1. +ResolvedPathNode? getSingleChild(Node node) { + if (node is ResolvedPathNode) { + return node; + } else if (node is ParentNode && node.children.length == 1) { + return getSingleChild(node.children.first); + } + return null; +} + +/// Simplifies masking operations into PathNodes. +/// Note this will not optimize cases where 'stroke-width' is set, +/// there are multiple path nodes in a mask or cases where +/// the intersection of the mask and the path results in +/// Path.commands being empty. +class MaskingOptimizer extends Visitor<_Result, Node> + with ErrorOnUnResolvedNode<_Result, Node> { + /// List of masks to add. + final List masksToApply = []; + + /// Applies visitor to given node. + Node apply(Node node) { + final Node newNode = node.accept(this, null).node; + return newNode; + } + + /// Applies mask to a path node, and returns resulting path node. + ResolvedPathNode applyMask( + ResolvedPathNode pathNode, ResolvedPathNode maskPathNode) { + final path_ops.Path maskPathOpsPath = toPathOpsPath(maskPathNode.path); + final path_ops.Path pathPathOpsPath = toPathOpsPath(pathNode.path); + final path_ops.Path intersection = + pathPathOpsPath.applyOp(maskPathOpsPath, path_ops.PathOp.intersect); + final Path newPath = toVectorGraphicsPath(intersection); + final ResolvedPathNode newPathNode = ResolvedPathNode( + paint: pathNode.paint, bounds: maskPathNode.bounds, path: newPath); + + maskPathOpsPath.dispose(); + pathPathOpsPath.dispose(); + intersection.dispose(); + + return newPathNode; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitEmptyNode(Node node, void data) { + final _Result result = _Result(node); + return result; + } + + /// Visits applies optimizer to all children of ResolvedMaskNode. + // ignore: library_private_types_in_public_api + _Result visitChildren(Node node, _Result data) { + if (node is ResolvedMaskNode) { + data = node.child.accept(this, data); + } + return data; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitParentNode(ParentNode parentNode, Node data) { + final List newChildren = []; + + for (final Node child in parentNode.children) { + final _Result childResult = child.accept(this, parentNode); + newChildren.add(childResult.node); + if (!childResult.deleteMaskNode) { + return _Result(parentNode, deleteMaskNode: false); + } + } + + final ParentNode newParentNode = ParentNode(parentNode.attributes, + precalculatedTransform: parentNode.transform, children: newChildren); + + final _Result result = _Result(newParentNode); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitMaskNode(MaskNode maskNode, Node data) { + final _Result result = _Result(maskNode); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitPathNode(PathNode pathNode, Node data) { + final _Result result = _Result(pathNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + _Result result = _Result(maskNode); + final ResolvedPathNode? singleMaskPathNode = getSingleChild(maskNode.mask); + + if (singleMaskPathNode != null) { + masksToApply.add(singleMaskPathNode); + final _Result childResult = maskNode.child.accept(this, maskNode); + masksToApply.removeLast(); + + if (childResult.deleteMaskNode) { + result = _Result(childResult.node); + } else { + final ResolvedMaskNode newMaskNode = ResolvedMaskNode( + child: childResult.node, + mask: maskNode.mask, + blendMode: maskNode.blendMode); + result = _Result(newMaskNode); + } + } else { + final _Result childResult = maskNode.child.accept(this, maskNode); + final ResolvedMaskNode newMaskNode = ResolvedMaskNode( + child: childResult.node, + mask: maskNode.mask, + blendMode: maskNode.blendMode); + result = _Result(newMaskNode); + } + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedClipNode(ResolvedClipNode clipNode, Node data) { + final _Result childResult = clipNode.child.accept(this, clipNode); + final ResolvedClipNode newClipNode = + ResolvedClipNode(clips: clipNode.clips, child: childResult.node); + final _Result result = _Result(newClipNode); + result.children.add(childResult.node); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPath(ResolvedPathNode pathNode, Node data) { + _Result result = _Result(pathNode); + + if (pathNode.paint.stroke?.width != null) { + return _Result(pathNode, deleteMaskNode: false); + } + + if (masksToApply.isNotEmpty) { + ResolvedPathNode newPathNode = pathNode; + for (final ResolvedPathNode maskPathNode in masksToApply) { + final ResolvedPathNode intersection = + applyMask(newPathNode, maskPathNode); + if (intersection.path.commands.isNotEmpty) { + newPathNode = intersection; + } else { + return _Result(pathNode, deleteMaskNode: false); + } + } + result = _Result(newPathNode); + } + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedText(ResolvedTextNode textNode, Node data) { + final _Result result = _Result(textNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedVerticesNode( + ResolvedVerticesNode verticesNode, Node data) { + final _Result result = _Result(verticesNode); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitSaveLayerNode(SaveLayerNode layerNode, Node data) { + final List newChildren = []; + for (final Node child in layerNode.children) { + final _Result childResult = child.accept(this, layerNode); + newChildren.add(childResult.node); + } + final SaveLayerNode newLayerNode = SaveLayerNode(layerNode.attributes, + paint: layerNode.paint, children: newChildren); + + final _Result result = _Result(newLayerNode); + result.children.addAll(newChildren); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitViewportNode(ViewportNode viewportNode, void data) { + final List children = []; + for (final Node child in viewportNode.children) { + final _Result childNode = child.accept(this, viewportNode); + children.add(childNode.node); + } + + final ViewportNode node = ViewportNode( + viewportNode.attributes, + width: viewportNode.width, + height: viewportNode.height, + transform: viewportNode.transform, + children: children, + ); + + final _Result result = _Result(node); + result.children.addAll(children); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedImageNode( + ResolvedImageNode resolvedImageNode, Node data) { + final _Result result = _Result(resolvedImageNode, deleteMaskNode: false); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPatternNode(ResolvedPatternNode patternNode, Node data) { + return _Result(patternNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + return _Result( + ResolvedTextPositionNode( + textPositionNode.textPosition, + [ + for (final Node child in textPositionNode.children) + child.accept(this, data).node + ], + ), + ); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/node.dart b/packages/vector_graphics_compiler/lib/src/svg/node.dart new file mode 100644 index 00000000000..91638ec270a --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/node.dart @@ -0,0 +1,651 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../geometry/basic_types.dart'; +import '../geometry/matrix.dart'; +import '../geometry/path.dart'; +import '../image/image_info.dart'; +import '../paint.dart'; +import 'parser.dart' show SvgAttributes; +import 'visitor.dart'; + +/// Signature of a method that resolves a string identifier to an object. +/// +/// Used by [ClipNode] and [MaskNode] to defer resolution of clips and masks. +typedef Resolver = T Function(String id); + +/// A node in a tree of graphics operations. +/// +/// Nodes describe painting attributes, clips, transformations, paths, and +/// vertices to draw in depth-first order. +abstract class Node { + /// Allows subclasses to be const. + const Node(); + + /// A node with no properties or operations, used for replacing `null` values + /// in the tree or nodes that cannot be resolved correctly. + static const Node empty = _EmptyNode(); + + /// Subclasses that have additional transformation information will + /// concatenate their transform to the supplied `currentTransform`. + AffineMatrix concatTransform(AffineMatrix currentTransform) { + return currentTransform; + } + + /// Calls `visitor` for each child node of this parent group. + /// + /// This call does not recursively call `visitChildren`. Callers must decide + /// whether to do BFS or DFS by calling `visitChildren` if the visited child + /// is a [ParentNode]. + void visitChildren(NodeCallback visitor); + + /// Accept a [Visitor] implementation. + S accept(Visitor visitor, V data); + + /// Creates a new compatible new node with attribute inheritence. + /// + /// If [replace] is true, treats the application of attributes as if this node + /// is the parent. Otherwise, treats the application of the attributes as if + /// the [newAttributes] are from the parent. + /// + /// By default, returns this. + Node applyAttributes(SvgAttributes newAttributes, {bool replace = false}) => + this; +} + +class _EmptyNode extends Node { + const _EmptyNode(); + + @override + S accept(Visitor visitor, V data) { + return visitor.visitEmptyNode(this, data); + } + + @override + void visitChildren(NodeCallback visitor) {} +} + +/// A node that contains a transform operation in the tree of graphics +/// operations. +abstract class TransformableNode extends Node { + /// Constructs a new tree node with [transform]. + TransformableNode(this.transform); + + /// The descendant child's transform + final AffineMatrix transform; + + @override + @mustCallSuper + AffineMatrix concatTransform(AffineMatrix currentTransform) { + if (transform == AffineMatrix.identity) { + return currentTransform; + } + return currentTransform.multiplied(transform); + } +} + +/// A node that has attributes in the tree of graphics operations. +abstract class AttributedNode extends TransformableNode { + /// Constructs a new tree node with [attributes]. + AttributedNode(this.attributes, {AffineMatrix? precalculatedTransform}) + : super(precalculatedTransform ?? attributes.transform); + + /// A collection of painting attributes. + /// + /// Painting attributes inherit down the tree. + final SvgAttributes attributes; +} + +/// A graphics node describing a viewport area, which has a [width] and [height] +/// for the viewable portion it describes. +/// +/// A viewport node is effectively a [ParentNode] with a width and height to +/// describe child coordinate space. It is typically used as the root of a tree, +/// but may also appear as a subtree root. +class ViewportNode extends ParentNode { + /// Creates a new viewport node. + /// + /// See [ViewportNode]. + ViewportNode( + super.attributes, { + required this.width, + required this.height, + required AffineMatrix transform, + super.children, + }) : super( + precalculatedTransform: transform, + ); + + /// The width of the viewport in pixels. + final double width; + + /// The height of the viewport in pixels. + final double height; + + /// The viewport rect described by [width] and [height]. + Rect get viewport => Rect.fromLTWH(0, 0, width, height); + + @override + S accept(Visitor visitor, V data) { + return visitor.visitViewportNode(this, data); + } +} + +/// The signature for a visitor callback to [ParentNode.visitChildren]. +typedef NodeCallback = void Function(Node child); + +/// A node that contains children, transformed by [transform]. +class ParentNode extends AttributedNode { + /// Creates a new [ParentNode]. + ParentNode( + super.attributes, { + super.precalculatedTransform, + List? children, + }) : _children = children ?? []; + + /// The child nodes of this node. + final List _children; + + /// The child nodes for the given parent node. + Iterable get children => _children; + + @override + void visitChildren(NodeCallback visitor) { + _children.forEach(visitor); + } + + /// Adds a child to this parent node. + /// + /// If `clips` is empty, the child is directly appended. Otherwise, a + /// [ClipNode] is inserted. + void addChild( + AttributedNode child, { + String? clipId, + String? maskId, + String? patternId, + required Resolver> clipResolver, + required Resolver maskResolver, + required Resolver patternResolver, + }) { + Node wrappedChild = child; + if (clipId != null) { + wrappedChild = ClipNode( + resolver: clipResolver, + clipId: clipId, + child: wrappedChild, + transform: child.attributes.transform, + ); + } + if (maskId != null) { + wrappedChild = MaskNode( + resolver: maskResolver, + maskId: maskId, + child: wrappedChild, + blendMode: child.attributes.blendMode, + transform: child.attributes.transform, + ); + } + if (patternId != null) { + wrappedChild = PatternNode( + resolver: patternResolver, + patternId: patternId, + child: wrappedChild, + transform: child.attributes.transform, + ); + } + _children.add(wrappedChild); + } + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + return ParentNode( + attributes.applyParent(newAttributes), + precalculatedTransform: transform, + ).._children.addAll(_children); + } + + /// Create the paint required to draw a save layer, or `null` if none is + /// required. + Paint? createLayerPaint() { + final double? fillOpacity = attributes.fill?.opacity; + final bool needsLayer = (attributes.blendMode != null) || + (fillOpacity != null && fillOpacity != 1.0 && fillOpacity != 0.0); + + if (needsLayer) { + return Paint( + blendMode: attributes.blendMode, + fill: attributes.fill?.toFill(Rect.largest, transform) ?? + Fill( + color: Color.opaqueBlack.withOpacity(fillOpacity ?? 1.0), + ), + ); + } + return null; + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitParentNode(this, data); + } +} + +/// A node describing an update to the [TextPosition], including any applicable +/// transformation matrix. +class TextPositionNode extends ParentNode { + /// See [TextPositionNode]. + TextPositionNode(super.attributes, {required this.reset}); + + /// Whether this node represents a reset of the current text position or not. + final bool reset; + + /// Computes a [TextPosition] to encode for this node. + TextPosition computeTextPosition(Rect bounds, AffineMatrix transform) { + final AffineMatrix computedTransform = concatTransform(transform); + + double? x = attributes.x?.calculate(bounds.width); + double? y = attributes.y?.calculate(bounds.height); + double? dx = attributes.dx?.calculate(bounds.width); + double? dy = attributes.dy?.calculate(bounds.height); + + final bool hasXY = x != null && y != null; + final bool hasDxDy = dx != null && dy != null; + final bool consumeTransform = computedTransform == AffineMatrix.identity || + (computedTransform.encodableInRect && (hasXY || hasDxDy)); + + if (hasXY) { + final Point baseline = consumeTransform + ? computedTransform.transformPoint(Point(x, y)) + : Point(x, y); + x = baseline.x; + y = baseline.y; + } + + if (hasDxDy) { + final Point baseline = consumeTransform + ? computedTransform.transformPoint(Point(dx, dy)) + : Point(dx, dy); + dx = baseline.x; + dy = baseline.y; + } + + return TextPosition( + x: x, + y: y, + dx: dx, + dy: dy, + reset: reset, + transform: consumeTransform ? null : computedTransform, + ); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitTextPositionNode(this, data); + } + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + return TextPositionNode(attributes.applyParent(newAttributes), reset: reset) + .._children.addAll(_children); + } +} + +/// A parent node that applies a save layer to its children. +class SaveLayerNode extends ParentNode { + /// Create a new [SaveLayerNode] + SaveLayerNode( + super.attributes, { + required this.paint, + super.children, + }) : super(precalculatedTransform: AffineMatrix.identity); + + /// The paint to apply to the saved layer. + final Paint paint; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitSaveLayerNode(this, data); + } +} + +/// A parent node that applies a clip to its children. +class ClipNode extends TransformableNode { + /// Creates a new clip node that applies clip paths to [child]. + ClipNode({ + required this.resolver, + required this.child, + required this.clipId, + required AffineMatrix transform, + }) : super(transform); + + /// Called by visitors to resolve [clipId] to a list of paths. + final Resolver> resolver; + + /// The clips to apply to the child node. + /// + /// Normally, there will only be one clip to apply. However, if multiple paths + /// with differeing [PathFillType]s are used, multiple clips must be + /// specified. + final String clipId; + + /// The child to clip. + final Node child; + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitClipNode(this, data); + } + + @override + Node applyAttributes(SvgAttributes newAttributes, {bool replace = false}) { + return ClipNode( + resolver: resolver, + clipId: clipId, + transform: transform, + child: child.applyAttributes(newAttributes, replace: replace), + ); + } +} + +/// A parent node that applies a mask to its child. +class MaskNode extends TransformableNode { + /// Creates a new mask node that applies [mask] to [child] using [blendMode]. + MaskNode({ + required this.child, + required this.maskId, + this.blendMode, + required this.resolver, + required AffineMatrix transform, + }) : super(transform); + + /// The mask to apply. + final String maskId; + + /// The child to mask. + final Node child; + + /// The blend mode to apply when saving a layer for the mask, if any. + final BlendMode? blendMode; + + /// Called by visitors to resolve [maskId] to an [AttributedNode]. + final Resolver resolver; + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitMaskNode(this, data); + } + + @override + Node applyAttributes(SvgAttributes newAttributes, {bool replace = false}) { + return MaskNode( + resolver: resolver, + maskId: maskId, + blendMode: blendMode, + transform: transform, + child: child.applyAttributes(newAttributes, replace: replace), + ); + } +} + +/// A leaf node in the graphics tree. +/// +/// Leaf nodes get added with all paint and transform accumulations from their +/// parents applied. +class PathNode extends AttributedNode { + /// Creates a new leaf node for the graphics tree with the specified [path] + /// and attributes + PathNode(this.path, super.attributes); + + /// The description of the geometry this leaf node draws. + final Path path; + + /// Compute the paint used by this Path. + Paint? computePaint(Rect bounds, AffineMatrix transform) { + final Stroke? stroke = attributes.stroke?.toStroke(bounds, transform); + final Fill? fill = attributes.fill?.toFill( + bounds, + transform, + defaultColor: Color.opaqueBlack, + ); + if (fill == null && stroke == null) { + return null; + } + return Paint( + blendMode: attributes.blendMode, + fill: fill, + stroke: stroke, + ); + } + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + return PathNode( + path, + replace + ? newAttributes.applyParent(attributes, transformOverride: transform) + : attributes.applyParent(newAttributes), + ); + } + + @override + void visitChildren(NodeCallback visitor) {} + + @override + S accept(Visitor visitor, V data) { + return visitor.visitPathNode(this, data); + } +} + +/// A node that refers to another node, and supplies a [resolver] for visitors +/// to materialize the referenced node into the tree. +class DeferredNode extends AttributedNode { + /// Creates a new deferred node with [attributes] that will call [resolver] + /// with [refId] when visited. + DeferredNode( + super.attributes, { + required this.refId, + required this.resolver, + }); + + /// The reference id to pass to [resolver]. + final String refId; + + /// The callback that materializes an [AttributedNode] for [refId] when + /// visited. + final Resolver resolver; + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + return DeferredNode( + replace + ? newAttributes.applyParent(attributes, transformOverride: transform) + : attributes.applyParent(newAttributes), + refId: refId, + resolver: resolver, + ); + } + + @override + void visitChildren(NodeCallback visitor) {} + + @override + S accept(Visitor visitor, V data) { + return visitor.visitDeferredNode(this, data); + } +} + +/// A leaf node in the tree that represents inline text. +/// +/// Leaf nodes get added with all paint and transform accumulations from their +/// parents applied. +class TextNode extends AttributedNode { + /// Create a new [TextNode] with the given [text]. + TextNode( + this.text, + super.attributes, + ); + + /// The text this node contains. + final String text; + + /// Compute the [Paint] that this text node uses. + Paint? computePaint(Rect bounds, AffineMatrix transform) { + final Fill? fill = attributes.fill + ?.toFill(bounds, transform, defaultColor: Color.opaqueBlack); + final Stroke? stroke = attributes.stroke?.toStroke(bounds, transform); + if (fill == null && stroke == null) { + return null; + } + return Paint( + blendMode: attributes.blendMode, + fill: fill, + stroke: stroke, + ); + } + + /// Compute the [TextConfig] that this text node uses. + TextConfig computeTextConfig(Rect bounds, AffineMatrix transform) { + // Don't concat the transform since it's repeated by the parent group + // the way the parser is set up. + return TextConfig( + text, + attributes.textAnchorMultiplier ?? 0, + attributes.fontFamily, + attributes.fontWeight ?? normalFontWeight, + attributes.fontSize ?? 16, // default in many browsers + attributes.textDecoration ?? TextDecoration.none, + attributes.textDecorationStyle ?? TextDecorationStyle.solid, + attributes.textDecorationColor ?? Color.opaqueBlack, + ); + } + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + final SvgAttributes resolvedAttributes = replace + ? newAttributes.applyParent(attributes, transformOverride: transform) + : attributes.applyParent(newAttributes); + return TextNode( + text, + resolvedAttributes, + ); + } + + @override + void visitChildren(NodeCallback visitor) {} + + @override + S accept(Visitor visitor, V data) { + return visitor.visitTextNode(this, data); + } +} + +/// A leaf node in the tree that represents an image. +/// +/// Leaf nodes get added with all paint and transform accumulations from their +/// parents applied. +class ImageNode extends AttributedNode { + /// Create a new [ImageNode] with the given [text]. + ImageNode( + this.data, + this.format, + super.attributes, + ); + + /// The image data this node contains. + final Uint8List data; + + /// The format of [data]. + final ImageFormat format; + + @override + AttributedNode applyAttributes( + SvgAttributes newAttributes, { + bool replace = false, + }) { + return ImageNode( + data, + format, + replace + ? newAttributes.applyParent(attributes, transformOverride: transform) + : attributes.applyParent(newAttributes), + ); + } + + @override + void visitChildren(NodeCallback visitor) {} + + @override + S accept(Visitor visitor, V data) { + return visitor.visitImageNode(this, data); + } +} + +/// A leaf node in the tree that reprents an patterned-node. +class PatternNode extends TransformableNode { + /// Creates a new pattern node that aaples [pattern] to [child]. + PatternNode({ + required this.child, + required this.patternId, + required this.resolver, + required AffineMatrix transform, + }) : super(transform); + + /// A unique identifier for this pattern. + final String patternId; + + /// The child(ren) to apply the pattern to. + final Node child; + + /// Called by visitors to resolve [patternId] to an [AttributedNode]. + final Resolver resolver; + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitPatternNode(this, data); + } + + @override + Node applyAttributes(SvgAttributes newAttributes, {bool replace = false}) { + return PatternNode( + resolver: resolver, + patternId: patternId, + transform: transform, + child: child.applyAttributes(newAttributes, replace: replace), + ); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/numbers.dart b/packages/vector_graphics_compiler/lib/src/svg/numbers.dart new file mode 100644 index 00000000000..fbe7563bf86 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/numbers.dart @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'theme.dart'; + +/// Parses a [rawDouble] `String` to a `double`. +/// +/// The [rawDouble] might include a unit (`px`, `em` or `ex`) +/// which is stripped off when parsed to a `double`. +/// +/// Passing `null` will return `null`. +double? parseDouble(String? rawDouble, {bool tryParse = false}) { + assert(tryParse != null); // ignore: unnecessary_null_comparison + if (rawDouble == null) { + return null; + } + + rawDouble = rawDouble + .replaceFirst('rem', '') + .replaceFirst('em', '') + .replaceFirst('ex', '') + .replaceFirst('px', '') + .replaceFirst('pt', '') + .trim(); + + if (tryParse) { + return double.tryParse(rawDouble); + } + return double.parse(rawDouble); +} + +/// Convert [degrees] to radians. +double radians(double degrees) => degrees * math.pi / 180; + +/// The number of pixels per CSS inch. +const int kCssPixelsPerInch = 96; + +/// The number of points per CSS inch. +const int kCssPointsPerInch = 72; + +/// The multiplicand to convert from CSS points to pixels. +const double kPointsToPixelFactor = kCssPixelsPerInch / kCssPointsPerInch; + +/// Parses a `rawDouble` `String` to a `double` +/// taking into account absolute and relative units +/// (`px`, `em` or `ex`). +/// +/// Passing an `em` value will calculate the result +/// relative to the provided [fontSize]: +/// 1 em = 1 * `fontSize`. +/// +/// Passing an `ex` value will calculate the result +/// relative to the provided [xHeight]: +/// 1 ex = 1 * `xHeight`. +/// +/// The `rawDouble` might include a unit which is +/// stripped off when parsed to a `double`. +/// +/// Passing `null` will return `null`. +double? parseDoubleWithUnits( + String? rawDouble, { + bool tryParse = false, + required SvgTheme theme, +}) { + double unit = 1.0; + + // 1 rem unit is equal to the root font size. + // 1 em unit is equal to the current font size. + // 1 ex unit is equal to the current x-height. + if (rawDouble?.contains('pt') ?? false) { + unit = kPointsToPixelFactor; + } else if (rawDouble?.contains('rem') ?? false) { + unit = theme.fontSize; + } else if (rawDouble?.contains('em') ?? false) { + unit = theme.fontSize; + } else if (rawDouble?.contains('ex') ?? false) { + unit = theme.xHeight; + } + final double? value = parseDouble( + rawDouble, + tryParse: tryParse, + ); + + return value != null ? value * unit : null; +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/overdraw_optimizer.dart b/packages/vector_graphics_compiler/lib/src/svg/overdraw_optimizer.dart new file mode 100644 index 00000000000..7856c8c5754 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/overdraw_optimizer.dart @@ -0,0 +1,371 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../vector_graphics_compiler.dart'; +import 'masking_optimizer.dart'; +import 'node.dart'; +import 'parser.dart'; +import 'path_ops.dart' as path_ops; +import 'visitor.dart'; + +class _Result { + _Result(this.node); + + final Node node; + final List children = []; + Node parent = Node.empty; +} + +/// Removes unnecessary overlappping. +class OverdrawOptimizer extends Visitor<_Result, Node> + with ErrorOnUnResolvedNode<_Result, Node> { + /// Applies visitor to given node. + Node apply(Node node) { + final Node newNode = node.accept(this, null).node; + return newNode; + } + + /// Removes overlap between top and bottom path from bottom. + ResolvedPathNode removeOverlap( + ResolvedPathNode bottomPathNode, ResolvedPathNode topPathNode) { + final path_ops.Path topPathOpsPath = toPathOpsPath(topPathNode.path); + final path_ops.Path bottomPathOpsPath = toPathOpsPath(bottomPathNode.path); + final path_ops.Path intersection = + bottomPathOpsPath.applyOp(topPathOpsPath, path_ops.PathOp.intersect); + final path_ops.Path newBottomPath = + bottomPathOpsPath.applyOp(intersection, path_ops.PathOp.difference); + final Path newPath = toVectorGraphicsPath(newBottomPath); + final ResolvedPathNode newPathNode = ResolvedPathNode( + paint: bottomPathNode.paint, + bounds: bottomPathNode.bounds, + path: newPath); + + bottomPathOpsPath.dispose(); + topPathOpsPath.dispose(); + intersection.dispose(); + newBottomPath.dispose(); + + return newPathNode; + } + + /// Calculates the resulting [Color] when two semi-transparent + /// colors are stacked on top of eachother. + Color calculateOverlapColor(Color bottomColor, Color topColor) { + final double a0 = topColor.a / 255; + final double a1 = bottomColor.a / 255; + final int r0 = topColor.r; + final int b0 = topColor.b; + final int g0 = topColor.g; + final int r1 = bottomColor.r; + final int b1 = bottomColor.b; + final int g1 = bottomColor.g; + + final double a = (1 - a0) * a1 + a0; + final double r = ((1 - a0) * a1 * r1 + a0 * r0) / a; + final double g = ((1 - a0) * a1 * g1 + a0 * g0) / a; + final double b = ((1 - a0) * a1 * b1 + a0 * b0) / a; + + final Color overlapColor = + Color.fromARGB((a * 255).round(), r.round(), g.round(), b.round()); + return overlapColor; + } + + /// Resolves overlapping between top and bottom path on + /// nodes where opacity is not 1 or null. + List resolveOpacityOverlap( + ResolvedPathNode bottomPathNode, ResolvedPathNode topPathNode) { + final Color? bottomColor = bottomPathNode.paint.fill?.color; + final Color? topColor = topPathNode.paint.fill?.color; + if (bottomColor != null && topColor != null) { + final Color overlapColor = calculateOverlapColor(bottomColor, topColor); + final path_ops.Path topPathOpsPath = toPathOpsPath(topPathNode.path); + final path_ops.Path bottomPathOpsPath = + toPathOpsPath(bottomPathNode.path); + final path_ops.Path intersection = + bottomPathOpsPath.applyOp(topPathOpsPath, path_ops.PathOp.intersect); + final path_ops.Path newBottomPath = + bottomPathOpsPath.applyOp(intersection, path_ops.PathOp.difference); + final path_ops.Path newTopPath = + topPathOpsPath.applyOp(intersection, path_ops.PathOp.difference); + + final Path newBottomVGPath = toVectorGraphicsPath(newBottomPath); + final Path newTopVGPath = toVectorGraphicsPath(newTopPath); + final Path newOverlapVGPath = toVectorGraphicsPath(intersection); + + final ResolvedPathNode newBottomPathNode = ResolvedPathNode( + paint: bottomPathNode.paint, + bounds: bottomPathNode.bounds, + path: newBottomVGPath); + final ResolvedPathNode newTopPathNode = ResolvedPathNode( + paint: topPathNode.paint, + bounds: bottomPathNode.bounds, + path: newTopVGPath); + final ResolvedPathNode newOverlapPathNode = ResolvedPathNode( + paint: Paint( + blendMode: bottomPathNode.paint.blendMode, + stroke: bottomPathNode.paint.stroke, + fill: Fill( + color: overlapColor, + shader: bottomPathNode.paint.fill?.shader)), + bounds: bottomPathNode.bounds, + path: newOverlapVGPath); + + bottomPathOpsPath.dispose(); + topPathOpsPath.dispose(); + intersection.dispose(); + newBottomPath.dispose(); + + return [ + newBottomPathNode, + newTopPathNode, + newOverlapPathNode + ]; + } + return [bottomPathNode, topPathNode]; + } + + /// Determines if node is optimizable. + bool isOptimizable(Node node) { + return node is ResolvedPathNode && + node.paint.stroke?.width == null && + node.paint.stroke?.color == null && + node.paint.fill?.shader == null; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitEmptyNode(Node node, void data) { + final _Result result = _Result(node); + return result; + } + + /// Visits applies optimizer to all children of ParentNode. + // ignore: library_private_types_in_public_api + _Result visitChildren(Node node, _Result data) { + if (node is ParentNode) { + data = node.accept(this, data); + } + return data; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitParentNode(ParentNode parentNode, Node data) { + int pathNodeCount = 0; + final List> newChildList = >[]; + final List newChildren = []; + + for (final Node child in parentNode.children) { + if (child is ResolvedPathNode) { + pathNodeCount++; + } + newChildList.add([child]); + } + + int index = 0; + ResolvedPathNode? lastPathNode; + int? lastPathNodeIndex; + + /// If the group opacity is set the children path nodes + /// cannot be optimized. + if (!parentNode.attributes.hasOpacity) { + /// If there are not at least 2 path nodes, an optimization cannot be + /// performed since 2 nodes are required for an 'overlap' to occur. + if (pathNodeCount >= 2) { + for (Node child in parentNode.children) { + if (isOptimizable(child)) { + child = child as ResolvedPathNode; + + /// If there is no previous path node to calculate + /// the overlap with, the current optimizable child will + /// be assigned as the lastPathNode. + if (lastPathNode == null || lastPathNodeIndex == null) { + lastPathNode = child; + lastPathNodeIndex = index; + } else { + /// If it is the case that the current node, which is + /// the "top" path, is opaque, the removeOverlap function + /// will be used. + if (child.paint.fill?.color.a == 255) { + newChildList[lastPathNodeIndex] = [ + removeOverlap(lastPathNode, child) + ]; + lastPathNode = child; + lastPathNodeIndex = index; + } else { + /// If it is the case that the current node, which is + /// the "top" path, is semi-transparent, the + /// resolveOpacityOverlap function will be used. + /// Note: The "top" and "intersection" path nodes that + /// are returned will not be further optimized. + newChildList[lastPathNodeIndex] = resolveOpacityOverlap( + newChildList[lastPathNodeIndex].first as ResolvedPathNode, + child); + newChildList[index] = []; + lastPathNode = null; + lastPathNodeIndex = null; + } + } + // } else { + // // Conservatively bail out here. There's some child that isn't + // // optimizable, and there aren't sufficient checks to make sure + // // we can make sense of what's actually going on anymore. + // return _Result(parentNode); + } + index++; + } + index = 0; + + /// Here the 2-dimensional list of new children is flattened. + for (final List child in newChildList) { + if (child.isNotEmpty) { + if (child.first is ResolvedPathNode) { + newChildren.addAll(child); + } else { + newChildren.add(child.first.accept(this, parentNode).node); + } + } + } + } else { + /// If there's less than 2 path nodes, the parent node's direct children + /// cannot be optimized, but it may have grand children that can be optimized, + /// so accept will be called on the children. + for (final Node child in parentNode.children) { + newChildren.add(child.accept(this, parentNode).node); + } + } + } else { + /// If group opacity is set, the parent nodes children cannot be optimized. + return _Result(parentNode); + } + final _Result result = _Result(ParentNode(parentNode.attributes, + children: newChildren, precalculatedTransform: parentNode.transform)); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitMaskNode(MaskNode maskNode, Node data) { + return _Result(maskNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitPathNode(PathNode pathNode, Node data) { + return _Result(pathNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + final _Result childResult = maskNode.child.accept(this, maskNode); + final ResolvedMaskNode newMaskNode = ResolvedMaskNode( + child: childResult.node, + mask: maskNode.mask, + blendMode: maskNode.blendMode); + final _Result result = _Result(newMaskNode); + result.children.add(childResult.node); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedClipNode(ResolvedClipNode clipNode, Node data) { + final _Result childResult = clipNode.child.accept(this, clipNode); + final ResolvedClipNode newClipNode = + ResolvedClipNode(clips: clipNode.clips, child: childResult.node); + final _Result result = _Result(newClipNode); + result.children.add(childResult.node); + + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPath(ResolvedPathNode pathNode, Node data) { + return _Result(pathNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedText(ResolvedTextNode textNode, Node data) { + return _Result(textNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedVerticesNode( + ResolvedVerticesNode verticesNode, Node data) { + return _Result(verticesNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitSaveLayerNode(SaveLayerNode layerNode, Node data) { + final List newChildren = []; + for (final Node child in layerNode.children) { + final _Result childResult = child.accept(this, layerNode); + newChildren.add(childResult.node); + } + final SaveLayerNode newLayerNode = SaveLayerNode(layerNode.attributes, + paint: layerNode.paint, children: newChildren); + + final _Result result = _Result(newLayerNode); + result.children.addAll(newChildren); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedImageNode( + ResolvedImageNode resolvedImageNode, Node data) { + return _Result(resolvedImageNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitViewportNode(ViewportNode viewportNode, void data) { + final List children = []; + + final ParentNode parentNode = ParentNode(SvgAttributes.empty, + children: viewportNode.children.toList()); + + final _Result childResult = parentNode.accept(this, viewportNode); + children.addAll((childResult.node as ParentNode).children); + + final ViewportNode node = ViewportNode( + viewportNode.attributes, + width: viewportNode.width, + height: viewportNode.height, + transform: viewportNode.transform, + children: children, + ); + + final _Result result = _Result(node); + result.children.addAll(children); + return result; + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedPatternNode(ResolvedPatternNode patternNode, Node data) { + return _Result(patternNode); + } + + @override + // ignore: library_private_types_in_public_api + _Result visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + return _Result( + ResolvedTextPositionNode( + textPositionNode.textPosition, + [ + for (final Node child in textPositionNode.children) + child.accept(this, data).node + ], + ), + ); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/parser.dart b/packages/vector_graphics_compiler/lib/src/svg/parser.dart new file mode 100644 index 00000000000..6cac8884d7f --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/parser.dart @@ -0,0 +1,2453 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'dart:collection'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:xml/xml_events.dart'; + +import '../geometry/basic_types.dart'; +import '../geometry/matrix.dart'; +import '../geometry/path.dart'; +import '../image/image_info.dart'; +import '../paint.dart'; +import '../vector_instructions.dart'; +import 'clipping_optimizer.dart'; +import 'color_mapper.dart'; +import 'colors.dart'; +import 'masking_optimizer.dart'; +import 'node.dart'; +import 'numbers.dart' as numbers show parseDoubleWithUnits; +import 'numbers.dart' hide parseDoubleWithUnits; +import 'overdraw_optimizer.dart'; +import 'parsers.dart'; +import 'path_ops.dart' as path_ops; +import 'resolver.dart'; +import 'tessellator.dart'; +import 'theme.dart'; +import 'visitor.dart'; + +final Set _unhandledElements = {'title', 'desc'}; + +typedef _ParseFunc = void Function( + SvgParser parserState, bool warningsAsErrors); +typedef _PathFunc = Path? Function(SvgParser parserState); + +final RegExp _whitespacePattern = RegExp(r'\s'); + +const Map _svgElementParsers = { + 'svg': _Elements.svg, + 'g': _Elements.g, + 'a': _Elements.g, // treat as group + 'use': _Elements.use, + 'symbol': _Elements.symbol, + 'mask': _Elements.symbol, // treat as symbol + 'pattern': _Elements.pattern, + 'radialGradient': _Elements.radialGradient, + 'linearGradient': _Elements.linearGradient, + 'clipPath': _Elements.clipPath, + 'image': _Elements.image, + 'text': _Elements.textOrTspan, + 'tspan': _Elements.textOrTspan, +}; + +const Map _svgPathFuncs = { + 'circle': _Paths.circle, + 'path': _Paths.path, + 'rect': _Paths.rect, + 'polygon': _Paths.polygon, + 'polyline': _Paths.polyline, + 'ellipse': _Paths.ellipse, + 'line': _Paths.line, +}; + +// ignore: avoid_classes_with_only_static_members +class _Elements { + static void svg(SvgParser parserState, bool warningsAsErrors) { + final _Viewport viewBox = parserState._parseViewBox(); + + // TODO(dnfield): Support nested SVG elements. https://github.com/dnfield/flutter_svg/issues/132 + if (parserState._root != null) { + const String errorMessage = 'Unsupported nested element.'; + if (warningsAsErrors) { + throw UnsupportedError(errorMessage); + } + parserState._parentDrawables.addLast( + _SvgGroupTuple( + 'svg', + ViewportNode( + parserState._currentAttributes, + width: viewBox.width, + height: viewBox.height, + transform: viewBox.transform, + ), + ), + ); + return; + } + parserState._root = ViewportNode( + parserState._currentAttributes, + width: viewBox.width, + height: viewBox.height, + transform: viewBox.transform, + ); + parserState.addGroup(parserState._currentStartElement!, parserState._root!); + return; + } + + static void g(SvgParser parserState, bool warningsAsErrors) { + if (parserState._currentStartElement?.isSelfClosing ?? false) { + return; + } + final ParentNode parent = parserState.currentGroup!; + + final ParentNode group = ParentNode(parserState._currentAttributes); + + parent.addChild( + group, + clipId: parserState._currentAttributes.clipPathId, + clipResolver: parserState._definitions.getClipPath, + maskId: parserState.attribute('mask'), + maskResolver: parserState._definitions.getDrawable, + patternId: parserState._definitions.getPattern(parserState), + patternResolver: parserState._definitions.getDrawable, + ); + parserState.addGroup(parserState._currentStartElement!, group); + return; + } + + static void textOrTspan(SvgParser parserState, bool warningsAsErrors) { + if (parserState._currentStartElement?.isSelfClosing ?? false) { + return; + } + final ParentNode parent = parserState.currentGroup!; + final XmlStartElementEvent element = parserState._currentStartElement!; + + final TextPositionNode group = TextPositionNode( + parserState._currentAttributes, + reset: element.localName == 'text', + ); + + parent.addChild( + group, + clipId: parserState._currentAttributes.clipPathId, + clipResolver: parserState._definitions.getClipPath, + maskId: parserState.attribute('mask'), + maskResolver: parserState._definitions.getDrawable, + patternId: parserState._definitions.getPattern(parserState), + patternResolver: parserState._definitions.getDrawable, + ); + parserState.addGroup(element, group); + return; + } + + static void symbol(SvgParser parserState, bool warningsAsErrors) { + final ParentNode group = ParentNode(parserState._currentAttributes); + parserState.addGroup(parserState._currentStartElement!, group); + return; + } + + static void pattern(SvgParser parserState, bool warningsAsErrors) { + final SvgAttributes attributes = parserState._currentAttributes; + final String rawWidth = parserState.attribute('width') ?? ''; + final String rawHeight = parserState.attribute('height') ?? ''; + + double? patternWidth = + parsePatternUnitToDouble(rawWidth, 'width', viewBox: parserState._root); + double? patternHeight = parsePatternUnitToDouble(rawHeight, 'height', + viewBox: parserState._root); + + if (patternWidth == null || patternHeight == null) { + final _Viewport viewBox = parserState._parseViewBox(); + patternWidth = viewBox.width; + patternHeight = viewBox.height; + } + + final String? rawX = attributes.raw['x']; + final String? rawY = attributes.raw['y']; + final String id = parserState.buildUrlIri(); + parserState.patternIds.add(id); + final SvgAttributes newAttributes = SvgAttributes._( + raw: attributes.raw, + id: attributes.id, + href: attributes.href, + transform: attributes.transform, + color: attributes.color, + stroke: attributes.stroke, + fill: attributes.fill, + fillRule: attributes.fillRule, + clipRule: attributes.clipRule, + clipPathId: attributes.clipPathId, + blendMode: attributes.blendMode, + fontFamily: attributes.fontFamily, + fontWeight: attributes.fontWeight, + fontSize: attributes.fontSize, + x: DoubleOrPercentage.fromString(rawX), + y: DoubleOrPercentage.fromString(rawY), + width: patternWidth, + height: patternHeight); + + final ParentNode group = ParentNode(newAttributes); + parserState.addGroup(parserState._currentStartElement!, group); + return; + } + + static void use(SvgParser parserState, bool warningsAsErrors) { + final ParentNode? parent = parserState.currentGroup; + final String? xlinkHref = parserState._currentAttributes.href; + if (xlinkHref == null || xlinkHref.isEmpty) { + return; + } + + final AffineMatrix transform = + (parseTransform(parserState.attribute('transform')) ?? + AffineMatrix.identity) + .translated( + parserState.parseDoubleWithUnits( + parserState.attribute('x', def: '0'), + )!, + parserState.parseDoubleWithUnits( + parserState.attribute('y', def: '0'), + )!, + ); + + final ParentNode group = ParentNode( + // parserState._currentAttributes, + SvgAttributes.empty, + precalculatedTransform: transform, + ); + + group.addChild( + DeferredNode( + parserState._currentAttributes, + refId: 'url($xlinkHref)', + resolver: parserState._definitions.getDrawable, + ), + clipResolver: parserState._definitions.getClipPath, + maskResolver: parserState._definitions.getDrawable, + patternResolver: parserState._definitions.getDrawable, + ); + if ('#${parserState._currentAttributes.id}' != xlinkHref) { + parserState.checkForIri(group); + } + parent!.addChild( + group, + clipId: parserState._currentAttributes.clipPathId, + clipResolver: parserState._definitions.getClipPath, + maskId: parserState.attribute('mask'), + maskResolver: parserState._definitions.getDrawable, + patternId: parserState._definitions.getPattern(parserState), + patternResolver: parserState._definitions.getDrawable, + ); + return; + } + + static void parseStops( + SvgParser parserState, + List colors, + List offsets, + ) { + for (final XmlEvent event in parserState._readSubtree()) { + if (event is XmlEndElementEvent) { + continue; + } + if (event is XmlStartElementEvent) { + final String rawOpacity = parserState.attribute( + 'stop-opacity', + def: '1', + )!; + final Color stopColor = parserState.parseColor( + parserState.attribute('stop-color'), + attributeName: 'stop-color', + id: parserState._currentAttributes.id) ?? + Color.opaqueBlack; + colors.add(stopColor.withOpacity(parseDouble(rawOpacity)!)); + + final String rawOffset = parserState.attribute( + 'offset', + def: '0%', + )!; + offsets.add(parseDecimalOrPercentage(rawOffset)); + } + } + return; + } + + static void radialGradient( + SvgParser parserState, + bool warningsAsErrors, + ) { + final GradientUnitMode? unitMode = parserState.parseGradientUnitMode(); + final String? rawCx = parserState.attribute('cx', def: '50%'); + final String? rawCy = parserState.attribute('cy', def: '50%'); + final String? rawR = parserState.attribute('r', def: '50%'); + final String? rawFx = parserState.attribute('fx', def: rawCx); + final String? rawFy = parserState.attribute('fy', def: rawCy); + final TileMode? spreadMethod = parserState.parseTileMode(); + final String id = parserState.buildUrlIri(); + final AffineMatrix? originalTransform = parseTransform( + parserState.attribute('gradientTransform'), + ); + + List? offsets; + List? colors; + + final bool defer = parserState._currentStartElement!.isSelfClosing; + if (!defer) { + offsets = []; + colors = []; + parseStops(parserState, colors, offsets); + } + + final double cx = parseDecimalOrPercentage(rawCx!); + final double cy = parseDecimalOrPercentage(rawCy!); + final double r = parseDecimalOrPercentage(rawR!); + final double fx = parseDecimalOrPercentage(rawFx!); + final double fy = parseDecimalOrPercentage(rawFy!); + + parserState._definitions.addGradient( + RadialGradient( + id: id, + center: Point(cx, cy), + radius: r, + focalPoint: (fx != cx || fy != cy) ? Point(fx, fy) : null, + colors: colors, + offsets: offsets, + unitMode: unitMode, + tileMode: spreadMethod, + transform: originalTransform, + ), + parserState._currentAttributes.href, + ); + return; + } + + static void linearGradient( + SvgParser parserState, + bool warningsAsErrors, + ) { + final GradientUnitMode? unitMode = parserState.parseGradientUnitMode(); + final String x1 = parserState.attribute('x1', def: '0%')!; + final String x2 = parserState.attribute('x2', def: '100%')!; + final String y1 = parserState.attribute('y1', def: '0%')!; + final String y2 = parserState.attribute('y2', def: '0%')!; + final String id = parserState.buildUrlIri(); + final AffineMatrix? originalTransform = parseTransform( + parserState.attribute('gradientTransform'), + ); + final TileMode? spreadMethod = parserState.parseTileMode(); + + List? offsets; + List? colors; + + final bool defer = parserState._currentStartElement!.isSelfClosing; + if (!defer) { + offsets = []; + colors = []; + parseStops(parserState, colors, offsets); + } + + final Point fromPoint = Point( + parseDecimalOrPercentage(x1), + parseDecimalOrPercentage(y1), + ); + final Point toPoint = Point( + parseDecimalOrPercentage(x2), + parseDecimalOrPercentage(y2), + ); + + parserState._definitions.addGradient( + LinearGradient( + id: id, + from: fromPoint, + to: toPoint, + colors: colors, + offsets: offsets, + tileMode: spreadMethod, + unitMode: unitMode, + transform: originalTransform, + ), + parserState._currentAttributes.href, + ); + + return; + } + + static void clipPath(SvgParser parserState, bool warningsAsErrors) { + final String id = parserState.buildUrlIri(); + final List pathNodes = []; + for (final XmlEvent event in parserState._readSubtree()) { + if (event is XmlEndElementEvent) { + continue; + } + if (event is XmlStartElementEvent) { + final _PathFunc? pathFn = _svgPathFuncs[event.name]; + + if (pathFn != null) { + final Path sourcePath = parserState.applyTransformIfNeeded( + pathFn(parserState)!, + parserState.currentGroup?.transform, + ); + pathNodes.add( + PathNode( + Path( + commands: sourcePath.commands.toList(), + fillType: parserState._currentAttributes.clipRule ?? + PathFillType.nonZero, + ), + parserState._currentAttributes, + ), + ); + } else if (event.name == 'use') { + final String? xlinkHref = parserState._currentAttributes.href; + pathNodes.add( + DeferredNode( + parserState._currentAttributes, + refId: 'url($xlinkHref)', + resolver: parserState._definitions.getDrawable, + ), + ); + } else { + final String errorMessage = + 'Unsupported clipPath child ${event.name}'; + if (warningsAsErrors) { + throw UnsupportedError(errorMessage); + } + } + } + } + parserState._definitions.addClipPath( + id, + pathNodes, + ); + return; + } + + static void image( + SvgParser parserState, + bool warningsAsErrors, + ) { + final String? xlinkHref = parserState._currentAttributes.href; + if (xlinkHref == null) { + return; + } + + if (xlinkHref.startsWith('data:')) { + const Map supportedMimeTypes = { + 'png': ImageFormat.png, + 'jpeg': ImageFormat.jpeg, + 'jpg': ImageFormat.jpeg, + 'webp': ImageFormat.webp, + 'gif': ImageFormat.gif, + 'bmp': ImageFormat.bmp, + }; + final int semiColonLocation = xlinkHref.indexOf(';') + 1; + final int commaLocation = xlinkHref.indexOf(',', semiColonLocation) + 1; + final String mimeType = xlinkHref + .substring(xlinkHref.indexOf('/') + 1, semiColonLocation - 1) + .replaceAll(_whitespacePattern, '') + .toLowerCase(); + + final ImageFormat? format = supportedMimeTypes[mimeType]; + if (format == null) { + if (warningsAsErrors) { + throw UnimplementedError( + 'Image data format not supported: $mimeType'); + } else { + print('Warning: Unsupported image format $mimeType'); + } + return; + } + + final Uint8List data = base64.decode(xlinkHref + .substring(commaLocation) + .replaceAll(_whitespacePattern, '')); + final ImageNode image = + ImageNode(data, format, parserState._currentAttributes); + parserState.currentGroup!.addChild( + image, + clipResolver: parserState._definitions.getClipPath, + maskResolver: parserState._definitions.getDrawable, + patternResolver: parserState._definitions.getDrawable, + ); + parserState.checkForIri(image); + return; + } + if (warningsAsErrors) { + throw UnimplementedError('Image data format not supported: $xlinkHref'); + } + return; + } +} + +// ignore: avoid_classes_with_only_static_members +class _Paths { + static Path circle(SvgParser parserState) { + final double cx = parserState.parseDoubleWithUnits( + parserState.attribute('cx', def: '0'), + )!; + final double cy = parserState.parseDoubleWithUnits( + parserState.attribute('cy', def: '0'), + )!; + final double r = parserState.parseDoubleWithUnits( + parserState.attribute('r', def: '0'), + )!; + final Rect oval = Rect.fromCircle(cx, cy, r); + return PathBuilder(parserState._currentAttributes.fillRule) + .addOval(oval) + .toPath(); + } + + static Path path(SvgParser parserState) { + final String d = parserState.attribute('d', def: '')!; + return parseSvgPathData(d, parserState._currentAttributes.fillRule); + } + + static Path rect(SvgParser parserState) { + final double x = parserState.parseDoubleWithUnits( + parserState.attribute('x', def: '0'), + )!; + final double y = parserState.parseDoubleWithUnits( + parserState.attribute('y', def: '0'), + )!; + final double w = parserState.parseDoubleWithUnits( + parserState.attribute('width', def: '0'), + )!; + final double h = parserState.parseDoubleWithUnits( + parserState.attribute('height', def: '0'), + )!; + String? rxRaw = parserState.attribute('rx'); + String? ryRaw = parserState.attribute('ry'); + rxRaw ??= ryRaw; + ryRaw ??= rxRaw; + + if (rxRaw != null && rxRaw != '') { + final double rx = parserState.parseDoubleWithUnits(rxRaw)!; + final double ry = parserState.parseDoubleWithUnits(ryRaw)!; + return PathBuilder(parserState._currentAttributes.fillRule) + .addRRect(Rect.fromLTWH(x, y, w, h), rx, ry) + .toPath(); + } + + return PathBuilder(parserState._currentAttributes.fillRule) + .addRect(Rect.fromLTWH(x, y, w, h)) + .toPath(); + } + + static Path? polygon(SvgParser parserState) { + return parsePathFromPoints(parserState, true); + } + + static Path? polyline(SvgParser parserState) { + return parsePathFromPoints(parserState, false); + } + + static Path? parsePathFromPoints(SvgParser parserState, bool close) { + final String points = parserState.attribute('points', def: '')!; + if (points == '') { + return null; + } + final String path = 'M$points${close ? 'z' : ''}'; + + return parseSvgPathData(path, parserState._currentAttributes.fillRule); + } + + static Path ellipse(SvgParser parserState) { + final double cx = parserState.parseDoubleWithUnits( + parserState.attribute('cx', def: '0'), + )!; + final double cy = parserState.parseDoubleWithUnits( + parserState.attribute('cy', def: '0'), + )!; + final double rx = parserState.parseDoubleWithUnits( + parserState.attribute('rx', def: '0'), + )!; + final double ry = parserState.parseDoubleWithUnits( + parserState.attribute('ry', def: '0'), + )!; + + final Rect r = Rect.fromLTWH(cx - rx, cy - ry, rx * 2, ry * 2); + return PathBuilder(parserState._currentAttributes.fillRule) + .addOval(r) + .toPath(); + } + + static Path line(SvgParser parserState) { + final double x1 = parserState.parseDoubleWithUnits( + parserState.attribute('x1', def: '0'), + )!; + final double x2 = parserState.parseDoubleWithUnits( + parserState.attribute('x2', def: '0'), + )!; + final double y1 = parserState.parseDoubleWithUnits( + parserState.attribute('y1', def: '0'), + )!; + final double y2 = parserState.parseDoubleWithUnits( + parserState.attribute('y2', def: '0'), + )!; + + return PathBuilder(parserState._currentAttributes.fillRule) + .moveTo(x1, y1) + .lineTo(x2, y2) + .toPath(); + } +} + +class _SvgGroupTuple { + _SvgGroupTuple(this.name, this.drawable); + + final String name; + final ParentNode drawable; +} + +/// Parse an SVG to the initial Node tree. +@visibleForTesting +Node parseToNodeTree(String source) { + return SvgParser(source, const SvgTheme(), null, true, null) + ._parseToNodeTree(); +} + +/// Reads an SVG XML string and via the [parse] method creates a set of +/// [VectorInstructions]. +class SvgParser { + /// Creates a new [SvgParser]. + SvgParser( + String xml, + this.theme, + this._key, + this._warningsAsErrors, + this._colorMapper, + ) : _eventIterator = parseEvents(xml).iterator; + + /// The theme used when parsing SVG elements. + final SvgTheme theme; + + final ColorMapper? _colorMapper; + + final Iterator _eventIterator; + final String? _key; + final bool _warningsAsErrors; + final _Resolver _definitions = _Resolver(); + final Queue<_SvgGroupTuple> _parentDrawables = ListQueue<_SvgGroupTuple>(10); + + /// Toggles whether [MaskingOptimizer] is enabled or disabled. + bool enableMaskingOptimizer = true; + + /// Toggles whether [ClippingOptimizer] is enabled or disabled. + bool enableClippingOptimizer = true; + + /// Toggles whether [OverdrawOptimizer] is enabled or disabled. + bool enableOverdrawOptimizer = true; + + /// List of known patternIds. + Set patternIds = {}; + + ViewportNode? _root; + SvgAttributes _currentAttributes = SvgAttributes.empty; + XmlStartElementEvent? _currentStartElement; + + /// The current depth of the reader in the XML hierarchy. + int depth = 0; + + void _discardSubtree() { + final int subtreeStartDepth = depth; + while (_eventIterator.moveNext()) { + final XmlEvent event = _eventIterator.current; + if (event is XmlStartElementEvent && !event.isSelfClosing) { + depth += 1; + } else if (event is XmlEndElementEvent) { + depth -= 1; + assert(depth >= 0); + } + _currentAttributes = SvgAttributes.empty; + _currentStartElement = null; + if (depth < subtreeStartDepth) { + return; + } + } + } + + XmlEndElementEvent? _lastEndElementEvent; + + Iterable _readSubtree() sync* { + final int subtreeStartDepth = depth; + while (_eventIterator.moveNext()) { + final XmlEvent event = _eventIterator.current; + bool isSelfClosing = false; + if (event is XmlStartElementEvent) { + final Map attributeMap = + _createAttributeMap(event.attributes); + if (!_isVisible(attributeMap)) { + if (!event.isSelfClosing) { + depth += 1; + _discardSubtree(); + } + continue; + } + _currentStartElement = event; + _currentAttributes = _createSvgAttributes( + attributeMap, + currentColor: depth == 0 ? theme.currentColor : null, + ); + depth += 1; + isSelfClosing = event.isSelfClosing; + } + yield event; + + if (isSelfClosing || event is XmlEndElementEvent) { + depth -= 1; + assert(depth >= 0); + _currentAttributes = SvgAttributes.empty; + _currentStartElement = null; + } + if (depth < subtreeStartDepth) { + return; + } + } + } + + static final RegExp _contiguousSpaceMatcher = RegExp(r' +'); + bool _lastTextEndedWithSpace = false; + void _appendText(String text) { + assert(_inTextOrTSpan); + + assert(_whitespacePattern.pattern == r'\s'); + final bool textHasNonWhitespace = text.trim() != ''; + + // Not from the spec, but seems like how Chrome behaves. + // - If `x` is specified, don't prepend whitespace. + // - If the last element was a tspan and we're dealing with some + // non-whitespace data, prepend a space. + // - If the last text wasn't whitespace and ended with whitespace, prepend + // a space. + final bool prependSpace = _currentAttributes.x == null && + (_lastEndElementEvent?.localName == 'tspan' && + textHasNonWhitespace) || + _lastTextEndedWithSpace; + + _lastTextEndedWithSpace = textHasNonWhitespace && + text.startsWith(_whitespacePattern, text.length - 1); + + // From the spec: + // First, it will remove all newline characters. + // Then it will convert all tab characters into space characters. + // Then, it will strip off all leading and trailing space characters. + // Then, all contiguous space characters will be consolidated. + text = text + .replaceAll('\n', '') + .replaceAll('\t', ' ') + .trim() + .replaceAll(_contiguousSpaceMatcher, ' '); + + if (text.isEmpty) { + return; + } + + currentGroup?.addChild( + TextNode( + prependSpace ? ' $text' : text, + _currentAttributes, + ), + // Do not supply pattern/clip/mask IDs, those are handled by the group + // text or tspan this text is part of. + clipResolver: _definitions.getClipPath, + maskResolver: _definitions.getDrawable, + patternResolver: _definitions.getDrawable, + ); + } + + bool get _inTextOrTSpan => + _parentDrawables.isNotEmpty && + (_parentDrawables.last.name == 'text' || + _parentDrawables.last.name == 'tspan'); + + void _parseTree() { + for (final XmlEvent event in _readSubtree()) { + if (event is XmlStartElementEvent) { + if (startElement(event)) { + continue; + } + final _ParseFunc? parseFunc = _svgElementParsers[event.name]; + if (parseFunc == null) { + if (!event.isSelfClosing) { + _discardSubtree(); + } + assert(() { + unhandledElement(event); + return true; + }()); + } else { + parseFunc(this, _warningsAsErrors); + } + } else if (event is XmlEndElementEvent) { + endElement(event); + } else if (_inTextOrTSpan) { + if (event is XmlCDATAEvent) { + _appendText(event.value); + } else if (event is XmlTextEvent) { + _appendText(event.value); + } + } + } + if (_root == null) { + throw StateError('Invalid SVG data'); + } + _definitions._seal(); + } + + /// Drive the XML reader to EOF and produce [VectorInstructions]. + VectorInstructions parse() { + _parseTree(); + + /// Resolve the tree + final ResolvingVisitor resolvingVisitor = ResolvingVisitor(); + final Tessellator tessellator = Tessellator(); + final MaskingOptimizer maskingOptimizer = MaskingOptimizer(); + final ClippingOptimizer clippingOptimizer = ClippingOptimizer(); + final OverdrawOptimizer overdrawOptimizer = OverdrawOptimizer(); + + Node newRoot = _root!.accept(resolvingVisitor, AffineMatrix.identity); + + // The order of these matters. The overdraw optimizer can do its best if + // masks and unnecessary clips have been eliminated. + if (enableMaskingOptimizer) { + if (path_ops.isPathOpsInitialized) { + newRoot = maskingOptimizer.apply(newRoot); + } else { + throw Exception('PathOps library was not initialized.'); + } + } + + if (enableClippingOptimizer) { + if (path_ops.isPathOpsInitialized) { + newRoot = clippingOptimizer.apply(newRoot); + } else { + throw Exception('PathOps library was not initialized.'); + } + } + + if (enableOverdrawOptimizer) { + if (path_ops.isPathOpsInitialized) { + newRoot = overdrawOptimizer.apply(newRoot); + } else { + throw Exception('PathOps library was not initialized.'); + } + } + + if (isTesselatorInitialized) { + newRoot = newRoot.accept(tessellator, null); + } + + /// Convert to vector instructions + final CommandBuilderVisitor commandVisitor = CommandBuilderVisitor(); + newRoot.accept(commandVisitor, null); + + return commandVisitor.toInstructions(); + } + + Node _parseToNodeTree() { + _parseTree(); + return _root!; + } + + /// Gets the attribute for the current position of the parser. + String? attribute(String name, {String? def}) => + _currentAttributes.raw[name] ?? def; + + /// The current group, if any, in the [Drawable] heirarchy. + ParentNode? get currentGroup { + assert(_parentDrawables.isNotEmpty); + return _parentDrawables.last.drawable; + } + + /// The root bounds of the drawable. + Rect get rootBounds { + assert(_root != null, 'Cannot get rootBounds with null root'); + return _root!.viewport; + } + + /// Whether this [DrawableStyleable] belongs in the [DrawableDefinitions] or not. + bool checkForIri(AttributedNode drawable) { + assert('#${_currentAttributes.id}' != _currentAttributes.href); + final String iri = buildUrlIri(); + if (iri != emptyUrlIri) { + _definitions.addDrawable(iri, drawable); + return true; + } + return false; + } + + /// Appends a group to the collection. + void addGroup(XmlStartElementEvent event, ParentNode drawable) { + _parentDrawables.addLast(_SvgGroupTuple(event.name, drawable)); + checkForIri(drawable); + } + + /// Updates the [VectorInstructions] with the current path and paint. + bool addShape(XmlStartElementEvent event) { + final _PathFunc? pathFunc = _svgPathFuncs[event.name]; + if (pathFunc == null) { + return false; + } + final ParentNode parent = _parentDrawables.last.drawable; + final Path path = pathFunc(this)!; + final PathNode drawable = PathNode(path, _currentAttributes); + checkForIri(drawable); + + parent.addChild( + drawable, + clipId: _currentAttributes.clipPathId, + clipResolver: _definitions.getClipPath, + maskId: attribute('mask'), + maskResolver: _definitions.getDrawable, + patternId: _definitions.getPattern(this), + patternResolver: _definitions.getDrawable, + ); + return true; + } + + /// Potentially handles a starting element, if it was a singular shape or a + /// `` element. + bool startElement(XmlStartElementEvent event) { + if (event.name == 'defs') { + if (!event.isSelfClosing) { + addGroup( + event, + ParentNode(_currentAttributes), + ); + return true; + } + } + return addShape(event); + } + + /// Handles the end of an XML element. + void endElement(XmlEndElementEvent event) { + while (event.name == _parentDrawables.last.name && + _parentDrawables.last.drawable is ClipNode) { + _parentDrawables.removeLast(); + } + if (event.name == _parentDrawables.last.name) { + _parentDrawables.removeLast(); + } + _lastEndElementEvent = event; + if (event.name == 'text') { + // reset state. + _lastTextEndedWithSpace = false; + } + } + + /// Prints an error for unhandled elements. + /// + /// Will only print an error once for unhandled/unexpected elements, except for + /// ``, ``, and `` elements. + void unhandledElement(XmlStartElementEvent event) { + final String errorMessage = + 'unhandled element <${event.name}/>; Picture key: $_key'; + if (_warningsAsErrors) { + // Throw error instead of log warning. + throw UnimplementedError(errorMessage); + } + if (_unhandledElements.add(event.name)) { + print(errorMessage); + } + } + + /// Parses a `rawDouble` `String` to a `double` + /// taking into account absolute and relative units + /// (`px`, `em` or `ex`). + /// + /// Passing an `em` value will calculate the result + /// relative to the provided [fontSize]: + /// 1 em = 1 * `fontSize`. + /// + /// Passing an `ex` value will calculate the result + /// relative to the provided [xHeight]: + /// 1 ex = 1 * `xHeight`. + /// + /// The `rawDouble` might include a unit which is + /// stripped off when parsed to a `double`. + /// + /// Passing `null` will return `null`. + double? parseDoubleWithUnits( + String? rawDouble, { + bool tryParse = false, + }) { + return numbers.parseDoubleWithUnits( + rawDouble, + tryParse: tryParse, + theme: theme, + ); + } + + static final Map _kTextSizeMap = { + 'xx-small': 10, + 'x-small': 12, + 'small': 14, + 'medium': 18, + 'large': 22, + 'x-large': 26, + 'xx-large': 32, + }; + + /// Parses a `font-size` attribute. + double? parseFontSize(String? raw) { + if (raw == null || raw == '') { + return null; + } + + double? ret = parseDoubleWithUnits( + raw, + tryParse: true, + ); + if (ret != null) { + return ret; + } + + raw = raw.toLowerCase().trim(); + ret = _kTextSizeMap[raw]; + if (ret != null) { + return ret; + } + + // TODO(dnfield): support 'larger' and 'smaller'. + // Rough idea for how to do this: this method returns a struct that contains + // either a double? fontSize or a double? multiplier. + // Larger multiplier: 1.2 + // Smaller multiplier: .8 + // When resolving the font size later, multiple the incoming size by the + // multiplier if specified. + // There was once an implementation of this which was definitely not + // correct but probably not used much either. + + throw StateError('Could not parse font-size: $raw'); + } + + /// Parses a `text-decoration` attribute value into a [TextDecoration]. + TextDecoration? parseTextDecoration(String? textDecoration) { + if (textDecoration == null) { + return null; + } + switch (textDecoration) { + case 'none': + return TextDecoration.none; + case 'underline': + return TextDecoration.underline; + case 'overline': + return TextDecoration.overline; + case 'line-through': + return TextDecoration.lineThrough; + } + throw UnsupportedError( + 'Attribute value for text-decoration="$textDecoration"' + ' is not supported'); + } + + /// Parses a `text-decoration-style` attribute value into a [TextDecorationStyle]. + TextDecorationStyle? parseTextDecorationStyle(String? textDecorationStyle) { + if (textDecorationStyle == null) { + return null; + } + switch (textDecorationStyle) { + case 'solid': + return TextDecorationStyle.solid; + case 'dashed': + return TextDecorationStyle.dashed; + case 'dotted': + return TextDecorationStyle.dotted; + case 'double': + return TextDecorationStyle.double; + case 'wavy': + return TextDecorationStyle.wavy; + } + throw UnsupportedError( + 'Attribute value for text-decoration-style="$textDecorationStyle"' + ' is not supported'); + } + + /// Parses a `text-anchor` attribute. + double? parseTextAnchor(String? raw) { + switch (raw) { + case 'end': + return 1; + case 'middle': + return .5; + case 'start': + return 0; + case 'inherit': + default: + return null; + } + } + + double _parseRawWidthHeight(String raw) { + if (raw == '100%' || raw == '') { + return double.infinity; + } + assert(() { + final RegExp notDigits = RegExp(r'[^\d\.]'); + if (!raw.endsWith('px') && + !raw.endsWith('em') && + !raw.endsWith('ex') && + raw.contains(notDigits)) { + print( + 'Warning: Flutter SVG only supports the following formats for `width` and `height` on the SVG root:\n' + ' width="100%"\n' + ' width="100em"\n' + ' width="100ex"\n' + ' width="100px"\n' + ' width="100" (where the number will be treated as pixels).\n' + 'The supplied value ($raw) will be discarded and treated as if it had not been specified.'); + } + return true; + }()); + return parseDoubleWithUnits(raw, tryParse: true) ?? double.infinity; + } + + /// Parses an SVG @viewBox attribute (e.g. 0 0 100 100) to a [Viewport]. + _Viewport _parseViewBox() { + final String viewBox = attribute('viewBox') ?? ''; + final String rawWidth = attribute('width') ?? ''; + final String rawHeight = attribute('height') ?? ''; + + if (viewBox == '' && rawWidth == '' && rawHeight == '') { + throw StateError('SVG did not specify dimensions\n\n' + 'The SVG library looks for a `viewBox` or `width` and `height` attribute ' + 'to determine the viewport boundary of the SVG. Note that these attributes, ' + 'as with all SVG attributes, are case sensitive.\n' + 'During processing, the following attributes were found:\n' + ' ${_currentAttributes.raw}'); + } + + if (viewBox == '') { + final double width = _parseRawWidthHeight(rawWidth); + final double height = _parseRawWidthHeight(rawHeight); + return _Viewport( + width, + height, + AffineMatrix.identity, + ); + } + + final List parts = viewBox.split(RegExp(r'[ ,]+')); + if (parts.length < 4) { + throw StateError('viewBox element must be 4 elements long'); + } + final double width = parseDouble(parts[2])!; + final double height = parseDouble(parts[3])!; + final double translateX = -parseDouble(parts[0])!; + final double translateY = -parseDouble(parts[1])!; + + return _Viewport( + width, + height, + AffineMatrix.identity.translated(translateX, translateY), + ); + } + + /// Builds an IRI in the form of `'url(#id)'`. + String buildUrlIri() => 'url(#${_currentAttributes.id})'; + + /// An empty IRI. + static const String emptyUrlIri = _Resolver.emptyUrlIri; + + /// Parses a `spreadMethod` attribute into a [TileMode]. + TileMode? parseTileMode() { + final String? spreadMethod = attribute('spreadMethod'); + switch (spreadMethod) { + case 'pad': + return TileMode.clamp; + case 'repeat': + return TileMode.repeated; + case 'reflect': + return TileMode.mirror; + } + return null; + } + + /// Parses the `@gradientUnits` attribute. + GradientUnitMode? parseGradientUnitMode() { + final String? gradientUnits = attribute('gradientUnits'); + switch (gradientUnits) { + case 'userSpaceOnUse': + return GradientUnitMode.userSpaceOnUse; + case 'objectBoundingBox': + return GradientUnitMode.objectBoundingBox; + } + return null; + } + + StrokeCap? _parseCap( + String? raw, + Stroke? definitionPaint, + ) { + switch (raw) { + case 'butt': + return StrokeCap.butt; + case 'round': + return StrokeCap.round; + case 'square': + return StrokeCap.square; + default: + return definitionPaint?.cap; + } + } + + StrokeJoin? _parseJoin( + String? raw, + Stroke? definitionPaint, + ) { + switch (raw) { + case 'miter': + return StrokeJoin.miter; + case 'bevel': + return StrokeJoin.bevel; + case 'round': + return StrokeJoin.round; + default: + return definitionPaint?.join; + } + } + + List? _parseDashArray(String? rawDashArray) { + if (rawDashArray == null || rawDashArray == '') { + return null; + } else if (rawDashArray == 'none') { + return const []; + } + + final List parts = rawDashArray.split(RegExp(r'[ ,]+')); + final List doubles = []; + bool atLeastOneNonZeroDash = false; + for (final String part in parts) { + final double dashOffset = parseDoubleWithUnits(part)!; + if (dashOffset != 0) { + atLeastOneNonZeroDash = true; + } + doubles.add(dashOffset); + } + if (doubles.isEmpty || !atLeastOneNonZeroDash) { + return null; + } + return doubles; + } + + double? _parseDashOffset(String? rawDashOffset) { + return parseDoubleWithUnits(rawDashOffset); + } + + /// Applies a transform to a path if the [attributes] contain a `transform`. + Path applyTransformIfNeeded(Path path, AffineMatrix? parentTransform) { + final AffineMatrix? transform = parseTransform(attribute('transform')); + + if (transform != null) { + return path.transformed(transform); + } else { + return path; + } + } + + /// Parses a `clipPath` element into a list of [Path]s. + List parseClipPath() { + final String? id = _currentAttributes.clipPathId; + if (id != null) { + return _definitions.getClipPath(id); + } + + return []; + } + + static const Map _blendModes = { + 'multiply': BlendMode.multiply, + 'screen': BlendMode.screen, + 'overlay': BlendMode.overlay, + 'darken': BlendMode.darken, + 'lighten': BlendMode.lighten, + 'color-dodge': BlendMode.colorDodge, + 'color-burn': BlendMode.colorBurn, + 'hard-light': BlendMode.hardLight, + 'soft-light': BlendMode.softLight, + 'difference': BlendMode.difference, + 'exclusion': BlendMode.exclusion, + 'hue': BlendMode.hue, + 'saturation': BlendMode.saturation, + 'color': BlendMode.color, + 'luminosity': BlendMode.luminosity, + }; + + /// Lookup the mask if the attribute is present. + Node? parseMask() { + final String? rawMaskAttribute = attribute('mask'); + if (rawMaskAttribute != null) { + return _definitions.getDrawable(rawMaskAttribute); + } + + return null; + } + + /// Lookup the pattern if the attribute is present. + Node? parsePattern() { + final String? rawPattern = attribute('fill'); + if (rawPattern != null) { + return _definitions.getDrawable(rawPattern); + } + return null; + } + + /// Parse the raw font weight string. + FontWeight? parseFontWeight(String? fontWeight) { + if (fontWeight == null) { + return null; + } + + switch (fontWeight) { + case 'normal': + return normalFontWeight; + case 'bold': + return boldFontWeight; + case '100': + return FontWeight.w100; + case '200': + return FontWeight.w200; + case '300': + return FontWeight.w300; + case '400': + return FontWeight.w400; + case '500': + return FontWeight.w500; + case '600': + return FontWeight.w600; + case '700': + return FontWeight.w700; + case '800': + return FontWeight.w800; + case '900': + return FontWeight.w900; + } + + throw StateError('Invalid "font-weight": $fontWeight'); + } + + /// Converts a SVG Color String (either a # prefixed color string or a named color) to a [Color]. + Color? parseColor( + String? colorString, { + Color? currentColor, + required String attributeName, + required String? id, + }) { + final Color? parsed = _parseColor(colorString, currentColor: currentColor); + if (parsed == null || _colorMapper == null) { + return parsed; + } + // Do not use _currentAttributes, since they may not be up to date when this + // is called. + return _colorMapper.substitute( + id, + _currentStartElement!.localName, + attributeName, + parsed, + ); + } + + Color? _parseColor(String? colorString, {Color? currentColor}) { + if (colorString == null || colorString.isEmpty) { + return null; + } + + if (colorString == 'none') { + return null; + } + + if (colorString.toLowerCase() == 'currentcolor') { + return currentColor ?? theme.currentColor; + } + + // handle hex colors e.g. #fff or #ffffff. This supports #RRGGBBAA + if (colorString[0] == '#') { + if (colorString.length == 4) { + final String r = colorString[1]; + final String g = colorString[2]; + final String b = colorString[3]; + colorString = '#$r$r$g$g$b$b'; + } + + if (colorString.length == 7 || colorString.length == 9) { + final int color = int.parse(colorString.substring(1, 7), radix: 16); + final int alpha = colorString.length == 9 + ? int.parse(colorString.substring(7, 9), radix: 16) + : 255; + return Color(color | alpha << 24); + } + } + + // handle rgba() colors e.g. rgba(255, 255, 255, 1.0) + if (colorString.toLowerCase().startsWith('rgba')) { + final List rawColorElements = colorString + .substring(colorString.indexOf('(') + 1, colorString.indexOf(')')) + .split(',') + .map((String rawColor) => rawColor.trim()) + .toList(); + + final double opacity = parseDouble(rawColorElements.removeLast())!; + + final List rgb = rawColorElements + .map((String rawColor) => int.parse(rawColor)) + .toList(); + + return Color.fromRGBO(rgb[0], rgb[1], rgb[2], opacity); + } + + // Conversion code from: https://github.com/MichaelFenwick/Color, thanks :) + if (colorString.toLowerCase().startsWith('hsl')) { + final List values = colorString + .substring(colorString.indexOf('(') + 1, colorString.indexOf(')')) + .split(',') + .map((String rawColor) { + rawColor = rawColor.trim(); + + if (rawColor.endsWith('%')) { + rawColor = rawColor.substring(0, rawColor.length - 1); + } + + if (rawColor.contains('.')) { + return (parseDouble(rawColor)! * 2.55).round(); + } + + return int.parse(rawColor); + }).toList(); + final double hue = values[0] / 360 % 1; + final double saturation = values[1] / 100; + final double luminance = values[2] / 100; + final int alpha = values.length > 3 ? values[3] : 255; + List rgb = [0, 0, 0]; + + if (hue < 1 / 6) { + rgb[0] = 1; + rgb[1] = hue * 6; + } else if (hue < 2 / 6) { + rgb[0] = 2 - hue * 6; + rgb[1] = 1; + } else if (hue < 3 / 6) { + rgb[1] = 1; + rgb[2] = hue * 6 - 2; + } else if (hue < 4 / 6) { + rgb[1] = 4 - hue * 6; + rgb[2] = 1; + } else if (hue < 5 / 6) { + rgb[0] = hue * 6 - 4; + rgb[2] = 1; + } else { + rgb[0] = 1; + rgb[2] = 6 - hue * 6; + } + + rgb = rgb + .map((double val) => val + (1 - saturation) * (0.5 - val)) + .toList(); + + if (luminance < 0.5) { + rgb = rgb.map((double val) => luminance * 2 * val).toList(); + } else { + rgb = rgb + .map((double val) => luminance * 2 * (1 - val) + 2 * val - 1) + .toList(); + } + + rgb = rgb.map((double val) => val * 255).toList(); + + return Color.fromARGB( + alpha, rgb[0].round(), rgb[1].round(), rgb[2].round()); + } + + // handle rgb() colors e.g. rgb(255, 255, 255) + if (colorString.toLowerCase().startsWith('rgb')) { + final List rgb = colorString + .substring(colorString.indexOf('(') + 1, colorString.indexOf(')')) + .split(',') + .map((String rawColor) { + rawColor = rawColor.trim(); + if (rawColor.endsWith('%')) { + rawColor = rawColor.substring(0, rawColor.length - 1); + return (parseDouble(rawColor)! * 2.55).round(); + } + return int.parse(rawColor); + }).toList(); + + // rgba() isn't really in the spec, but Firefox supported it at one point so why not. + final int a = rgb.length > 3 ? rgb[3] : 255; + return Color.fromARGB(a, rgb[0], rgb[1], rgb[2]); + } + + // handle named colors ('red', 'green', etc.). + final Color? namedColor = namedColors[colorString]; + if (namedColor != null) { + return namedColor; + } + + // This is an error, but browsers are permissive here, so we can be too. + // See for example https://github.com/dnfield/flutter_svg/issues/764 - a + // user may be working with a network based SVG that uses the string "null" + // which is not part of the specification. + return null; + } + + Map _createAttributeMap(List attributes) { + final Map attributeMap = {}; + + for (final XmlEventAttribute attribute in attributes) { + final String value = attribute.value.trim(); + if (attribute.localName == 'style') { + for (final String style in value.split(';')) { + if (style.isEmpty) { + continue; + } + final List styleParts = style.split(':'); + final String attributeValue = styleParts[1].trim(); + if (attributeValue == 'inherit') { + continue; + } + attributeMap[styleParts[0].trim()] = attributeValue; + } + } else if (value != 'inherit') { + attributeMap[attribute.localName] = value; + } + } + return attributeMap; + } + + SvgStrokeAttributes? _parseStrokeAttributes( + Map attributeMap, + double? uniformOpacity, + Color? currentColor, + String? id, + ) { + final String? rawStroke = attributeMap['stroke']; + + final String? rawStrokeOpacity = attributeMap['stroke-opacity']; + double? opacity; + if (rawStrokeOpacity != null) { + opacity = parseDouble(rawStrokeOpacity)!.clamp(0.0, 1.0); + } + if (uniformOpacity != null) { + if (opacity == null) { + opacity = uniformOpacity; + } else { + opacity *= uniformOpacity; + } + } + + final String? rawStrokeCap = attributeMap['stroke-linecap']; + final String? rawLineJoin = attributeMap['stroke-linejoin']; + final String? rawMiterLimit = attributeMap['stroke-miterlimit']; + final String? rawStrokeWidth = attributeMap['stroke-width']; + final String? rawStrokeDashArray = attributeMap['stroke-dasharray']; + final String? rawStrokeDashOffset = attributeMap['stroke-dashoffset']; + + final String? anyStrokeAttribute = rawStroke ?? + rawStrokeCap ?? + rawLineJoin ?? + rawMiterLimit ?? + rawStrokeWidth ?? + rawStrokeDashArray ?? + rawStrokeDashOffset; + + if (anyStrokeAttribute == null) { + return null; + } + + Color? strokeColor; + String? shaderId; + bool? hasPattern; + if (rawStroke?.startsWith('url') ?? false) { + shaderId = rawStroke; + strokeColor = const Color(0xFFFFFFFF); + if (patternIds.contains(rawStroke)) { + hasPattern = true; + } + } else { + strokeColor = parseColor(rawStroke, attributeName: 'stroke', id: id); + } + + final Color? color = strokeColor; + + return SvgStrokeAttributes._( + _definitions, + shaderId: shaderId, + color: rawStroke == 'none' + ? const ColorOrNone.none() + : ColorOrNone.color(color), + cap: _parseCap(rawStrokeCap, null), + join: _parseJoin(rawLineJoin, null), + miterLimit: parseDouble(rawMiterLimit), + width: parseDoubleWithUnits(rawStrokeWidth), + dashArray: _parseDashArray(rawStrokeDashArray), + dashOffset: _parseDashOffset(rawStrokeDashOffset), + hasPattern: hasPattern, + opacity: opacity, + ); + } + + SvgFillAttributes? _parseFillAttributes( + Map attributeMap, + double? uniformOpacity, + Color? currentColor, + String? id, + ) { + final String rawFill = attributeMap['fill'] ?? ''; + + final String? rawFillOpacity = attributeMap['fill-opacity']; + double? opacity; + if (rawFillOpacity != null) { + opacity = parseDouble(rawFillOpacity)!.clamp(0.0, 1.0); + } + if (uniformOpacity != null) { + if (opacity == null) { + opacity = uniformOpacity; + } else { + opacity *= uniformOpacity; + } + } + bool? hasPattern; + if (rawFill.startsWith('url')) { + if (patternIds.contains(rawFill)) { + hasPattern = true; + } + return SvgFillAttributes._( + _definitions, + color: const ColorOrNone.color(Color(0xFFFFFFFF)), + shaderId: rawFill, + hasPattern: hasPattern, + opacity: opacity, + ); + } + + Color? fillColor = parseColor(rawFill, attributeName: 'fill', id: id); + + if ((fillColor?.a ?? 255) != 255) { + opacity = fillColor!.a / 255; + fillColor = fillColor.withOpacity(1); + } + + return SvgFillAttributes._( + _definitions, + color: rawFill == 'none' + ? const ColorOrNone.none() + : ColorOrNone.color(fillColor), + opacity: opacity, + ); + } + + bool _isVisible(Map attributeMap) { + return attributeMap['display'] != 'none' && + attributeMap['visibility'] != 'hidden'; + } + + SvgAttributes _createSvgAttributes( + Map attributeMap, { + Color? currentColor, + }) { + final String? id = attributeMap['id']; + final double? opacity = + parseDouble(attributeMap['opacity'])?.clamp(0.0, 1.0); + final Color? color = + parseColor(attributeMap['color'], attributeName: 'color', id: id) ?? + currentColor; + + final String? rawX = attributeMap['x']; + final String? rawY = attributeMap['y']; + + final String? rawDx = attributeMap['dx']; + final String? rawDy = attributeMap['dy']; + + return SvgAttributes._( + raw: attributeMap, + id: id, + x: DoubleOrPercentage.fromString(rawX), + y: DoubleOrPercentage.fromString(rawY), + dx: DoubleOrPercentage.fromString(rawDx), + dy: DoubleOrPercentage.fromString(rawDy), + href: attributeMap['href'], + color: attributeMap['color']?.toLowerCase() == 'none' + ? const ColorOrNone.none() + : ColorOrNone.color(color), + stroke: _parseStrokeAttributes( + attributeMap, + opacity, + color, + id, + ), + fill: _parseFillAttributes( + attributeMap, + opacity, + color, + id, + ), + fillRule: parseRawFillRule(attributeMap['fill-rule']), + clipRule: parseRawFillRule(attributeMap['clip-rule']), + clipPathId: attributeMap['clip-path'], + blendMode: _blendModes[attributeMap['mix-blend-mode']], + transform: + parseTransform(attributeMap['transform']) ?? AffineMatrix.identity, + fontFamily: attributeMap['font-family'], + fontWeight: parseFontWeight(attributeMap['font-weight']), + fontSize: parseFontSize(attributeMap['font-size']), + textDecoration: parseTextDecoration(attributeMap['text-decoration']), + textDecorationStyle: + parseTextDecorationStyle(attributeMap['text-decoration-style']), + textDecorationColor: parseColor(attributeMap['text-decoration-color'], + attributeName: 'text-decoration-color', id: id), + textAnchorMultiplier: parseTextAnchor(attributeMap['text-anchor'])); + } +} + +/// A resolver is used by the parser and node tree to handle forward/backwards +/// references with identifiers. +class _Resolver { + /// A default empty identifier. + static const String emptyUrlIri = 'url(#)'; + final Map _drawables = {}; + final Map _shaders = {}; + final Map> _clips = >{}; + + bool _sealed = false; + + void _seal() { + assert(_deferredShaders.isEmpty); + _sealed = true; + } + + /// Retrieve the drawable defined by [ref]. + AttributedNode? getDrawable(String ref) { + assert(_sealed); + return _drawables[ref]; + } + + /// Retrieve the clip defined by [ref], or `null` if it is undefined. + List getClipPath(String ref) { + assert(_sealed); + final List? nodes = _clips[ref]; + if (nodes == null) { + return []; + } + + final List pathBuilders = []; + PathBuilder? currentPath; + void extractPathsFromNode(Node? target) { + if (target is PathNode) { + final PathBuilder nextPath = PathBuilder.fromPath(target.path); + nextPath.fillType = target.attributes.clipRule ?? PathFillType.nonZero; + if (currentPath != null && nextPath.fillType != currentPath!.fillType) { + currentPath = nextPath; + pathBuilders.add(currentPath!); + } else if (currentPath == null) { + currentPath = nextPath; + pathBuilders.add(currentPath!); + } else { + currentPath!.addPath(nextPath.toPath(reset: false)); + } + } else if (target is DeferredNode) { + extractPathsFromNode(target.resolver(target.refId)); + } else if (target is ParentNode) { + target.visitChildren(extractPathsFromNode); + } + } + + nodes.forEach(extractPathsFromNode); + + return pathBuilders + .map((PathBuilder builder) => builder.toPath()) + .toList(growable: false); + } + + /// Get the pattern id if one exists. + String? getPattern(SvgParser parserState) { + if (parserState.attribute('fill') != null) { + final String? fill = parserState.attribute('fill'); + if (fill!.startsWith('url') && parserState.patternIds.contains(fill)) { + return fill; + } + } + + if (parserState.attribute('stroke') != null) { + final String? stroke = parserState.attribute('stroke'); + if (stroke!.startsWith('url') && + parserState.patternIds.contains(stroke)) { + return stroke; + } + } + return null; + } + + /// Retrieve the [Gradeint] defined by [ref]. + T? getGradient(String ref) { + assert(_sealed); + return _shaders[ref] as T?; + } + + final Map> _deferredShaders = + >{}; + + /// Add a deferred [gradient] to the resolver, identified by [href]. + void addDeferredGradient(String ref, Gradient gradient) { + assert(!_sealed); + _deferredShaders.putIfAbsent(ref, () => []).add(gradient); + } + + /// Add the [gradient] to the resolver, identified by [href]. + void addGradient( + Gradient gradient, + String? href, + ) { + assert(!_sealed); + if (_shaders.containsKey(gradient.id)) { + return; + } + _shaders[gradient.id] = gradient; + if (href != null) { + href = 'url($href)'; + final Gradient? gradientRef = _shaders[href]; + if (gradientRef != null) { + // Gradient is defined after its reference. + _shaders[gradient.id] = gradient.applyProperties(gradientRef); + } else { + // Gradient is defined before its reference, check later when that + // reference has been parsed. + addDeferredGradient(href, gradient); + } + } else { + for (final Gradient deferred + in _deferredShaders.remove(gradient.id) ?? []) { + _shaders[deferred.id] = deferred.applyProperties(gradient); + } + } + } + + /// Add the clip defined by [pathNodes] to the resolver identifier by [ref]. + void addClipPath(String ref, List pathNodes) { + assert(!_sealed); + _clips.putIfAbsent(ref, () => pathNodes); + } + + /// Add the [drawable] to the resolver identifier by [ref]. + void addDrawable(String ref, AttributedNode drawable) { + assert(!_sealed); + _drawables.putIfAbsent(ref, () => drawable); + } +} + +class _Viewport { + const _Viewport(this.width, this.height, this.transform); + + final double width; + final double height; + final AffineMatrix transform; +} + +/// A collection of attributes for an SVG element. +class SvgAttributes { + /// Create a new [SvgAttributes] from the given properties. + const SvgAttributes._({ + required this.raw, + this.id, + this.href, + this.transform = AffineMatrix.identity, + this.color = const ColorOrNone.color(), + this.stroke, + this.fill, + this.fillRule, + this.clipRule, + this.clipPathId, + this.blendMode, + this.fontFamily, + this.fontWeight, + this.fontSize, + this.textDecoration, + this.textDecorationStyle, + this.textDecorationColor, + this.x, + this.dx, + this.textAnchorMultiplier, + this.y, + this.dy, + this.width, + this.height, + }); + + /// For use in tests to construct arbitrary attributes. + @visibleForTesting + const SvgAttributes.forTest({ + this.raw = const {}, + this.id, + this.href, + this.transform = AffineMatrix.identity, + this.color = const ColorOrNone.color(), + this.stroke, + this.fill, + this.fillRule, + this.clipRule, + this.clipPathId, + this.blendMode, + this.fontFamily, + this.fontWeight, + this.fontSize, + this.textDecoration, + this.textDecorationStyle, + this.textDecorationColor, + this.x, + this.dx, + this.textAnchorMultiplier, + this.y, + this.dy, + this.width, + this.height, + }); + + /// The empty set of properties. + static const SvgAttributes empty = SvgAttributes._(raw: {}); + + /// The raw attribute map. + final Map raw; + + /// Whether either the stroke or fill on this object has opacity. + bool get hasOpacity => (fill?.opacity ?? stroke?.opacity) != null; + + /// Generated from https://www.w3.org/TR/SVG11/single-page.html + /// + /// Using this: + /// ```javascript + /// let set = '{'; + /// document.querySelectorAll('.propdef') + /// .forEach((propdef) => { + /// const nameNode = propdef.querySelector('.propdef-title.prop-name'); + /// if (!nameNode) { + /// return; + /// } + /// const inherited = propdef.querySelector('tbody tr:nth-child(4) td:nth-child(2)').innerText.startsWith('yes'); + /// if (inherited) { + /// set += `'${nameNode.innerText.replaceAll(/[‘’]/g, '')}',`; + /// } + /// }); + /// set += '};'; + /// console.log(set); + /// ``` + static const Set _heritableProps = { + 'writing-mode', + 'glyph-orientation-vertical', + 'glyph-orientation-horizontal', + 'direction', + 'text-anchor', + 'font-family', + 'font-style', + 'font-variant', + 'font-weight', + 'font-stretch', + 'font-size', + 'font-size-adjust', + 'font', + 'kerning', + 'letter-spacing', + 'word-spacing', + 'fill', + 'fill-rule', + 'fill-opacity', + 'stroke', + 'stroke-width', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-opacity', + 'visibility', + 'marker-start', + 'marker', + 'color-interpolation', + 'color-interpolation-filters', + 'color-rendering', + 'shape-rendering', + 'text-rendering', + 'image-rendering', + 'color', + 'color-profile', + 'clip-rule', + 'pointer-events', + 'cursor', + }; + + /// The properties in [raw] that are heritable per the SVG 1.1 specification. + Iterable> get heritable { + return raw.entries.where((MapEntry entry) { + return _heritableProps.contains(entry.key); + }); + } + + /// The `@id` attribute. + final String? id; + + /// The `@href` attribute. + final String? href; + + /// The `@color` attribute, which provides an indirect current color. + /// + /// Does _not_ include the opacity value, which is specified on the [fill] or + /// [stroke]. + /// + /// https://www.w3.org/TR/SVG11/color.html#ColorProperty + final ColorOrNone color; + + /// The stroking properties of this element. + final SvgStrokeAttributes? stroke; + + /// The filling properties of this element. + final SvgFillAttributes? fill; + + /// The `@transform` attribute. + final AffineMatrix transform; + + /// The `@fill-rule` attribute. + final PathFillType? fillRule; + + /// The `@clip-rule` attribute. + final PathFillType? clipRule; + + /// The raw identifier for clip path(s) to apply. + final String? clipPathId; + + /// The `mix-blend-mode` attribute. + final BlendMode? blendMode; + + /// The `font-family` attribute, as a string. + final String? fontFamily; + + /// The font weight attribute. + final FontWeight? fontWeight; + + /// The `font-size` attribute. + final double? fontSize; + + /// The `text-decoration` attribute. + final TextDecoration? textDecoration; + + /// The `text-decoration-style` attribute. + final TextDecorationStyle? textDecorationStyle; + + /// The `text-decoration-color` attribute. + final Color? textDecorationColor; + + /// The `width` attribute. + final double? width; + + /// The `height` attribute. + final double? height; + + /// The x translation. + final DoubleOrPercentage? x; + + /// A multiplier for text-anchoring. + final double? textAnchorMultiplier; + + /// The y translation. + final DoubleOrPercentage? y; + + /// The relative x translation. + final DoubleOrPercentage? dx; + + /// The relative y translation. + final DoubleOrPercentage? dy; + + /// A copy of these attributes after absorbing a saveLayer. + /// + /// Specifically, this will null out `blendMode` and any opacity related + /// attributes, since those have been applied in a saveLayer call. + /// + /// The [raw] map preserves old values. + SvgAttributes forSaveLayer() { + return SvgAttributes._( + raw: raw, + id: id, + href: href, + transform: transform, + color: color, + stroke: stroke?.forSaveLayer(), + fill: fill?.forSaveLayer(), + fillRule: fillRule, + clipRule: clipRule, + clipPathId: clipPathId, + blendMode: blendMode, + fontFamily: fontFamily, + fontWeight: fontWeight, + fontSize: fontSize, + textDecoration: textDecoration, + textDecorationStyle: textDecorationStyle, + textDecorationColor: textDecorationColor, + x: x, + textAnchorMultiplier: textAnchorMultiplier, + y: y, + width: width, + height: height, + ); + } + + /// Creates a new set of attributes as if this inherited from `parent`. + /// + /// If `includePosition` is true, the `x`/`y` coordinates are also inherited. This + /// is intended to be used by text parsing. Defaults to `false`. + SvgAttributes applyParent( + SvgAttributes parent, { + bool includePosition = false, + AffineMatrix? transformOverride, + String? hrefOverride, + }) { + final Map newRaw = { + ...Map.fromEntries(parent.heritable), + if (includePosition && parent.raw.containsKey('x')) 'x': parent.raw['x']!, + if (includePosition && parent.raw.containsKey('y')) 'y': parent.raw['y']!, + ...raw, + }; + + return SvgAttributes._( + raw: newRaw, + id: newRaw['id'], + href: newRaw['href'], + transform: transformOverride ?? transform, + color: color._applyParent(parent.color), + stroke: stroke?.applyParent(parent.stroke) ?? parent.stroke, + fill: fill?.applyParent(parent.fill) ?? parent.fill, + fillRule: fillRule ?? parent.fillRule, + clipRule: clipRule ?? parent.clipRule, + clipPathId: clipPathId ?? parent.clipPathId, + blendMode: blendMode ?? parent.blendMode, + fontFamily: fontFamily ?? parent.fontFamily, + fontWeight: fontWeight ?? parent.fontWeight, + fontSize: fontSize ?? parent.fontSize, + textDecoration: textDecoration ?? parent.textDecoration, + textDecorationStyle: textDecorationStyle ?? parent.textDecorationStyle, + textDecorationColor: textDecorationColor ?? parent.textDecorationColor, + textAnchorMultiplier: textAnchorMultiplier ?? parent.textAnchorMultiplier, + height: height ?? parent.height, + width: width ?? parent.width, + x: x, + y: y, + dx: dx, + dy: dy, + ); + } +} + +/// A value that represents either an absolute pixel coordinate or a percentage +/// of a bounding dimension. +@immutable +class DoubleOrPercentage { + const DoubleOrPercentage._(this._value, this._isPercentage); + + /// Constructs a [DoubleOrPercentage] from a raw SVG attribute string. + static DoubleOrPercentage? fromString(String? raw) { + if (raw == null || raw == '') { + return null; + } + + if (isPercentage(raw)) { + return DoubleOrPercentage._(parsePercentage(raw), true); + } + return DoubleOrPercentage._(parseDouble(raw)!, false); + } + + final double _value; + final bool _isPercentage; + + /// Calculates the result of applying this dimension within [bound]. + /// + /// If this is a percentage, it will return a percentage of bound. Otherwise, + /// it returns the value of this. + double calculate(double bound) { + if (_isPercentage) { + return _value * bound; + } + return _value; + } + + @override + int get hashCode => Object.hash(_value, _isPercentage); + + @override + bool operator ==(Object other) { + return other is DoubleOrPercentage && + other._isPercentage == _isPercentage && + other._value == _value; + } +} + +/// SVG attributes specific to stroking. +class SvgStrokeAttributes { + const SvgStrokeAttributes._( + this._definitions, { + this.color = const ColorOrNone.color(), + this.shaderId, + this.join, + this.cap, + this.miterLimit, + this.width, + this.dashArray, + this.dashOffset, + this.hasPattern, + this.opacity, + }); + + final _Resolver? _definitions; + + /// The color to use for stroking. Does _not_ include the [opacity] value. Only + /// opacity is used if the [shaderId] is not null. + final ColorOrNone color; + + /// The literal reference to a shader defined elsewhere. + final String? shaderId; + + /// The join style to use for the stroke. + final StrokeJoin? join; + + /// The cap style to use for the stroke. + final StrokeCap? cap; + + /// The miter limit to use if the [join] is [StrokeJoin.miter]. + final double? miterLimit; + + /// The width of the stroke. + final double? width; + + /// The dashing array to use if the path is dashed. + final List? dashArray; + + /// The offset for [dashArray], if any. + final double? dashOffset; + + /// Indicates whether or not a pattern is used for stroke. + final bool? hasPattern; + + /// The opacity to apply to a default color, if [color] is null. + final double? opacity; + + /// A copy of these attributes after absorbing a saveLayer. + /// + /// Specifically, this will null out any opacity related + /// attributes, since those have been applied in a saveLayer call. + SvgStrokeAttributes forSaveLayer() { + return SvgStrokeAttributes._( + _definitions, + color: color, + shaderId: shaderId, + join: join, + cap: cap, + miterLimit: miterLimit, + width: width, + dashArray: dashArray, + dashOffset: dashOffset, + hasPattern: hasPattern, + ); + } + + /// Inherits attributes in this from parent. + SvgStrokeAttributes applyParent(SvgStrokeAttributes? parent) { + return SvgStrokeAttributes._( + _definitions, + color: color._applyParent(parent?.color), + shaderId: shaderId ?? parent?.shaderId, + join: join ?? parent?.join, + cap: cap ?? parent?.cap, + miterLimit: miterLimit ?? parent?.miterLimit, + width: width ?? parent?.width, + dashArray: dashArray ?? parent?.dashArray, + dashOffset: dashOffset ?? parent?.dashOffset, + hasPattern: hasPattern ?? parent?.hasPattern, + opacity: opacity ?? parent?.opacity, + ); + } + + /// Creates a stroking paint object from this set of attributes, using the + /// bounds and transform specified for shader computation. + /// + /// Returns null if this is [none]. + Stroke? toStroke(Rect shaderBounds, AffineMatrix transform) { + // A zero width stroke is a hairline in Flutter, but a nop in SVG. + if (color.isNone || + (color.color == null && hasPattern == null && shaderId == null || + width == 0)) { + return null; + } + + if (hasPattern ?? false) { + return Stroke( + join: join, + cap: cap, + miterLimit: miterLimit, + width: width, + ); + } + + if (_definitions == null) { + return null; + } + + Gradient? shader; + if (shaderId != null) { + shader = _definitions + .getGradient(shaderId!) + ?.applyBounds(shaderBounds, transform); + if (shader == null) { + return null; + } + } + + return Stroke( + color: color.color!.withOpacity(opacity ?? 1.0), + shader: shader, + join: join, + cap: cap, + miterLimit: miterLimit, + width: transform.scaleStrokeWidth(width), + ); + } +} + +/// SVG attributes specific to filling. +class SvgFillAttributes { + /// Create a new [SvgFillAttributes]; + const SvgFillAttributes._( + this._definitions, { + this.color = const ColorOrNone.color(), + this.shaderId, + this.hasPattern, + this.opacity, + }); + + final _Resolver? _definitions; + + /// The color to use for filling. Does _not_ include the [opacity] value. Only + /// opacity is used if the [shaderId] is not null. + final ColorOrNone color; + + /// The opacity to apply to a default color, if [color] is null. + final double? opacity; + + /// The literal reference to a shader defined elsewhere. + final String? shaderId; + + /// If there is a pattern a default fill will be returned. + final bool? hasPattern; + + /// A copy of these attributes after absorbing a saveLayer. + /// + /// Specifically, this will null out any opacity related + /// attributes, since those have been applied in a saveLayer call. + SvgFillAttributes forSaveLayer() { + return SvgFillAttributes._( + _definitions, + color: color, + shaderId: shaderId, + hasPattern: hasPattern, + ); + } + + /// Inherits attributes in this from parent. + SvgFillAttributes applyParent(SvgFillAttributes? parent) { + return SvgFillAttributes._( + _definitions, + color: color._applyParent(parent?.color), + shaderId: shaderId ?? parent?.shaderId, + hasPattern: hasPattern ?? parent?.hasPattern, + opacity: opacity ?? parent?.opacity, + ); + } + + /// Creates a [Fill] from this information with appropriate transforms and + /// bounds for shaders. + /// + /// Returns null if this is [none]. + Fill? toFill( + Rect shaderBounds, + AffineMatrix transform, { + Color? defaultColor, + }) { + if (color.isNone) { + return null; + } + final Color? resolvedColor = color.color?.withOpacity(opacity ?? 1.0) ?? + defaultColor?.withOpacity(opacity ?? 1.0); + if (resolvedColor == null) { + return null; + } + if (hasPattern ?? false) { + return Fill(color: resolvedColor); + } + + if (_definitions == null) { + return null; + } + Gradient? shader; + if (shaderId != null) { + shader = _definitions + .getGradient(shaderId!) + ?.applyBounds(shaderBounds, transform); + if (shader == null) { + return null; + } + } + + return Fill(color: resolvedColor, shader: shader); + } + + @override + String toString() { + return 'SvgFillAttributes(' + 'definitions: $_definitions, ' + 'color: $color, ' + 'shaderId: $shaderId, ' + 'hasPattern: $hasPattern, ' + 'oapctiy: $opacity)'; + } +} + +/// Represents a color for filling or stroking. +/// +/// If the [color] is not null, [isNone] is false. +/// +/// If the [color] is null and [isNone] is false, color should be inherited from +/// the parent or defaulted as per the SVG specification. +/// +/// If the [color] is null and [isNone] is true, inheritence should be cleared +/// and no painting should happen. +class ColorOrNone { + /// See [ColorOrNone]. + const ColorOrNone.none() + : isNone = true, + color = null; + + /// See [ColorOrNone]. + const ColorOrNone.color([this.color]) : isNone = false; + + /// Whether to paint anything. + final bool isNone; + + /// The color to use when painting. If null and [isNone] is false, inherit + /// from parent. + final Color? color; + + ColorOrNone _applyParent(ColorOrNone? parent) { + if (parent == null || isNone) { + return this; + } + + if (parent.isNone && color == null) { + return const ColorOrNone.none(); + } + + return ColorOrNone.color(color ?? parent.color); + } + + @override + String toString() => isNone ? '"none"' : (color?.toString() ?? 'null'); +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/parsers.dart b/packages/vector_graphics_compiler/lib/src/svg/parsers.dart new file mode 100644 index 00000000000..6dcba4b2be8 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/parsers.dart @@ -0,0 +1,194 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import '../geometry/matrix.dart'; +import '../geometry/path.dart'; +import 'node.dart'; +import 'numbers.dart'; + +const String _transformCommandAtom = r' *,?([^(]+)\(([^)]*)\)'; +final RegExp _transformValidator = RegExp('^($_transformCommandAtom)*\$'); +final RegExp _transformCommand = RegExp(_transformCommandAtom); + +typedef _MatrixParser = AffineMatrix Function( + List params, AffineMatrix current); + +const Map _matrixParsers = { + 'matrix': _parseSvgMatrix, + 'translate': _parseSvgTranslate, + 'scale': _parseSvgScale, + 'rotate': _parseSvgRotate, + 'skewX': _parseSvgSkewX, + 'skewY': _parseSvgSkewY, +}; + +List _parseTransformParams(String params) { + final List result = []; + String current = ''; + for (int i = 0; i < params.length; i += 1) { + final String char = params[i]; + final bool isSeparator = char == ' ' || char == '-' || char == ','; + final bool isExponent = i > 0 && params[i - 1].toLowerCase() == 'e'; + if (isSeparator && !isExponent) { + if (current != '') { + result.add(parseDouble(current)!); + } + if (char == '-') { + current = '-'; + } else { + current = ''; + } + } else { + if (char == '.') { + if (current.contains('.')) { + result.add(parseDouble(current)!); + current = ''; + } + } + current += char; + } + } + if (current.isNotEmpty) { + result.add(parseDouble(current)!); + } + return result; +} + +/// Parses a SVG transform attribute into a [AffineMatrix]. +AffineMatrix? parseTransform(String? transform) { + if (transform == null || transform == '') { + return null; + } + + if (!_transformValidator.hasMatch(transform)) { + throw StateError('illegal or unsupported transform: $transform'); + } + final Iterable matches = + _transformCommand.allMatches(transform).toList().reversed; + AffineMatrix result = AffineMatrix.identity; + for (final Match m in matches) { + final String command = m.group(1)!.trim(); + final List params = _parseTransformParams(m.group(2)!.trim()); + + final _MatrixParser? transformer = _matrixParsers[command]; + if (transformer == null) { + throw StateError('Unsupported transform: $command'); + } + + result = transformer(params, result); + } + return result; +} + +AffineMatrix _parseSvgMatrix(List params, AffineMatrix current) { + assert(params.isNotEmpty); + assert(params.length == 6); + final double a = params[0]; + final double b = params[1]; + final double c = params[2]; + final double d = params[3]; + final double e = params[4]; + final double f = params[5]; + + return AffineMatrix(a, b, c, d, e, f).multiplied(current); +} + +AffineMatrix _parseSvgSkewX(List params, AffineMatrix current) { + assert(params.isNotEmpty); + return AffineMatrix(1.0, 0.0, tan(params.first), 1.0, 0.0, 0.0) + .multiplied(current); +} + +AffineMatrix _parseSvgSkewY(List params, AffineMatrix current) { + assert(params.isNotEmpty); + return AffineMatrix(1.0, tan(params.first), 0.0, 1.0, 0.0, 0.0) + .multiplied(current); +} + +AffineMatrix _parseSvgTranslate(List params, AffineMatrix current) { + assert(params.isNotEmpty); + assert(params.length <= 2); + final double y = params.length < 2 ? 0.0 : params[1]; + return AffineMatrix(1.0, 0.0, 0.0, 1.0, params.first, y).multiplied(current); +} + +AffineMatrix _parseSvgScale(List params, AffineMatrix current) { + assert(params.isNotEmpty); + assert(params.length <= 2); + final double x = params[0]; + final double y = params.length < 2 ? x : params[1]; + return AffineMatrix(x, 0.0, 0.0, y, 0.0, 0.0).multiplied(current); +} + +AffineMatrix _parseSvgRotate(List params, AffineMatrix current) { + assert(params.length <= 3); + final double a = radians(params[0]); + + final AffineMatrix rotate = AffineMatrix.identity.rotated(a); + + if (params.length > 1) { + final double x = params[1]; + final double y = params.length == 3 ? params[2] : x; + return AffineMatrix(1.0, 0.0, 0.0, 1.0, x, y) + .multiplied(rotate) + .translated(-x, -y) + .multiplied(current); + } else { + return rotate.multiplied(current); + } +} + +/// Parses a `fill-rule` attribute. +PathFillType? parseRawFillRule(String? rawFillRule) { + if (rawFillRule == 'inherit' || rawFillRule == null) { + return null; + } + + return rawFillRule != 'evenodd' ? PathFillType.nonZero : PathFillType.evenOdd; +} + +/// 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('%') ?? false; + +/// Parses value from the form '25%', 0.25 or 25.0 as a double. +/// Note: Percentage or decimals will be multiplied by the total +/// view box size, where as doubles will be returned as is. +double? parsePatternUnitToDouble(String rawValue, String mode, + {ViewportNode? viewBox}) { + double? value; + double? viewBoxValue; + if (viewBox != null) { + if (mode == 'width') { + viewBoxValue = viewBox.width; + } else if (mode == 'height') { + viewBoxValue = viewBox.height; + } + } + + if (rawValue.contains('%')) { + value = ((double.parse(rawValue.substring(0, rawValue.length - 1))) / 100) * + viewBoxValue!; + } else if (rawValue.startsWith('0.')) { + value = (double.parse(rawValue)) * viewBoxValue!; + } else if (rawValue.isNotEmpty) { + value = double.parse(rawValue); + } + return value; +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/path_ops.dart b/packages/vector_graphics_compiler/lib/src/svg/path_ops.dart new file mode 100644 index 00000000000..9b0e67e6dd3 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/path_ops.dart @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Copied from flutter/engine repository: https://github.com/flutter/engine/tree/main/tools/path_ops +// NOTE: For now, this copy and flutter/engine copy should be kept in sync. + +import '_path_ops_unsupported.dart' if (dart.library.ffi) '_path_ops_ffi.dart' + as impl; +export '_path_ops_unsupported.dart' if (dart.library.ffi) '_path_ops_ffi.dart'; + +// ignore_for_file: camel_case_types, non_constant_identifier_names + +/// Initialize the libpathops dynamic library. +void initializeLibPathOps(String path) => impl.initializeLibPathOps(path); + +/// Determines the winding rule that decides how the interior of a Path is +/// calculated. +/// +/// This enum is used by the [Path] constructor +// must match ordering in //third_party/skia/include/core/SkPathTypes.h +enum FillType { + /// The interior is defined by a non-zero sum of signed edge crossings. + nonZero, + + /// The interior is defined by an odd number of edge crossings. + evenOdd, +} + +/// A set of operations applied to two paths. +// Sync with //third_party/skia/include/pathops/SkPathOps.h +enum PathOp { + /// Subtracts the second path from the first. + difference, + + /// Creates a new path representing the intersection of the first and second. + intersect, + + /// Creates a new path representing the union of the first and second + /// (includive-or). + union, + + /// Creates a new path representing the exclusive-or of two paths. + xor, + + /// Creates a new path that subtracts the first path from the second.s + reversedDifference, +} + +/// The commands used in a [Path] object. +/// +/// This enumeration is a subset of the commands that SkPath supports. +// Sync with //third_party/skia/include/core/SkPathTypes.h +enum PathVerb { + /// Picks up the pen and moves it without drawing. Uses two point values. + moveTo, + + /// A straight line from the current point to the specified point. + lineTo, + + /// A quadratic bezier curve from the current point. + /// + /// The next two points are the control point. The next two points after + /// that are the target point. + quadTo, + + /// A cubic bezier curve from the current point. + /// + /// The next two points are used as the first control point. The next two + /// points form the second control point. The next two points form the + /// target point. + cubicTo, + + /// A straight line from the current point to the last [moveTo] point. + close, +} + +/// A proxy class for [Path.replay]. +/// +/// Allows implementations to easily inspect the contents of a [Path]. +abstract class PathProxy { + /// Picks up the pen and moves to absolute coordinates x,y. + void moveTo(double x, double y); + + /// Draws a straight line from the current point to absolute coordinates x,y. + void lineTo(double x, double y); + + /// Creates a cubic Bezier curve from the current point to point x3,y3 using + /// x1,y1 as the first control point and x2,y2 as the second. + void cubicTo( + double x1, double y1, double x2, double y2, double x3, double y3); + + /// Draws a straight line from the current point to the last [moveTo] point. + void close(); + + /// Called by [Path.replay] to indicate that a new path is being played. + void reset() {} +} + +/// A path proxy that can print the SVG path-data representation of this path. +class SvgPathProxy implements PathProxy { + final StringBuffer _buffer = StringBuffer(); + + @override + void reset() { + _buffer.clear(); + } + + @override + void close() { + _buffer.write('Z'); + } + + @override + void cubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _buffer.write('C$x1,$y1 $x2,$y2 $x3,$y3'); + } + + @override + void lineTo(double x, double y) { + _buffer.write('L$x,$y'); + } + + @override + void moveTo(double x, double y) { + _buffer.write('M$x,$y'); + } + + @override + String toString() => _buffer.toString(); +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/resolver.dart b/packages/vector_graphics_compiler/lib/src/svg/resolver.dart new file mode 100644 index 00000000000..76f34b43c35 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/resolver.dart @@ -0,0 +1,567 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import '../geometry/basic_types.dart'; +import '../geometry/matrix.dart'; +import '../geometry/path.dart'; +import '../geometry/vertices.dart'; +import '../image/image_info.dart'; +import '../paint.dart'; +import 'node.dart'; +import 'parser.dart'; +import 'visitor.dart'; + +/// A visitor class that processes relative coordinates in the tree into a +/// single coordinate space, removing extra attributes, empty nodes, resolving +/// references/masks/clips. +class ResolvingVisitor extends Visitor { + late Rect _bounds; + + @override + Node visitClipNode(ClipNode clipNode, AffineMatrix data) { + final AffineMatrix childTransform = clipNode.concatTransform(data); + final List transformedClips = [ + for (final Path clip in clipNode.resolver(clipNode.clipId)) + clip.transformed(childTransform) + ]; + if (transformedClips.isEmpty) { + return clipNode.child.accept(this, data); + } + return ResolvedClipNode( + clips: transformedClips, + child: clipNode.child.accept(this, data), + ); + } + + @override + Node visitMaskNode(MaskNode maskNode, AffineMatrix data) { + final AttributedNode? resolvedMask = maskNode.resolver(maskNode.maskId); + if (resolvedMask == null) { + return maskNode.child.accept(this, data); + } + final Node child = maskNode.child.accept(this, data); + final AffineMatrix childTransform = maskNode.concatTransform(data); + final Node mask = resolvedMask.accept(this, childTransform); + + return ResolvedMaskNode( + child: child, + mask: mask, + blendMode: maskNode.blendMode, + ); + } + + @override + Node visitParentNode(ParentNode parentNode, AffineMatrix data) { + final AffineMatrix nextTransform = parentNode.concatTransform(data); + + final Paint? saveLayerPaint = parentNode.createLayerPaint(); + + final Node result; + if (saveLayerPaint == null) { + result = ParentNode( + SvgAttributes.empty, + precalculatedTransform: AffineMatrix.identity, + children: [ + for (final Node child in parentNode.children) + child + .applyAttributes(parentNode.attributes) + .accept(this, nextTransform), + ], + ); + } else { + result = SaveLayerNode( + SvgAttributes.empty, + paint: saveLayerPaint, + children: [ + for (final Node child in parentNode.children) + child + .applyAttributes(parentNode.attributes.forSaveLayer()) + .accept(this, nextTransform), + ], + ); + } + return result; + } + + @override + Node visitPathNode(PathNode pathNode, AffineMatrix data) { + final AffineMatrix transform = data.multiplied( + pathNode.attributes.transform, + ); + final Path transformedPath = pathNode.path + .transformed(transform) + .withFillType(pathNode.attributes.fillRule ?? pathNode.path.fillType); + final Rect originalBounds = pathNode.path.bounds(); + final Rect newBounds = transformedPath.bounds(); + final Paint? paint = pathNode.computePaint(originalBounds, transform); + if (paint != null) { + if (pathNode.attributes.stroke?.dashArray != null) { + final List children = []; + final ParentNode parent = ParentNode( + pathNode.attributes, + children: children, + ); + if (paint.fill != null) { + children.add(ResolvedPathNode( + paint: Paint(blendMode: paint.blendMode, fill: paint.fill), + bounds: newBounds, + path: transformedPath, + )); + } + if (paint.stroke != null) { + children.add(ResolvedPathNode( + paint: Paint(blendMode: paint.blendMode, stroke: paint.stroke), + bounds: newBounds, + path: transformedPath.dashed( + pathNode.attributes.stroke!.dashArray!, + ), + )); + } + return parent; + } + return ResolvedPathNode( + paint: paint, + bounds: newBounds, + path: transformedPath, + ); + } + return Node.empty; + } + + @override + Node visitTextPositionNode( + TextPositionNode textPositionNode, + AffineMatrix data, + ) { + final AffineMatrix nextTransform = textPositionNode.concatTransform(data); + + return ResolvedTextPositionNode( + textPositionNode.computeTextPosition(_bounds, data), + [ + for (final Node child in textPositionNode.children) + child + .applyAttributes(textPositionNode.attributes) + .accept(this, nextTransform), + ], + ); + } + + @override + Node visitTextNode(TextNode textNode, AffineMatrix data) { + final Paint? paint = textNode.computePaint(_bounds, data); + final TextConfig textConfig = textNode.computeTextConfig(_bounds, data); + + if (paint != null && textConfig.text.trim().isNotEmpty) { + return ResolvedTextNode( + textConfig: textConfig, + paint: paint, + ); + } + return Node.empty; + } + + @override + Node visitViewportNode(ViewportNode viewportNode, AffineMatrix data) { + _bounds = Rect.fromLTWH(0, 0, viewportNode.width, viewportNode.height); + final AffineMatrix transform = viewportNode.concatTransform(data); + return ViewportNode( + SvgAttributes.empty, + width: viewportNode.width, + height: viewportNode.height, + transform: AffineMatrix.identity, + children: [ + for (final Node child in viewportNode.children) + child + .applyAttributes(viewportNode.attributes) + .accept(this, transform), + ], + ); + } + + @override + Node visitDeferredNode(DeferredNode deferredNode, AffineMatrix data) { + final AttributedNode? resolvedNode = + deferredNode.resolver(deferredNode.refId); + if (resolvedNode == null) { + return Node.empty; + } + final Node concreteRef = resolvedNode.applyAttributes( + deferredNode.attributes, + replace: true, + ); + return concreteRef.accept(this, data); + } + + @override + Node visitEmptyNode(Node node, AffineMatrix data) => node; + + @override + Node visitResolvedText(ResolvedTextNode textNode, AffineMatrix data) { + assert(false); + return textNode; + } + + @override + Node visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, AffineMatrix data) { + assert(false); + return textPositionNode; + } + + @override + Node visitResolvedPath(ResolvedPathNode pathNode, AffineMatrix data) { + assert(false); + return pathNode; + } + + @override + Node visitResolvedClipNode(ResolvedClipNode clipNode, AffineMatrix data) { + assert(false); + return clipNode; + } + + @override + Node visitResolvedMaskNode(ResolvedMaskNode maskNode, AffineMatrix data) { + assert(false); + return maskNode; + } + + @override + Node visitSaveLayerNode(SaveLayerNode layerNode, AffineMatrix data) { + assert(false); + return layerNode; + } + + @override + Node visitResolvedVerticesNode( + ResolvedVerticesNode verticesNode, AffineMatrix data) { + assert(false); + return verticesNode; + } + + @override + Node visitImageNode(ImageNode imageNode, AffineMatrix data) { + final AffineMatrix childTransform = imageNode.concatTransform(data); + + final SvgAttributes attributes = imageNode.attributes; + final double left = double.parse(attributes.raw['x'] ?? '0'); + final double top = double.parse(attributes.raw['y'] ?? '0'); + + double? width = double.tryParse(attributes.raw['width'] ?? ''); + double? height = double.tryParse(attributes.raw['height'] ?? ''); + if (width == null || height == null) { + final ImageSizeData data = ImageSizeData.fromBytes(imageNode.data); + width ??= data.width.toDouble(); + height ??= data.height.toDouble(); + } + final Rect rect = Rect.fromLTWH(left, top, width, height); + + // Determine if this image can be drawn without any transforms because + // it only has an offset and/or scale. + if (childTransform.encodableInRect) { + // trivial transform. + return ResolvedImageNode( + data: imageNode.data, + format: imageNode.format, + rect: childTransform.transformRect(rect), + transform: null, + ); + } + + // Non-trivial transform. + return ResolvedImageNode( + data: imageNode.data, + format: imageNode.format, + rect: rect, + transform: childTransform, + ); + } + + @override + Node visitResolvedImageNode( + ResolvedImageNode resolvedImageNode, AffineMatrix data) { + assert(false); + return resolvedImageNode; + } + + @override + Node visitPatternNode(PatternNode node, AffineMatrix data) { + final AttributedNode? resolvedPattern = node.resolver(node.patternId); + if (resolvedPattern == null) { + return node.child.accept(this, data); + } + final Node child = node.child.accept(this, data); + final AffineMatrix childTransform = node.concatTransform(data); + final Node pattern = resolvedPattern.accept(this, childTransform); + + return ResolvedPatternNode( + child: child, + pattern: pattern, + x: resolvedPattern.attributes.x?.calculate(0) ?? 0, + y: resolvedPattern.attributes.y?.calculate(0) ?? 0, + width: resolvedPattern.attributes.width!, + height: resolvedPattern.attributes.height!, + transform: data, + id: node.patternId, + ); + } + + @override + Node visitResolvedPatternNode( + ResolvedPatternNode patternNode, AffineMatrix data) { + assert(false); + return patternNode; + } +} + +/// A text position update that is final and has a fully known transform. +/// +/// Constructed from a [TextPositionNode] by a [ResolvingVisitor]. +class ResolvedTextPositionNode extends Node { + /// Create a new [ResolvedTextPositionNode]. + ResolvedTextPositionNode(this.textPosition, this.children); + + /// The resolved [TextPosition]. + final TextPosition textPosition; + + /// The children of this node. + final List children; + + @override + void visitChildren(NodeCallback visitor) { + children.forEach(visitor); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedTextPositionNode(this, data); + } +} + +/// A block of text that has its position and final transfrom fully known. +/// +/// This should only be constructed from a [TextNode] in a [ResolvingVisitor]. +class ResolvedTextNode extends Node { + /// Create a new [ResolvedTextNode]. + ResolvedTextNode({ + required this.textConfig, + required this.paint, + }); + + /// The text configuration to draw this piece of text. + final TextConfig textConfig; + + /// The paint used to draw this piece of text. + final Paint paint; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedText(this, data); + } + + @override + void visitChildren(NodeCallback visitor) {} +} + +/// A path node that has its bounds fully computed. +/// +/// This should only be constructed from a [PathNode] in a [ResolvingVisitor]. +class ResolvedPathNode extends Node { + /// Create a new [ResolvedPathNode]. + ResolvedPathNode({ + required this.paint, + required this.bounds, + required this.path, + }); + + /// The paint for the current path node. + final Paint paint; + + /// The bounds estimate for the current path. + final Rect bounds; + + /// The path to be drawn. + final Path path; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedPath(this, data); + } + + @override + void visitChildren(NodeCallback visitor) {} +} + +/// A node that draws resolved vertices. +class ResolvedVerticesNode extends Node { + /// Create a new [ResolvedVerticesNode] + ResolvedVerticesNode({ + required this.paint, + required this.vertices, + required this.bounds, + }) : assert(paint.stroke == null); + + /// The paint (fill only) to draw on the given node. + final Paint paint; + + /// The vertices to be drawn. + final IndexedVertices vertices; + + /// The original bounds of the path that created this node. + final Rect bounds; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedVerticesNode(this, data); + } + + @override + void visitChildren(NodeCallback visitor) {} +} + +/// A clip node where all paths are known and transformed in a single +/// coordinate space. +/// +/// This should only be constructed from a [ClipNode] in a [ResolvingVisitor]. +class ResolvedClipNode extends Node { + /// Create a new [ResolvedClipNode]. + ResolvedClipNode({ + required this.clips, + required this.child, + }); + + /// One or more clips to apply to rendered children. + final List clips; + + /// The child node. + final Node child; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedClipNode(this, data); + } + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } +} + +/// A mask node with child and mask fully resolved. +/// +/// This should only be constructed from a [MaskNode] in a [ResolvingVisitor]. +class ResolvedMaskNode extends Node { + /// Create a new [ResolvedMaskNode]. + ResolvedMaskNode({ + required this.child, + required this.mask, + required this.blendMode, + }); + + /// The child to apply as a mask. + final Node mask; + + /// The child of this mask layer. + final Node child; + + /// The blend mode to apply when saving a layer for the mask, if any. + final BlendMode? blendMode; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedMaskNode(this, data); + } + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } +} + +/// An image node that has a fully resolved position and data. +class ResolvedImageNode extends Node { + /// Create a new [ResolvedImageNode]. + const ResolvedImageNode({ + required this.data, + required this.format, + required this.rect, + required this.transform, + }); + + /// The image [data] encoded as a PNG. + final Uint8List data; + + /// The format of [data]. + final ImageFormat format; + + /// The region to draw the image to. + final Rect rect; + + /// An optional transform. + /// + /// This is set when the accumulated image transform causes the image rect + /// to not stay rectangular. + final AffineMatrix? transform; + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedImageNode(this, data); + } + + @override + void visitChildren(NodeCallback visitor) {} +} + +/// A pattern node that has a fully resolved position and data. +class ResolvedPatternNode extends Node { + /// Creates a new [ResolvedPatternNode]. + + ResolvedPatternNode({ + required this.child, + required this.pattern, + required this.width, + required this.x, + required this.y, + required this.height, + required this.transform, + required this.id, + }); + + /// The child to apply a pattern to. + final Node child; + + /// A node that represents the pattern. + final Node pattern; + + /// The x coordinate shift of the pattern tile. + final double x; + + /// The y coordinate shift of the pattern tile. + final double y; + + /// The width of the pattern's viewbox in px. + /// Values must be > = 1. + final double width; + + /// The height of the pattern's viewbox in px. + /// Values must be > = 1. + final double height; + + /// A unique identifier for the [pattern]. + final Object id; + + /// This is the transform of the pattern that has been created from the children. + AffineMatrix transform; + + @override + void visitChildren(NodeCallback visitor) { + visitor(child); + } + + @override + S accept(Visitor visitor, V data) { + return visitor.visitResolvedPatternNode(this, data); + } +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/tessellator.dart b/packages/vector_graphics_compiler/lib/src/svg/tessellator.dart new file mode 100644 index 00000000000..feaea57e57e --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/tessellator.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '_tessellator_unsupported.dart' + if (dart.library.ffi) '_tessellator_ffi.dart' as impl; +import 'node.dart'; +import 'visitor.dart'; + +/// Whether or not tesselation should be used. +bool get isTesselatorInitialized => impl.isTesselatorInitialized; + +/// Initialize the libtesselator dynamic library. +/// +/// This method must be called before [VerticesBuilder] can be used or +/// constructed. +void initializeLibTesselator(String path) => impl.initializeLibTesselator(path); + +/// Information about how to approximate points on a curved path segment. +/// +/// In particular, the values in this object control how many vertices to +/// generate when approximating curves, and what tolerances to use when +/// calculating the sharpness of curves. +/// +/// Used by [VerticesBuilder.tessellate]. +class SmoothingApproximation { + /// Creates a new smoothing approximation instance with default values. + const SmoothingApproximation({ + this.scale = 1.0, + this.angleTolerance = 0.0, + this.cuspLimit = 0.0, + }); + + /// The scaling coefficient to use when translating to screen coordinates. + /// + /// Values approaching 0.0 will generate smoother looking curves with a + /// greater number of vertices, and will be more expensive to calculate. + final double scale; + + /// The tolerance value in radians for calculating sharp angles. + /// + /// Values approaching 0.0 will provide more accurate approximation of sharp + /// turns. A 0.0 vlaue means angle conditions are not considered at all. + final double angleTolerance; + + /// An angle in radians at which to introduce bevel cuts. + /// + /// Values greater than zero will restirct the sharpness of bevel cuts on + /// turns. + final double cuspLimit; +} + +/// A visitor that replaces fill paths with tesselated vertices. +abstract class Tessellator extends Visitor { + /// Create a new [Tessellator] visitor. + factory Tessellator() = impl.Tessellator; +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/theme.dart b/packages/vector_graphics_compiler/lib/src/svg/theme.dart new file mode 100644 index 00000000000..d82ed511ccc --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/theme.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import '../paint.dart'; + +/// A theme used when decoding an SVG picture. +@immutable +class SvgTheme { + /// Instantiates an SVG theme with the [currentColor] + /// and [fontSize]. + /// + /// Defaults the [fontSize] to 14. + const SvgTheme({ + this.currentColor = Color.opaqueBlack, + this.fontSize = 14, + double? xHeight, + }) : xHeight = xHeight ?? fontSize / 2; + + /// The default color applied to SVG elements that inherit the color property. + /// See: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword + final Color currentColor; + + /// The font size used when calculating em units of SVG elements. + /// See: https://www.w3.org/TR/SVG11/coords.html#Units + final double fontSize; + + /// The x-height (corpus size) of the font used when calculating ex units of SVG elements. + /// Defaults to [fontSize] / 2 if not provided. + /// See: https://www.w3.org/TR/SVG11/coords.html#Units, https://en.wikipedia.org/wiki/X-height + final double xHeight; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is SvgTheme && + currentColor == other.currentColor && + fontSize == other.fontSize && + xHeight == other.xHeight; + } + + @override + int get hashCode => Object.hash(currentColor, fontSize, xHeight); + + @override + String toString() => + 'SvgTheme(currentColor: $currentColor, fontSize: $fontSize, xHeight: $xHeight)'; +} diff --git a/packages/vector_graphics_compiler/lib/src/svg/visitor.dart b/packages/vector_graphics_compiler/lib/src/svg/visitor.dart new file mode 100644 index 00000000000..b92d3746a42 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/visitor.dart @@ -0,0 +1,242 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../draw_command_builder.dart'; +import '../geometry/path.dart'; +import '../paint.dart'; +import '../vector_instructions.dart'; +import 'node.dart'; +import 'resolver.dart'; + +/// A visitor implementation used to process the tree. +abstract class Visitor { + /// Const constructor so subclasses can be const. + const Visitor(); + + /// Visit a [ViewportNode]. + S visitViewportNode(ViewportNode viewportNode, V data); + + /// Visit a [MaskNode]. + S visitMaskNode(MaskNode maskNode, V data); + + /// Visit a [ClipNode]. + S visitClipNode(ClipNode clipNode, V data); + + /// Visit a [TextPositionNode]. + S visitTextPositionNode(TextPositionNode textPositionNode, V data); + + /// Visit a [TextNode]. + S visitTextNode(TextNode textNode, V data); + + /// VIsit an [ImageNode]. + S visitImageNode(ImageNode imageNode, V data); + + /// Visit a [PathNode]. + S visitPathNode(PathNode pathNode, V data); + + /// Visit a [ParentNode]. + S visitParentNode(ParentNode parentNode, V data); + + /// Visit a [DeferredNode]. + S visitDeferredNode(DeferredNode deferredNode, V data); + + /// Visit a [Node] that has no meaningful content. + S visitEmptyNode(Node node, V data); + + /// Visit a [PatternNode]. + S visitPatternNode(PatternNode node, V data); + + /// Visit a [ResolvedTextPositionNode]. + S visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, V data); + + /// Visit a [ResolvedTextNode]. + S visitResolvedText(ResolvedTextNode textNode, V data); + + /// Visit a [ResolvedPathNode]. + S visitResolvedPath(ResolvedPathNode pathNode, V data); + + /// Visit a [ResolvedClipNode]. + S visitResolvedClipNode(ResolvedClipNode clipNode, V data); + + /// Visit a [ResolvedMaskNode]. + S visitResolvedMaskNode(ResolvedMaskNode maskNode, V data); + + /// Visit a [ResolvedImageNode]. + S visitResolvedImageNode(ResolvedImageNode resolvedImageNode, V data); + + /// Visit a [SaveLayerNode]. + S visitSaveLayerNode(SaveLayerNode layerNode, V data); + + /// Visit a [ResolvedVerticesNode]. + S visitResolvedVerticesNode(ResolvedVerticesNode verticesNode, V data); + + /// Visit a [ResolvedPatternNode]. + S visitResolvedPatternNode(ResolvedPatternNode patternNode, V data); +} + +/// A mixin that can be applied to a [Visitor] that makes visiting an +/// unreloved [Node] an error. +mixin ErrorOnUnResolvedNode on Visitor { + String get _message => 'Cannot visit unresolved nodes with $this'; + + @override + S visitDeferredNode(DeferredNode deferredNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitMaskNode(MaskNode maskNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitClipNode(ClipNode clipNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitTextPositionNode(TextPositionNode textPositionNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitTextNode(TextNode textNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitPathNode(PathNode pathNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitImageNode(ImageNode imageNode, V data) { + throw UnsupportedError(_message); + } + + @override + S visitPatternNode(PatternNode patternNode, V data) { + throw UnsupportedError(_message); + } +} + +/// A visitor that builds up a [VectorInstructions] for binary encoding. +class CommandBuilderVisitor extends Visitor + with ErrorOnUnResolvedNode { + final DrawCommandBuilder _builder = DrawCommandBuilder(); + late double _width; + late double _height; + + /// The current patternId. This will be `null` if + /// there is no current pattern. + Object? currentPatternId; + + /// Return the vector instructions encoded by the visitor given to this tree. + VectorInstructions toInstructions() { + return _builder.toInstructions(_width, _height); + } + + @override + void visitEmptyNode(Node node, void data) {} + + @override + void visitParentNode(ParentNode parentNode, void data) { + for (final Node child in parentNode.children) { + child.accept(this, data); + } + } + + @override + void visitPathNode(PathNode pathNode, void data) { + assert(false); + } + + @override + void visitResolvedClipNode(ResolvedClipNode clipNode, void data) { + for (final Path clip in clipNode.clips) { + _builder.addClip(clip); + clipNode.child.accept(this, data); + _builder.restore(); + } + } + + @override + void visitResolvedMaskNode(ResolvedMaskNode maskNode, void data) { + _builder.addSaveLayer(Paint( + blendMode: maskNode.blendMode, + fill: const Fill(), + )); + maskNode.child.accept(this, data); + _builder.addMask(); + maskNode.mask.accept(this, data); + _builder.restore(); + _builder.restore(); + } + + @override + void visitResolvedPath(ResolvedPathNode pathNode, void data) { + _builder.addPath(pathNode.path, pathNode.paint, null, currentPatternId); + } + + @override + void visitResolvedTextPositionNode( + ResolvedTextPositionNode textPositionNode, void data) { + _builder.updateTextPosition(textPositionNode.textPosition); + textPositionNode.visitChildren((Node child) { + child.accept(this, data); + }); + } + + @override + void visitResolvedText(ResolvedTextNode textNode, void data) { + _builder.addText( + textNode.textConfig, textNode.paint, null, currentPatternId); + } + + @override + void visitViewportNode(ViewportNode viewportNode, void data) { + _width = viewportNode.width; + _height = viewportNode.height; + for (final Node child in viewportNode.children) { + child.accept(this, data); + } + } + + @override + void visitSaveLayerNode(SaveLayerNode layerNode, void data) { + _builder.addSaveLayer(layerNode.paint); + for (final Node child in layerNode.children) { + child.accept(this, data); + } + _builder.restore(); + } + + @override + void visitResolvedVerticesNode(ResolvedVerticesNode verticesNode, void data) { + _builder.addVertices(verticesNode.vertices, verticesNode.paint); + } + + @override + void visitResolvedImageNode(ResolvedImageNode resolvedImageNode, void data) { + _builder.addImage(resolvedImageNode, null); + } + + @override + void visitResolvedPatternNode(ResolvedPatternNode patternNode, void data) { + _builder.addPattern( + patternNode.id, + x: patternNode.x, + y: patternNode.y, + width: patternNode.width, + height: patternNode.height, + transform: patternNode.transform, + ); + patternNode.pattern.accept(this, data); + _builder.restore(); + currentPatternId = patternNode.id; + patternNode.child.accept(this, data); + currentPatternId = null; + } +} diff --git a/packages/vector_graphics_compiler/lib/src/util.dart b/packages/vector_graphics_compiler/lib/src/util.dart new file mode 100644 index 00000000000..829016f7a92 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/util.dart @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A utility method for comparing lists for equality. +/// +/// This method assumes that [T] implements a meaningful equality operator. +/// Therefore, thi method should not be used to compare lists containing +/// nested lists. +bool listEquals(List? a, List? b) { + if (a == null) { + return b == null; + } + if (b == null || a.length != b.length) { + return false; + } + if (identical(a, b)) { + return true; + } + for (int index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) { + return false; + } + } + return true; +} + +/// Linearly interpolates between two doubles by factor t. +@pragma('vm:prefer-inline') +double lerpDouble(double a, double b, double t) { + assert(a.isFinite); + assert(b.isFinite); + assert(t <= 1.0); + assert(t >= 0.0); + return (1 - t) * a + t * b; +} diff --git a/packages/vector_graphics_compiler/lib/src/vector_instructions.dart b/packages/vector_graphics_compiler/lib/src/vector_instructions.dart new file mode 100644 index 00000000000..edff5dfa2a5 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/vector_instructions.dart @@ -0,0 +1,237 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'geometry/image.dart'; +import 'geometry/path.dart'; +import 'geometry/pattern.dart'; +import 'geometry/vertices.dart'; +import 'paint.dart'; +import 'util.dart'; + +/// An immutable collection of vector instructions, with [width] and [height] +/// specifying the viewport coordinates. +@immutable +class VectorInstructions { + /// Creates a new set of [VectorInstructions]. + /// + /// The combined lengths of [paths] and [vertices] must be greater than 0. + const VectorInstructions({ + required this.width, + required this.height, + required this.paints, + this.paths = const [], + this.vertices = const [], + this.text = const [], + this.images = const [], + this.drawImages = const [], + this.patternData = const [], + this.textPositions = const [], + required this.commands, + }); + + /// The extent of the viewport on the x axis. + final double width; + + /// The extent of the viewport on the y axis. + final double height; + + /// The [Paint] objects used in [commands]. + final List paints; + + /// The [Path] objects, if any, used in [commands]. + final List paths; + + /// The [IndexedVertices] objects, if any, used in [commands]. + final List vertices; + + /// The [TextConfig] objects, if any, used in [commands]. + final List text; + + /// The [ImageData] objects, if any, used in [commands]. + final List images; + + /// The [DrawImageData] objects, if any, used in [commands]. + final List drawImages; + + /// The pattern data objects, if any used in [commands]. + final List patternData; + + /// A list of text position advances, if any, used in [commands]. + final List textPositions; + + /// The painting order list of drawing commands. + /// + /// If the command type is [DrawCommandType.path], this command specifies + /// drawing with [paths]. If it is [DrawCommandType.vertices], this command + /// specifies drawing with [vertices]. + /// + /// If drawing using vertices, the [Paint.stroke] property is ignored. + final List commands; + + @override + int get hashCode => Object.hash( + width, + height, + Object.hashAll(patternData), + Object.hashAll(paints), + Object.hashAll(paths), + Object.hashAll(vertices), + Object.hashAll(text), + Object.hashAll(commands), + Object.hashAll(images), + Object.hashAll(drawImages), + Object.hashAll(textPositions)); + + @override + bool operator ==(Object other) { + return other is VectorInstructions && + other.width == width && + other.height == height && + listEquals(other.patternData, patternData) && + listEquals(other.paints, paints) && + listEquals(other.paths, paths) && + listEquals(other.vertices, vertices) && + listEquals(other.text, text) && + listEquals(other.commands, commands) && + listEquals(other.images, images) && + listEquals(other.drawImages, drawImages) && + listEquals(other.textPositions, textPositions); + } + + @override + String toString() => 'VectorInstructions($width, $height)'; +} + +/// The drawing mode of a [DrawCommand]. +/// +/// See [DrawCommand.type] and [VectorInstructions.commands]. +enum DrawCommandType { + /// Specifies that this command draws a [Path]. + path, + + /// Specifies that this command draws an [IndexedVertices] object. + /// + /// In this case, any [Stroke] properties on the [Paint] are ignored. + vertices, + + /// Specifies that this command saves a layer. + /// + /// In this case, any [Stroke] properties on the [Paint] are ignored. + saveLayer, + + /// Specifies that this command restores a layer. + /// + /// In this case, both the objectId and paintId will be `null`. + restore, + + /// Specifies that this command adds a clip to the stack. + /// + /// In this case, the objectId will be for a path, and the paint id will be + /// `null`. + clip, + + /// Specifies that this command adds a mask to the stack. + /// + /// Implementations should save a layer using a grey scale color matrix. + mask, + + /// Specifies that this command draws text. + text, + + /// Specifies that this command draws an image. + image, + + /// Specifies that this command draws a pattern. + pattern, + + /// Specifies an adjustment to the current text position. + textPosition, +} + +/// A drawing command combining the index of a [Path] or an [IndexedVertices] +/// with a [Paint]. +/// +/// The type of object is specified by [type]. +/// +/// The debug string property is some identifier, possibly from the source SVG, +/// identifying an original source for this information. +@immutable +class DrawCommand { + /// Creates a new canvas drawing operation. + /// + /// See [DrawCommand]. + const DrawCommand( + this.type, { + this.objectId, + this.paintId, + this.debugString, + this.patternId, + this.patternDataId, + }); + + /// A string, possibly from the original source SVG file, identifying a source + /// for this command. + final String? debugString; + + /// Whether [objectId] points to a [Path] or a [IndexedVertices] object in + /// [VectorInstructions]. + final DrawCommandType type; + + /// The path or vertices object index in [VectorInstructions.paths] or + /// [VectorInstructions.vertices]. + /// + /// A value of `null` indicates that there is no object associated with + /// this command. + /// + /// Use [type] to determine which type of object this is. + final int? objectId; + + /// The index of a [Paint] for this object in [VectorInstructions.paints]. + /// + /// A value of `null` indicates that there is no paint object associated with + /// this command. + final int? paintId; + + /// The index of a pattern for this object in [VectorInstructions.patterns]. + final int? patternId; + + /// The index of a pattern configuration for this object in + /// [VectorInstructions.patternData]. + final int? patternDataId; + + @override + int get hashCode => Object.hash(type, objectId, paintId, debugString); + + @override + bool operator ==(Object other) { + return other is DrawCommand && + other.type == type && + other.objectId == objectId && + other.paintId == paintId; + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('DrawCommand($type'); + if (objectId != null) { + buffer.write(', objectId: $objectId'); + } + if (paintId != null) { + buffer.write(', paintId: $paintId'); + } + if (debugString != null) { + buffer.write(", debugString: '$debugString'"); + } + if (patternId != null) { + buffer.write(', patternId: $patternId'); + } + if (patternDataId != null) { + buffer.write(', patternDataId: $patternDataId'); + } + buffer.write(')'); + return buffer.toString(); + } +} diff --git a/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart new file mode 100644 index 00000000000..c4a8efe3b27 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart @@ -0,0 +1,352 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; + +import 'src/geometry/image.dart'; +import 'src/geometry/matrix.dart'; +import 'src/geometry/path.dart'; +import 'src/geometry/pattern.dart'; +import 'src/geometry/vertices.dart'; +import 'src/paint.dart'; +import 'src/svg/color_mapper.dart'; +import 'src/svg/parser.dart'; +import 'src/svg/theme.dart'; +import 'src/vector_instructions.dart'; + +export 'src/_initialize_path_ops_io.dart' + if (dart.library.html) 'src/_initialize_path_ops_web.dart'; +export 'src/_initialize_tessellator_io.dart' + if (dart.library.html) 'src/_initialize_tessellator_web.dart'; +export 'src/geometry/basic_types.dart'; +export 'src/geometry/matrix.dart'; +export 'src/geometry/path.dart'; +export 'src/geometry/vertices.dart'; +export 'src/paint.dart'; +export 'src/svg/color_mapper.dart'; +export 'src/svg/path_ops.dart' show initializeLibPathOps; +export 'src/svg/resolver.dart'; +export 'src/svg/tessellator.dart' show initializeLibTesselator; +export 'src/svg/theme.dart'; +export 'src/vector_instructions.dart'; + +/// Parses an SVG string into a [VectorInstructions] object, with all optional +/// optimizers disabled. +VectorInstructions parseWithoutOptimizers( + String xml, { + String key = '', + bool warningsAsErrors = false, + SvgTheme theme = const SvgTheme(), +}) { + return parse( + xml, + key: key, + warningsAsErrors: warningsAsErrors, + theme: theme, + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); +} + +/// Parses an SVG string into a [VectorInstructions] object. +VectorInstructions parse( + String xml, { + String key = '', + bool warningsAsErrors = false, + SvgTheme theme = const SvgTheme(), + bool enableMaskingOptimizer = true, + bool enableClippingOptimizer = true, + bool enableOverdrawOptimizer = true, + ColorMapper? colorMapper, +}) { + final SvgParser parser = SvgParser( + xml, + theme, + key, + warningsAsErrors, + colorMapper, + ); + parser.enableMaskingOptimizer = enableMaskingOptimizer; + parser.enableClippingOptimizer = enableClippingOptimizer; + parser.enableOverdrawOptimizer = enableOverdrawOptimizer; + return parser.parse(); +} + +Float64List? _encodeMatrix(AffineMatrix? matrix) { + if (matrix == null || matrix == AffineMatrix.identity) { + return null; + } + return matrix.toMatrix4(); +} + +void _encodeShader( + Gradient? shader, + Map shaderIds, + VectorGraphicsCodec codec, + VectorGraphicsBuffer buffer, +) { + if (shader == null) { + return; + } + int shaderId; + if (shader is LinearGradient) { + shaderId = codec.writeLinearGradient( + buffer, + fromX: shader.from.x, + fromY: shader.from.y, + toX: shader.to.x, + toY: shader.to.y, + colors: Int32List.fromList( + [for (final Color color in shader.colors!) color.value]), + offsets: Float32List.fromList(shader.offsets!), + tileMode: shader.tileMode!.index, + ); + } else if (shader is RadialGradient) { + shaderId = codec.writeRadialGradient( + buffer, + centerX: shader.center.x, + centerY: shader.center.y, + radius: shader.radius, + focalX: shader.focalPoint?.x, + focalY: shader.focalPoint?.y, + colors: Int32List.fromList( + [for (final Color color in shader.colors!) color.value]), + offsets: Float32List.fromList(shader.offsets!), + tileMode: shader.tileMode!.index, + transform: _encodeMatrix(shader.transform), + ); + } else { + assert(false); + throw StateError('illegal shader type: $shader'); + } + shaderIds[shader] = shaderId; +} + +/// String input, String filename +/// Encode an SVG [input] string into a vector_graphics binary format. +Uint8List encodeSvg({ + required String xml, + required String debugName, + SvgTheme theme = const SvgTheme(), + bool enableMaskingOptimizer = true, + bool enableClippingOptimizer = true, + bool enableOverdrawOptimizer = true, + bool warningsAsErrors = false, + bool useHalfPrecisionControlPoints = false, + ColorMapper? colorMapper, +}) { + return _encodeInstructions( + parse( + xml, + key: debugName, + theme: theme, + enableMaskingOptimizer: enableMaskingOptimizer, + enableClippingOptimizer: enableClippingOptimizer, + enableOverdrawOptimizer: enableOverdrawOptimizer, + warningsAsErrors: warningsAsErrors, + colorMapper: colorMapper, + ), + useHalfPrecisionControlPoints, + ); +} + +Uint8List _encodeInstructions( + VectorInstructions instructions, + bool useHalfPrecisionControlPoints, +) { + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final VectorGraphicsBuffer buffer = VectorGraphicsBuffer(); + + codec.writeSize(buffer, instructions.width, instructions.height); + + final Map fillIds = {}; + final Map strokeIds = {}; + final Map shaderIds = {}; + + for (final ImageData data in instructions.images) { + codec.writeImage(buffer, data.format, data.data); + } + + for (final Paint paint in instructions.paints) { + _encodeShader(paint.fill?.shader, shaderIds, codec, buffer); + _encodeShader(paint.stroke?.shader, shaderIds, codec, buffer); + } + + int nextPaintId = 0; + for (final Paint paint in instructions.paints) { + final Fill? fill = paint.fill; + final Stroke? stroke = paint.stroke; + + if (fill != null) { + final int? shaderId = shaderIds[fill.shader]; + final int fillId = codec.writeFill( + buffer, + fill.color.value, + paint.blendMode.index, + shaderId, + ); + fillIds[nextPaintId] = fillId; + } + if (stroke != null) { + final int? shaderId = shaderIds[stroke.shader]; + final int strokeId = codec.writeStroke( + buffer, + stroke.color.value, + stroke.cap?.index ?? 0, + stroke.join?.index ?? 0, + paint.blendMode.index, + stroke.miterLimit ?? 4, + stroke.width ?? 1, + shaderId, + ); + strokeIds[nextPaintId] = strokeId; + } + nextPaintId += 1; + } + + final Map pathIds = {}; + int nextPathId = 0; + for (final Path path in instructions.paths) { + final List controlPointTypes = []; + final List controlPoints = []; + + for (final PathCommand command in path.commands) { + switch (command.type) { + case PathCommandType.move: + final MoveToCommand move = command as MoveToCommand; + controlPointTypes.add(ControlPointTypes.moveTo); + controlPoints.addAll([move.x, move.y]); + case PathCommandType.line: + final LineToCommand line = command as LineToCommand; + controlPointTypes.add(ControlPointTypes.lineTo); + controlPoints.addAll([line.x, line.y]); + case PathCommandType.cubic: + final CubicToCommand cubic = command as CubicToCommand; + controlPointTypes.add(ControlPointTypes.cubicTo); + controlPoints.addAll([ + cubic.x1, + cubic.y1, + cubic.x2, + cubic.y2, + cubic.x3, + cubic.y3, + ]); + case PathCommandType.close: + controlPointTypes.add(ControlPointTypes.close); + } + } + final int id = codec.writePath( + buffer, + Uint8List.fromList(controlPointTypes), + Float32List.fromList(controlPoints), + path.fillType.index, + half: useHalfPrecisionControlPoints, + ); + pathIds[nextPathId] = id; + nextPathId += 1; + } + + for (final TextPosition position in instructions.textPositions) { + codec.writeTextPosition( + buffer, + position.x, + position.y, + position.dx, + position.dy, + position.reset, + position.transform?.toMatrix4(), + ); + } + + for (final TextConfig textConfig in instructions.text) { + codec.writeTextConfig( + buffer: buffer, + text: textConfig.text, + fontFamily: textConfig.fontFamily, + xAnchorMultiplier: textConfig.xAnchorMultiplier, + fontWeight: textConfig.fontWeight.index, + fontSize: textConfig.fontSize, + decoration: textConfig.decoration.mask, + decorationStyle: textConfig.decorationStyle.index, + decorationColor: textConfig.decorationColor.value, + ); + } + + for (final DrawCommand command in instructions.commands) { + switch (command.type) { + case DrawCommandType.path: + if (fillIds.containsKey(command.paintId)) { + codec.writeDrawPath( + buffer, + pathIds[command.objectId]!, + fillIds[command.paintId]!, + command.patternId, + ); + } + if (strokeIds.containsKey(command.paintId)) { + codec.writeDrawPath( + buffer, + pathIds[command.objectId]!, + strokeIds[command.paintId]!, + command.patternId, + ); + } + case DrawCommandType.vertices: + final IndexedVertices vertices = + instructions.vertices[command.objectId!]; + final int fillId = fillIds[command.paintId]!; + codec.writeDrawVertices( + buffer, vertices.vertices, vertices.indices, fillId); + case DrawCommandType.saveLayer: + codec.writeSaveLayer(buffer, fillIds[command.paintId]!); + case DrawCommandType.restore: + codec.writeRestoreLayer(buffer); + case DrawCommandType.clip: + codec.writeClipPath(buffer, pathIds[command.objectId]!); + case DrawCommandType.mask: + codec.writeMask(buffer); + + case DrawCommandType.pattern: + final PatternData patternData = + instructions.patternData[command.patternDataId!]; + codec.writePattern( + buffer, + patternData.x, + patternData.y, + patternData.width, + patternData.height, + patternData.transform.toMatrix4(), + ); + + case DrawCommandType.textPosition: + codec.writeUpdateTextPosition(buffer, command.objectId!); + + case DrawCommandType.text: + codec.writeDrawText( + buffer, + command.objectId!, + fillIds[command.paintId], + strokeIds[command.paintId], + command.patternId, + ); + + case DrawCommandType.image: + final DrawImageData drawImageData = + instructions.drawImages[command.objectId!]; + codec.writeDrawImage( + buffer, + drawImageData.id, + drawImageData.rect.left, + drawImageData.rect.top, + drawImageData.rect.width, + drawImageData.rect.height, + drawImageData.transform?.toMatrix4(), + ); + } + } + return buffer.done().buffer.asUint8List(); +} diff --git a/packages/vector_graphics_compiler/pubspec.yaml b/packages/vector_graphics_compiler/pubspec.yaml new file mode 100644 index 00000000000..ccc6989f4c1 --- /dev/null +++ b/packages/vector_graphics_compiler/pubspec.yaml @@ -0,0 +1,51 @@ +name: vector_graphics_compiler +description: A compiler to convert SVGs to the binary format used by `package:vector_graphics`. +repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_compiler +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 +# See https://github.com/flutter/flutter/issues/157626 before publishing a new +# version. +version: 1.1.12 + +executables: + vector_graphics_compiler: + +environment: + sdk: ^3.4.0 + +dependencies: + args: ^2.3.0 + meta: ^1.7.0 + path: ^1.8.0 + path_parsing: ^1.0.1 + # See https://github.com/flutter/flutter/issues/157626 + vector_graphics_codec: ">=1.1.11+1 <= 1.1.12" + # This uses an exact upper range because it is an external dependency (see + # https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#dependencies + # for more information), and this mitigates transitive risk exposure to + # clients. To update this value: + # - Audit the package's source diffs between the current max version listed + # here and the target version. + # - Update the max version here to exactly the audited version. + xml: ">=6.3.0 <=6.5.0" + +dev_dependencies: + flutter: + sdk: flutter + flutter_test: + sdk: flutter + test: ^1.20.1 + # See https://github.com/flutter/flutter/issues/157626 + vector_graphics: ">=1.1.11+1 <= 1.1.12" + vector_math: ^2.1.2 + +platforms: + android: + ios: + linux: + macos: + web: + windows: + +topics: + - svg + - vector-graphics diff --git a/packages/vector_graphics_compiler/test/basic_types_test.dart b/packages/vector_graphics_compiler/test/basic_types_test.dart new file mode 100644 index 00000000000..ea3d050c63b --- /dev/null +++ b/packages/vector_graphics_compiler/test/basic_types_test.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('Point tests', () { + expect(Point.zero.x, 0); + expect(Point.zero.y, 0); + + expect(const Point(5, 5) / 2, const Point(2.5, 2.5)); + expect(const Point(5, 5) * 2, const Point(10, 10)); + }); + + test('Point distance', () { + expect(Point.distance(Point.zero, Point.zero), 0); + expect(Point.distance(Point.zero, const Point(1, 0)), 1); + expect(Point.distance(Point.zero, const Point(0, 1)), 1); + expect(Point.distance(Point.zero, const Point(1, 1)), 1.4142135623730951); + }); + + test('Point lerp', () { + expect(Point.lerp(Point.zero, Point.zero, .3), Point.zero); + expect(Point.lerp(Point.zero, const Point(1, 0), .5), const Point(.5, 0)); + }); + + test('Rect tests', () { + expect(Rect.zero.left, 0); + expect(Rect.zero.top, 0); + expect(Rect.zero.right, 0); + expect(Rect.zero.bottom, 0); + + expect( + const Rect.fromLTRB(1, 2, 3, 4) + .expanded(const Rect.fromLTRB(0, 0, 10, 10)), + const Rect.fromLTRB(0, 0, 10, 10), + ); + + expect( + const Rect.fromCircle(10, 10, 5), + const Rect.fromLTWH(5, 5, 10, 10), + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/cli_test.dart b/packages/vector_graphics_compiler/test/cli_test.dart new file mode 100644 index 00000000000..501790d858b --- /dev/null +++ b/packages/vector_graphics_compiler/test/cli_test.dart @@ -0,0 +1,139 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; + +import '../bin/util/isolate_processor.dart'; +import '../bin/vector_graphics_compiler.dart' as cli; + +void main() { + final File output = File('test_data/example.vec'); + final File outputDebug = File('test_data/example.vec.debug'); + + test('currentColor/font-size works', () async { + try { + await cli.main([ + '-i', + 'test_data/example.svg', + '-o', + output.path, + '--current-color', + 'red', + '--font-size', + '12', + '--dump-debug', + ]); + expect(output.existsSync(), true); + expect(outputDebug.existsSync(), true); + } finally { + if (output.existsSync()) { + output.deleteSync(); + } + if (outputDebug.existsSync()) { + outputDebug.deleteSync(); + } + } + }); + + test('Can run with isolate processor', () async { + try { + final IsolateProcessor processor = IsolateProcessor(null, null, 4); + final bool result = await processor.process( + [ + Pair('test_data/example.svg', output.path), + ], + maskingOptimizerEnabled: false, + clippingOptimizerEnabled: false, + overdrawOptimizerEnabled: false, + tessellate: false, + dumpDebug: false, + useHalfPrecisionControlPoints: false, + ); + expect(result, isTrue); + expect(output.existsSync(), isTrue); + } finally { + if (output.existsSync()) { + output.deleteSync(); + } + } + }); + + test('Can dump debug format with isolate processor', () async { + try { + final IsolateProcessor processor = IsolateProcessor(null, null, 4); + final bool result = await processor.process( + [ + Pair('test_data/example.svg', output.path), + ], + maskingOptimizerEnabled: false, + clippingOptimizerEnabled: false, + overdrawOptimizerEnabled: false, + tessellate: false, + dumpDebug: true, + useHalfPrecisionControlPoints: false, + ); + expect(result, isTrue); + expect(output.existsSync(), isTrue); + expect(outputDebug.existsSync(), isTrue); + } finally { + if (output.existsSync()) { + output.deleteSync(); + } + if (outputDebug.existsSync()) { + outputDebug.deleteSync(); + } + } + }); + + test('out-dir option works', () async { + const String inputTestDir = 'test_data'; + + const String outTestDir = 'output_vec'; + + try { + await cli.main([ + '--input-dir', + inputTestDir, + '--out-dir', + outTestDir, + ]); + + bool passed = false; + + final Directory inputDir = Directory(inputTestDir); + final Directory outDir = Directory(outTestDir); + + if (inputDir.existsSync() && outDir.existsSync()) { + final List inputTestFiles = inputDir + .listSync(recursive: true) + .whereType() + .where((File element) => element.path.endsWith('svg')) + .map((File e) => p.basenameWithoutExtension(e.path)) + .toList(); + + final List outTestFiles = outDir + .listSync(recursive: true) + .whereType() + .where((File element) => element.path.endsWith('vec')) + .map((File e) => + p.withoutExtension(p.basenameWithoutExtension(e.path))) + .toList(); + + if (listEquals(inputTestFiles, outTestFiles)) { + passed = true; + } + } + + expect(passed, true); + } finally { + if (Directory(outTestDir).existsSync()) { + Directory(outTestDir).deleteSync(recursive: true); + } + } + }); +} diff --git a/packages/vector_graphics_compiler/test/clipping_optimizer_test.dart b/packages/vector_graphics_compiler/test/clipping_optimizer_test.dart new file mode 100644 index 00000000000..811c84cf069 --- /dev/null +++ b/packages/vector_graphics_compiler/test/clipping_optimizer_test.dart @@ -0,0 +1,186 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/clipping_optimizer.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'test_svg_strings.dart'; + +Node parseAndResolve(String source) { + final Node node = parseToNodeTree(source); + final ResolvingVisitor visitor = ResolvingVisitor(); + return node.accept(visitor, AffineMatrix.identity); +} + +List queryChildren(Node node) { + final List children = []; + void visitor(Node child) { + if (child is T) { + children.add(child); + } + child.visitChildren(visitor); + } + + node.visitChildren(visitor); + return children; +} + +void main() { + setUpAll(() { + if (!initializePathOpsFromFlutterCache()) { + fail('error in setup'); + } + }); + + test('Only resolve ClipNode if .clips has one PathNode', () { + final Node node = parseAndResolve(''' + + + + + + + + +'''); + + final ClippingOptimizer visitor = ClippingOptimizer(); + final Node newNode = visitor.apply(node); + + final List clipNodesNew = + queryChildren(newNode); + + expect(clipNodesNew.length, 0); + }); + + test( + "Don't resolve a ClipNode if one of the PathNodes it's applied to has stroke.width set", + () async { + final Node node = parseAndResolve(''' + + + + + +'''); + + final ClippingOptimizer visitor = ClippingOptimizer(); + final Node newNode = visitor.apply(node); + + final List clipNodesNew = + queryChildren(newNode); + + expect(clipNodesNew.length, 1); + }); + + test("Don't resolve ClipNode if intersection of Clip and Path is empty", + () async { + final Node node = parseAndResolve(''' + + + + + + + + + +'''); + final ClippingOptimizer visitor = ClippingOptimizer(); + final Node newNode = visitor.apply(node); + + final List clipNodesNew = + queryChildren(newNode); + + expect(clipNodesNew.length, 1); + }); + + test('ParentNode and PathNode count should stay the same', () async { + final Node node = parseAndResolve(pathAndParent); + + final List pathNodesOld = + queryChildren(node); + final List parentNodesOld = queryChildren(node); + + final ClippingOptimizer visitor = ClippingOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + final List parentNodesNew = queryChildren(newNode); + + expect(pathNodesOld.length, pathNodesNew.length); + expect(parentNodesOld.length, parentNodesNew.length); + }); + + test('Does not combine clips with multiple fill rules', () { + final VectorInstructions instructions = parse(multiClip); + expect(instructions.paths, [ + parseSvgPathData( + 'M 250,75 L 323,301 131,161 369,161 177,301 z', PathFillType.evenOdd), + PathBuilder().addOval(const Rect.fromCircle(400, 200, 150)).toPath(), + parseSvgPathData('M 250,75 L 323,301 131,161 369,161 177,301 z') + .transformed(AffineMatrix.identity.translated(250, 0)), + PathBuilder().addOval(const Rect.fromCircle(450, 300, 150)).toPath(), + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.clip, objectId: 0), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.clip, objectId: 2), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.clip, objectId: 0), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 1), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.clip, objectId: 2), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 1), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('Combines clips where possible', () { + final VectorInstructions instructions = + parse(basicClip, enableClippingOptimizer: false); + final VectorInstructions instructionsWithOptimizer = parse(basicClip); + + expect(instructionsWithOptimizer.paths, basicClipsForClippingOptimzer); + + expect(instructions.paths, [ + PathBuilder() + .addOval(const Rect.fromCircle(30, 30, 20)) + .addOval(const Rect.fromCircle(70, 70, 20)) + .toPath(), + PathBuilder().addRect(const Rect.fromLTWH(10, 10, 100, 100)).toPath(), + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.clip, objectId: 0), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 0), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('Preserves fill type changes', () { + const String svg = ''' + + + + + + + + + +'''; + final VectorInstructions instructions = parse(svg); + + expect( + instructions.paths.single.fillType, + PathFillType.evenOdd, + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/draw_command_builder_test.dart b/packages/vector_graphics_compiler/test/draw_command_builder_test.dart new file mode 100644 index 00000000000..13b24b30132 --- /dev/null +++ b/packages/vector_graphics_compiler/test/draw_command_builder_test.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/draw_command_builder.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('DrawCommandBuilder does not emit empty paths', () { + final DrawCommandBuilder builder = DrawCommandBuilder(); + builder.addPath(Path(), const Paint(), null, null); + expect(builder.toInstructions(100, 100).commands, isEmpty); + }); +} diff --git a/packages/vector_graphics_compiler/test/end_to_end_test.dart b/packages/vector_graphics_compiler/test/end_to_end_test.dart new file mode 100644 index 00000000000..5f225a412a1 --- /dev/null +++ b/packages/vector_graphics_compiler/test/end_to_end_test.dart @@ -0,0 +1,1113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics/vector_graphics.dart'; +import 'package:vector_graphics_codec/vector_graphics_codec.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'test_svg_strings.dart'; + +class TestBytesLoader extends BytesLoader { + const TestBytesLoader(this.data); + + final ByteData data; + + @override + Future loadBytes(BuildContext? context) async { + return data; + } + + @override + int get hashCode => data.hashCode; + + @override + bool operator ==(Object other) { + return other is TestBytesLoader && other.data == data; + } +} + +void main() { + testWidgets('Can endcode and decode simple SVGs with no errors', + (WidgetTester tester) async { + for (final String svg in allSvgTestStrings) { + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test.svg', + warningsAsErrors: true, + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + + await tester.pumpWidget(Center( + child: VectorGraphic( + loader: TestBytesLoader(bytes.buffer.asByteData())))); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + } + }); + + testWidgets('Errors on unsupported image mime type', + (WidgetTester tester) async { + const String svgInlineImage = r''' + + + +'''; + + expect( + () => encodeSvg( + xml: svgInlineImage, + debugName: 'test.svg', + warningsAsErrors: true, + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ), + throwsA(isA())); + }); + + test('encodeSvg encodes stroke shaders', () async { + const String svg = ''' + + + + + + + + + + + +'''; + + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final TestListener listener = TestListener(); + codec.decode(bytes.buffer.asByteData(), listener); + expect(listener.commands, [ + const OnSize(120, 120), + OnLinearGradient( + id: 0, + fromX: 69, + fromY: 59, + toX: 36, + toY: 84, + colors: Int32List.fromList([0xffffffff, 0xff000000]), + offsets: Float32List.fromList([0, 1]), + tileMode: 0, + ), + OnPaintObject( + color: 0xffffffff, + strokeCap: 1, + strokeJoin: 1, + blendMode: BlendMode.srcOver.index, + strokeMiterLimit: 4.0, + strokeWidth: 8, + paintStyle: 1, + id: 0, + shaderId: 0, + ), + const OnPathStart(0, 0), + const OnPathMoveTo(34, 76), + const OnPathLineTo(57, 76), + const OnPathFinished(), + const OnDrawPath(0, 0, null), + ]); + }); + + test('Encodes nested tspan for text', () async { + const String svg = ''' + + + + Plain text Roboto + + Plain text Verdana + + + Bold text Verdana + + + Stroked bold line + Line 3 + + +'''; + + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final TestListener listener = TestListener(); + codec.decode(bytes.buffer.asByteData(), listener); + expect(listener.commands, [ + const OnSize(1000, 300), + OnPaintObject( + color: 4278190335, + strokeCap: null, + strokeJoin: null, + blendMode: BlendMode.srcOver.index, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: 0, + shaderId: null, + ), + OnPaintObject( + color: 4278222848, + strokeCap: null, + strokeJoin: null, + blendMode: BlendMode.srcOver.index, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: 1, + shaderId: null, + ), + OnPaintObject( + color: 4294901760, + strokeCap: 0, + strokeJoin: 0, + blendMode: BlendMode.srcOver.index, + strokeMiterLimit: 4.0, + strokeWidth: 1.0, + paintStyle: 1, + id: 2, + shaderId: null, + ), + OnPaintObject( + color: 4278222848, + strokeCap: null, + strokeJoin: null, + blendMode: BlendMode.srcOver.index, + strokeMiterLimit: null, + strokeWidth: null, + paintStyle: 0, + id: 3, + shaderId: null, + ), + const OnTextConfig( + 'Plain text Roboto', 0, 55, 'Roboto', 3, 0, 0, 4278190080, 0), + const OnTextConfig( + 'Plain text Verdana', 0, 55, 'Verdana', 3, 0, 0, 4278190080, 1), + const OnTextConfig( + 'Bold text Verdana', 0, 55, 'Verdana', 6, 0, 0, 4278190080, 2), + const OnTextConfig( + 'Stroked bold line', 0, 55, 'Roboto', 8, 0, 0, 4278190080, 3), + const OnTextConfig(' Line 3', 0, 55, 'Roboto', 3, 0, 0, 4278190080, 4), + const OnDrawText(0, 0, null, null), + const OnDrawText(1, 0, null, null), + const OnDrawText(2, 0, null, null), + const OnDrawText(3, 1, 2, null), + const OnDrawText(4, 3, null, null), + ]); + }); + + test('Encodes image elids trivial translation transform', () async { + const String svg = ''' + + + + + +'''; + + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final TestListener listener = TestListener(); + final ByteData data = bytes.buffer.asByteData(); + final DecodeResponse response = codec.decode(data, listener); + codec.decode(data, listener, response: response); + + expect(listener.commands, [ + const OnSize(1000, 300), + OnImage(0, 0, base64.decode(kBase64ImageContents)), + const OnDrawImage(0, 3, 3, 50, 50, null), + ]); + }); + + test('Encodes image elids trivial scale transform', () async { + const String svg = ''' + + + + + +'''; + + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final TestListener listener = TestListener(); + final ByteData data = bytes.buffer.asByteData(); + final DecodeResponse response = codec.decode(data, listener); + codec.decode(data, listener, response: response); + + expect(listener.commands, [ + const OnSize(1000, 300), + OnImage(0, 0, base64.decode(kBase64ImageContents)), + const OnDrawImage(0, 0, 0, 100, 100, null), + ]); + }); + + test('Encodes image does not elide non-trivial transform', () async { + const String svg = ''' + + + + + +'''; + + final Uint8List bytes = encodeSvg( + xml: svg, + debugName: 'test', + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + const VectorGraphicsCodec codec = VectorGraphicsCodec(); + final TestListener listener = TestListener(); + final ByteData data = bytes.buffer.asByteData(); + final DecodeResponse response = codec.decode(data, listener); + codec.decode(data, listener, response: response); + + expect(listener.commands, [ + const OnSize(1000, 300), + OnImage(0, 0, base64.decode(kBase64ImageContents)), + const OnDrawImage(0, 0, 0, 50, 50, [ + 3.0, + 1.0, + 0.0, + 0.0, + -1.0, + 3.0, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 30.0, + 40.0, + 0.0, + 1.0, + ]), + ]); + }); +} + +class TestListener extends VectorGraphicsCodecListener { + final List commands = []; + + @override + void onTextPosition(int textPositionId, double? x, double? y, double? dx, + double? dy, bool reset, Float64List? transform) {} + + @override + void onUpdateTextPosition(int textPositionId) {} + + @override + void onDrawPath(int pathId, int? paintId, int? patternId) { + commands.add(OnDrawPath(pathId, paintId, patternId)); + } + + @override + void onDrawVertices(Float32List vertices, Uint16List? indices, int? paintId) { + commands.add(OnDrawVertices(vertices, indices, paintId)); + } + + @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, + }) { + commands.add( + OnPaintObject( + color: color, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + blendMode: blendMode, + strokeMiterLimit: strokeMiterLimit, + strokeWidth: strokeWidth, + paintStyle: paintStyle, + id: id, + shaderId: shaderId, + ), + ); + } + + @override + void onPathClose() { + commands.add(const OnPathClose()); + } + + @override + void onPathCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + commands.add(OnPathCubicTo(x1, y1, x2, y2, x3, y3)); + } + + @override + void onPathFinished() { + commands.add(const OnPathFinished()); + } + + @override + void onPathLineTo(double x, double y) { + commands.add(OnPathLineTo(x, y)); + } + + @override + void onPathMoveTo(double x, double y) { + commands.add(OnPathMoveTo(x, y)); + } + + @override + void onPathStart(int id, int fillType) { + commands.add(OnPathStart(id, fillType)); + } + + @override + void onRestoreLayer() { + commands.add(const OnRestoreLayer()); + } + + @override + void onMask() { + commands.add(const OnMask()); + } + + @override + void onSaveLayer(int id) { + commands.add(OnSaveLayer(id)); + } + + @override + void onClipPath(int pathId) { + commands.add(OnClipPath(pathId)); + } + + @override + void onRadialGradient( + double centerX, + double centerY, + double radius, + double? focalX, + double? focalY, + Int32List colors, + Float32List? offsets, + Float64List? transform, + int tileMode, + int id, + ) { + commands.add( + OnRadialGradient( + centerX: centerX, + centerY: centerY, + radius: radius, + focalX: focalX, + focalY: focalY, + colors: colors, + offsets: offsets, + transform: transform, + tileMode: tileMode, + id: id, + ), + ); + } + + @override + void onLinearGradient( + double fromX, + double fromY, + double toX, + double toY, + Int32List colors, + Float32List? offsets, + int tileMode, + int id, + ) { + commands.add(OnLinearGradient( + fromX: fromX, + fromY: fromY, + toX: toX, + toY: toY, + colors: colors, + offsets: offsets, + tileMode: tileMode, + id: id, + )); + } + + @override + void onSize(double width, double height) { + commands.add(OnSize(width, height)); + } + + @override + void onTextConfig( + String text, + String? fontFamily, + double xAnchorMultiplier, + int fontWeight, + double fontSize, + int decoration, + int decorationStyle, + int decorationColor, + int id, + ) { + commands.add(OnTextConfig( + text, + xAnchorMultiplier, + fontSize, + fontFamily, + fontWeight, + decoration, + decorationStyle, + decorationColor, + id, + )); + } + + @override + void onDrawText(int textId, int? fillId, int? strokeId, int? patternId) { + commands.add(OnDrawText(textId, fillId, strokeId, patternId)); + } + + @override + void onDrawImage( + int imageId, + double x, + double y, + double width, + double height, + Float64List? transform, + ) { + commands.add(OnDrawImage(imageId, x, y, width, height, transform)); + } + + @override + void onImage( + int imageId, + int format, + Uint8List data, { + VectorGraphicsErrorListener? onError, + }) { + commands.add(OnImage( + imageId, + format, + data, + onError: onError, + )); + } + + @override + void onPatternStart(int patternId, double x, double y, double width, + double height, Float64List transform) { + commands.add(OnPatternStart(patternId, x, y, width, height, transform)); + } +} + +@immutable +class OnMask { + const OnMask(); +} + +@immutable +class OnLinearGradient { + const OnLinearGradient({ + required this.fromX, + required this.fromY, + required this.toX, + required this.toY, + required this.colors, + required this.offsets, + required this.tileMode, + required this.id, + }); + + final double fromX; + final double fromY; + final double toX; + final double toY; + final Int32List colors; + final Float32List? offsets; + final int tileMode; + final int id; + + @override + int get hashCode => Object.hash( + fromX, + fromY, + toX, + toY, + Object.hashAll(colors), + Object.hashAll(offsets ?? []), + tileMode, + id, + ); + + @override + bool operator ==(Object other) { + return other is OnLinearGradient && + other.fromX == fromX && + other.fromY == fromY && + other.toX == toX && + other.toY == toY && + _listEquals(other.colors, colors) && + _listEquals(other.offsets, offsets) && + other.tileMode == tileMode && + other.id == id; + } + + @override + String toString() { + return 'OnLinearGradient(' + 'fromX: $fromX, ' + 'toX: $toX, ' + 'fromY: $fromY, ' + 'toY: $toY, ' + 'colors: Int32List.fromList($colors), ' + 'offsets: Float32List.fromList($offsets), ' + 'tileMode: $tileMode, ' + 'id: $id)'; + } +} + +@immutable +class OnRadialGradient { + const OnRadialGradient({ + required this.centerX, + required this.centerY, + required this.radius, + required this.focalX, + required this.focalY, + required this.colors, + required this.offsets, + required this.transform, + required this.tileMode, + required this.id, + }); + + final double centerX; + final double centerY; + final double radius; + final double? focalX; + final double? focalY; + final Int32List colors; + final Float32List? offsets; + final Float64List? transform; + final int tileMode; + final int id; + + @override + int get hashCode => Object.hash( + centerX, + centerY, + radius, + focalX, + focalY, + Object.hashAll(colors), + Object.hashAll(offsets ?? []), + Object.hashAll(transform ?? []), + tileMode, + id, + ); + + @override + bool operator ==(Object other) { + return other is OnRadialGradient && + other.centerX == centerX && + other.centerY == centerY && + other.radius == radius && + other.focalX == focalX && + other.focalX == focalY && + _listEquals(other.colors, colors) && + _listEquals(other.offsets, offsets) && + _listEquals(other.transform, transform) && + other.tileMode == tileMode && + other.id == id; + } +} + +@immutable +class OnSaveLayer { + const OnSaveLayer(this.id); + + final int id; + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(Object other) => other is OnSaveLayer && other.id == id; +} + +@immutable +class OnClipPath { + const OnClipPath(this.id); + + final int id; + + @override + int get hashCode => id.hashCode; + + @override + bool operator ==(Object other) => other is OnClipPath && other.id == id; +} + +@immutable +class OnRestoreLayer { + const OnRestoreLayer(); +} + +@immutable +class OnDrawPath { + const OnDrawPath(this.pathId, this.paintId, this.patternId); + + final int pathId; + final int? paintId; + final int? patternId; + + @override + int get hashCode => Object.hash(pathId, paintId, patternId); + + @override + bool operator ==(Object other) => + other is OnDrawPath && + other.pathId == pathId && + other.paintId == paintId && + other.patternId == patternId; + + @override + String toString() => 'OnDrawPath($pathId, $paintId, $patternId)'; +} + +@immutable +class OnDrawVertices { + const OnDrawVertices(this.vertices, this.indices, this.paintId); + + final List vertices; + final List? indices; + final int? paintId; + + @override + int get hashCode => Object.hash( + Object.hashAll(vertices), Object.hashAll(indices ?? []), paintId); + + @override + bool operator ==(Object other) => + other is OnDrawVertices && + _listEquals(vertices, other.vertices) && + _listEquals(indices, other.indices) && + other.paintId == paintId; + + @override + String toString() => 'OnDrawVertices($vertices, $indices, $paintId)'; +} + +@immutable +class OnPaintObject { + const OnPaintObject({ + required this.color, + required this.strokeCap, + required this.strokeJoin, + required this.blendMode, + required this.strokeMiterLimit, + required this.strokeWidth, + required this.paintStyle, + required this.id, + required this.shaderId, + }); + + final int color; + final int? strokeCap; + final int? strokeJoin; + final int blendMode; + final double? strokeMiterLimit; + final double? strokeWidth; + final int paintStyle; + final int id; + final int? shaderId; + + @override + int get hashCode => Object.hash(color, strokeCap, strokeJoin, blendMode, + strokeMiterLimit, strokeWidth, paintStyle, id, shaderId); + + @override + bool operator ==(Object other) => + other is OnPaintObject && + other.color == color && + other.strokeCap == strokeCap && + other.strokeJoin == strokeJoin && + other.blendMode == blendMode && + other.strokeMiterLimit == strokeMiterLimit && + other.strokeWidth == strokeWidth && + other.paintStyle == paintStyle && + other.id == id && + other.shaderId == shaderId; + + @override + String toString() => + 'OnPaintObject(color: $color, strokeCap: $strokeCap, strokeJoin: $strokeJoin, ' + 'blendMode: $blendMode, strokeMiterLimit: $strokeMiterLimit, strokeWidth: $strokeWidth, ' + 'paintStyle: $paintStyle, id: $id, shaderId: $shaderId)'; +} + +@immutable +class OnPathClose { + const OnPathClose(); + + @override + int get hashCode => 44221; + + @override + bool operator ==(Object other) => other is OnPathClose; + + @override + String toString() => 'OnPathClose'; +} + +@immutable +class OnPathCubicTo { + const OnPathCubicTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double x2; + final double x3; + final double y1; + final double y2; + final double y3; + + @override + int get hashCode => Object.hash(x1, y1, x2, y2, x3, y3); + + @override + bool operator ==(Object other) => + other is OnPathCubicTo && + other.x1 == x1 && + other.y1 == y1 && + other.x2 == x2 && + other.y2 == y2 && + other.x3 == x3 && + other.y3 == y3; + + @override + String toString() => 'OnPathCubicTo($x1, $y1, $x2, $y2, $x3, $y3)'; +} + +@immutable +class OnPathFinished { + const OnPathFinished(); + + @override + int get hashCode => 1223; + + @override + bool operator ==(Object other) => other is OnPathFinished; + + @override + String toString() => 'OnPathFinished'; +} + +@immutable +class OnPathLineTo { + const OnPathLineTo(this.x, this.y); + + final double x; + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) => + other is OnPathLineTo && other.x == x && other.y == y; + + @override + String toString() => 'OnPathLineTo($x, $y)'; +} + +@immutable +class OnPathMoveTo { + const OnPathMoveTo(this.x, this.y); + + final double x; + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) => + other is OnPathMoveTo && other.x == x && other.y == y; + + @override + String toString() => 'OnPathMoveTo($x, $y)'; +} + +@immutable +class OnPathStart { + const OnPathStart(this.id, this.fillType); + + final int id; + final int fillType; + + @override + int get hashCode => Object.hash(id, fillType); + + @override + bool operator ==(Object other) => + other is OnPathStart && other.id == id && other.fillType == fillType; + + @override + String toString() => 'OnPathStart($id, $fillType)'; +} + +@immutable +class OnSize { + const OnSize(this.width, this.height); + + final double width; + final double height; + + @override + int get hashCode => Object.hash(width, height); + + @override + bool operator ==(Object other) => + other is OnSize && other.width == width && other.height == height; + + @override + String toString() => 'OnSize($width, $height)'; +} + +@immutable +class OnTextConfig { + const OnTextConfig( + this.text, + this.xAnchorMultiplier, + this.fontSize, + this.fontFamily, + this.fontWeight, + this.decoration, + this.decorationStyle, + this.decorationColor, + this.id, + ); + + final String text; + final double xAnchorMultiplier; + final double fontSize; + final String? fontFamily; + final int fontWeight; + final int id; + final int decoration; + final int decorationStyle; + final int decorationColor; + + @override + int get hashCode => Object.hash( + text, + xAnchorMultiplier, + fontSize, + fontFamily, + fontWeight, + decoration, + decorationStyle, + decorationColor, + id, + ); + + @override + bool operator ==(Object other) => + other is OnTextConfig && + other.text == text && + other.xAnchorMultiplier == xAnchorMultiplier && + other.fontSize == fontSize && + other.fontFamily == fontFamily && + other.fontWeight == fontWeight && + other.decoration == decoration && + other.decorationStyle == decorationStyle && + other.decorationColor == decorationColor && + other.id == id; + + @override + String toString() => + 'OnTextConfig($text, (anchor: $xAnchorMultiplier), $fontSize, $fontFamily, $fontWeight, $decoration, $decorationStyle, $decorationColor, $id)'; +} + +@immutable +class OnDrawText { + const OnDrawText(this.textId, this.fillId, this.strokeId, this.patternId); + + final int textId; + final int? fillId; + final int? strokeId; + final int? patternId; + + @override + int get hashCode => Object.hash(textId, fillId, strokeId, patternId); + + @override + bool operator ==(Object other) => + other is OnDrawText && + other.textId == textId && + other.fillId == fillId && + other.strokeId == strokeId && + other.patternId == patternId; + + @override + String toString() => 'OnDrawText($textId, $fillId, $strokeId, $patternId)'; +} + +@immutable +class OnImage { + const OnImage(this.id, this.format, this.data, {this.onError}); + + final int id; + final int format; + final List data; + final VectorGraphicsErrorListener? onError; + + @override + int get hashCode => Object.hash(id, format, data, onError); + + @override + bool operator ==(Object other) => + other is OnImage && + other.id == id && + other.format == format && + other.onError == onError && + _listEquals(other.data, data); + + @override + String toString() => 'OnImage($id, $format, data:${data.length} bytes)'; +} + +@immutable +class OnDrawImage { + const OnDrawImage( + this.id, + this.x, + this.y, + this.width, + this.height, + this.transform, + ); + + final int id; + final double x; + final double y; + final double width; + final double height; + final List? transform; + + @override + int get hashCode => Object.hash(id, x, y, width, height); + + @override + bool operator ==(Object other) { + return other is OnDrawImage && + other.id == id && + other.x == x && + other.y == y && + other.width == width && + other.height == height && + _listEquals(other.transform, transform); + } + + @override + String toString() => 'OnDrawImage($id, $x, $y, $width, $height, $transform)'; +} + +@immutable +class OnPatternStart { + const OnPatternStart( + this.patternId, this.x, this.y, this.width, this.height, this.transform); + + final int patternId; + final double x; + final double y; + final double width; + final double height; + final Float64List transform; + + @override + int get hashCode => + Object.hash(patternId, x, y, width, height, Object.hashAll(transform)); + + @override + bool operator ==(Object other) => + other is OnPatternStart && + other.patternId == patternId && + other.x == x && + other.y == y && + other.width == width && + other.height == height && + _listEquals(other.transform, transform); + + @override + String toString() => + 'OnPatternStart($patternId, $x, $y, $width, $height, $transform)'; +} + +bool _listEquals(List? left, List? right) { + if (left == null && right == null) { + return true; + } + if (left == null || right == null) { + return false; + } + if (left.length != right.length) { + return false; + } + for (int i = 0; i < left.length; i++) { + if (left[i] != right[i]) { + return false; + } + } + return true; +} diff --git a/packages/vector_graphics_compiler/test/helpers.dart b/packages/vector_graphics_compiler/test/helpers.dart new file mode 100644 index 00000000000..c25e6b0832d --- /dev/null +++ b/packages/vector_graphics_compiler/test/helpers.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:vector_graphics_compiler/src/svg/node.dart'; + +List queryChildren(Node node) { + final List children = []; + void visitor(Node child) { + if (child is T) { + children.add(child); + } + child.visitChildren(visitor); + } + + node.visitChildren(visitor); + return children; +} diff --git a/packages/vector_graphics_compiler/test/masking_optimizer_test.dart b/packages/vector_graphics_compiler/test/masking_optimizer_test.dart new file mode 100644 index 00000000000..fe9e4c4dd60 --- /dev/null +++ b/packages/vector_graphics_compiler/test/masking_optimizer_test.dart @@ -0,0 +1,299 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:core'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/masking_optimizer.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'helpers.dart'; +import 'test_svg_strings.dart'; + +Node parseAndResolve(String source) { + final Node node = parseToNodeTree(source); + final ResolvingVisitor visitor = ResolvingVisitor(); + return node.accept(visitor, AffineMatrix.identity); +} + +const String xmlString = + ''''''; + +void main() { + setUpAll(() { + if (!initializePathOpsFromFlutterCache()) { + fail('error in setup'); + } + }); + + test('Only remove MaskNode if the mask is described by a singular PathNode', + () { + final Node node = parseAndResolve(''' + + + + + + + +'''); + + final MaskingOptimizer visitor = MaskingOptimizer(); + final Node newNode = visitor.apply(node); + + final List maskNodesNew = + queryChildren(newNode); + + expect(maskNodesNew.length, 0); + }); + + test("Don't remove MaskNode if the mask is described by multiple PathNodes", + () { + final Node node = parseAndResolve(''' + + + + + + + + +'''); + final MaskingOptimizer visitor = MaskingOptimizer(); + final Node newNode = visitor.apply(node); + + final List maskNodesNew = + queryChildren(newNode); + + expect(maskNodesNew.length, 1); + }); + + test( + "Don't resolve a MaskNode if one of PathNodes it's applied to has stroke.width set", + () { + final Node node = parseAndResolve(''' + + + + + + + + +'''); + + final MaskingOptimizer visitor = MaskingOptimizer(); + final Node newNode = visitor.apply(node); + + final List maskNodesNew = + queryChildren(newNode); + + expect(maskNodesNew.length, 1); + }); + + test("Don't remove MaskNode if intersection of Mask and Path is empty", () { + final Node node = parseAndResolve(''' + + + + + + +'''); + final MaskingOptimizer visitor = MaskingOptimizer(); + final Node newNode = visitor.apply(node); + + final List maskNodesNew = + queryChildren(newNode); + expect(maskNodesNew.length, 1); + }); + test('ParentNode and PathNode count should stay the same', () { + final Node node = parseAndResolve(xmlString); + + final List pathNodesOld = + queryChildren(node); + final List parentNodesOld = queryChildren(node); + + final MaskingOptimizer visitor = MaskingOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + final List parentNodesNew = queryChildren(newNode); + + expect(pathNodesOld.length, pathNodesNew.length); + expect(parentNodesOld.length, parentNodesNew.length); + }); + + test('Masks on groups', () { + final VectorInstructions instructions = + parse(groupMask, enableMaskingOptimizer: false); + expect(instructions.paths, [ + parseSvgPathData( + 'M 17.438 8.438 C 17.748 8.438 18 8.69 18 9 L 18 16.313 C 17.99834725871 17.24440923535 17.24341005121 17.99889920517 16.312 18 L 1.688 18 C 0.75620021668 17.99889792932 0.00110207068 17.24379978332 0 16.312 L 0 9 C 0.01271270943 8.69855860173 0.26079065383 8.46072235233 0.5625 8.46072235233 C 0.86420934617 8.46072235233 1.11228729057 8.69855860173 1.125 9 L 1.125 16.313 C 1.125 16.622 1.377 16.875 1.688 16.875 L 16.312 16.875 C 16.622 16.875 16.875 16.622 16.875 16.312 L 16.875 9 C 16.875 8.69 17.127 8.437 17.438 8.437 Z M 9 0 C 9.169 0 9.316 0.079 9.418 0.196 L 9.423 0.192 L 13.361 4.692 C 13.443 4.795 13.5 4.921 13.5 5.062 C 13.5 5.373 13.248 5.625 12.937 5.625 C 12.77572417052 5.6238681172 12.62300981305 5.55226042805 12.519 5.429 L 12.514 5.433 L 9.563 2.06 L 9.563 11.812 C 9.56299999183 12.12293630838 9.31093630838 12.3749999852 9 12.3749999852 C 8.68906369162 12.3749999852 8.43700000817 12.12293630838 8.437 11.812 L 8.437 2.06 L 5.486 5.433 C 5.37775998399 5.5529360201 5.22453705399 5.62248401669 5.063 5.625 C 4.75206368585 5.625 4.5 5.37293631415 4.5 5.062 C 4.5 4.921 4.557 4.795 4.644 4.696 L 4.639 4.692 L 8.577 0.192 C 8.68524001601 0.0720639799 8.83846294601 0.00251598331 9 0 Z', + PathFillType.evenOdd) + .transformed(const AffineMatrix(0.00000000000000006123233995736766, 1, + -1, 0.00000000000000006123233995736766, 21, 3)), + parseSvgPathData( + 'M -3 -3 L 21 -3 L 21 21 L -3 21 Z', PathFillType.evenOdd) + .transformed(const AffineMatrix(1, 0, 0, 1, 3, 3)), + parseSvgPathData( + 'M 17.438 8.438 C 17.748 8.438 18 8.69 18 9 L 18 16.313 C 17.99834725871 17.24440923535 17.24341005121 17.99889920517 16.312 18 L 1.688 18 C 0.75620021668 17.99889792932 0.00110207068 17.24379978332 0 16.312 L 0 9 C 0.01271270943 8.69855860173 0.26079065383 8.46072235233 0.5625 8.46072235233 C 0.86420934617 8.46072235233 1.11228729057 8.69855860173 1.125 9 L 1.125 16.313 C 1.125 16.622 1.377 16.875 1.688 16.875 L 16.312 16.875 C 16.622 16.875 16.875 16.622 16.875 16.312 L 16.875 9 C 16.875 8.69 17.127 8.437 17.438 8.437 Z M 9 0 C 9.169 0 9.316 0.079 9.418 0.196 L 9.423 0.192 L 13.361 4.692 C 13.443 4.795 13.5 4.921 13.5 5.062 C 13.5 5.373 13.248 5.625 12.937 5.625 C 12.77572417052 5.6238681172 12.62300981305 5.55226042805 12.519 5.429 L 12.514 5.433 L 9.563 2.06 L 9.563 11.812 C 9.56299999183 12.12293630838 9.31093630838 12.3749999852 9 12.3749999852 C 8.68906369162 12.3749999852 8.43700000817 12.12293630838 8.437 11.812 L 8.437 2.06 L 5.486 5.433 C 5.37775998399 5.5529360201 5.22453705399 5.62248401669 5.063 5.625 C 4.75206368585 5.625 4.5 5.37293631415 4.5 5.062 C 4.5 4.921 4.557 4.795 4.644 4.696 L 4.639 4.692 L 8.577 0.192 C 8.68524001601 0.0720639799 8.83846294601 0.00251598331 9 0 Z', + PathFillType.evenOdd) + .transformed(const AffineMatrix(1, 0, 0, 1, 3, 3)), + ]); + + final VectorInstructions instructionsWithOptimizer = parse(groupMask); + expect(instructionsWithOptimizer.paths, groupMaskForMaskingOptimizer); + + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xff727272))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff8e93a1))), + Paint(fill: Fill(color: Color(0xffffffff))) + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.saveLayer, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 2), + DrawCommand(DrawCommandType.mask), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 3), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.restore) + ]); + }); + + test('Handles masks with blends and gradients correctly', () { + final VectorInstructions instructions = parse( + blendAndMask, + enableClippingOptimizer: false, + enableMaskingOptimizer: false, + enableOverdrawOptimizer: false, + ); + expect( + instructions.paths, + [ + PathBuilder().addOval(const Rect.fromCircle(50, 50, 50)).toPath(), + PathBuilder().addOval(const Rect.fromCircle(50, 50, 40)).toPath(), + ], + ); + + final VectorInstructions instructionsWithOptimizer = parse(blendAndMask); + expect(instructionsWithOptimizer.paths, blendsAndMasksForMaskingOptimizer); + + const LinearGradient gradient1 = LinearGradient( + id: 'url(#linearGradient-3)', + from: Point(46.9782516, 60.9121966), + to: Point(60.42279469999999, 90.6839734), + colors: [Color(0xffffffff), Color(0xff0000ff)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ); + const LinearGradient gradient2 = LinearGradient( + id: 'url(#linearGradient-3)', + from: Point(47.58260128, 58.72975728), + to: Point(58.338235759999996, 82.54717871999999), + colors: [Color(0xffffffff), Color(0xff0000ff)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ); + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xffadd8e6))), + Paint( + blendMode: BlendMode.multiply, + fill: Fill(), + ), + Paint( + blendMode: BlendMode.multiply, + fill: Fill(color: Color(0x98ffffff), shader: gradient1), + ), + Paint(fill: Fill(color: Color(0x98ffffff), shader: gradient2)), + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.saveLayer, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 2), + DrawCommand(DrawCommandType.mask), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 3), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.restore) + ]); + }); + + test('Does not partially apply mask to some children but not others', () { + final VectorInstructions instructions = parse(''' + + + + + + + + + +'''); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.saveLayer, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 2), + DrawCommand(DrawCommandType.mask), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 3), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.restore), + ]); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(44.1855, 464.814), + LineToCommand(244.564, 464.814), + LineToCommand(244.564, 64.0564), + LineToCommand(44.1855, 64.0564), + LineToCommand(44.1855, 464.814), + CloseCommand(), + MoveToCommand(45.8428, 462.333), + LineToCommand(242.081, 462.333), + LineToCommand(242.081, 65.7158), + LineToCommand(45.8428, 65.7158), + LineToCommand(45.8428, 462.333), + CloseCommand() + ], + fillType: PathFillType.evenOdd, + ), + Path( + commands: const [ + MoveToCommand(103.803, 481.375), + LineToCommand(184.948, 481.375) + ], + ), + Path( + commands: const [ + MoveToCommand(21.5625, 0.0), + LineToCommand(267.1875, 0.0), + CubicToCommand(279.0881677156519, 0.0, 288.75, 9.661832284348126, + 288.75, 21.5625), + LineToCommand(288.75, 506.4375), + CubicToCommand(288.75, 518.3381677156518, 279.0881677156519, 528.0, + 267.1875, 528.0), + LineToCommand(21.5625, 528.0), + CubicToCommand( + 9.661832284348126, 528.0, 0.0, 518.3381677156518, 0.0, 506.4375), + LineToCommand(0.0, 21.5625), + CubicToCommand( + 0.0, 9.661832284348126, 9.661832284348126, 0.0, 21.5625, 0.0), + CloseCommand() + ], + ), + ]); + }); +} diff --git a/packages/vector_graphics_compiler/test/matrix_test.dart b/packages/vector_graphics_compiler/test/matrix_test.dart new file mode 100644 index 00000000000..3f2a6a10aea --- /dev/null +++ b/packages/vector_graphics_compiler/test/matrix_test.dart @@ -0,0 +1,178 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/parsers.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; +import 'package:vector_math/vector_math_64.dart'; + +void main() { + test('scaleStrokeWidth', () { + expect(AffineMatrix.identity.scaleStrokeWidth(null), null); + expect(AffineMatrix.identity.scaleStrokeWidth(1), 1); + expect(AffineMatrix.identity.scaleStrokeWidth(2), 2); + expect(AffineMatrix.identity.rotated(1.2).scaleStrokeWidth(1), 1); + expect(AffineMatrix.identity.rotated(1.2).scaleStrokeWidth(2), 2); + + expect(AffineMatrix.identity.scaled(2.0).scaleStrokeWidth(1), 2); + expect(AffineMatrix.identity.scaled(2.0).scaleStrokeWidth(2), 4); + expect( + AffineMatrix.identity.scaled(2.0).rotated(1.2).scaleStrokeWidth(1), 2); + expect( + AffineMatrix.identity.scaled(2.0).rotated(1.2).scaleStrokeWidth(2), 4); + + expect(AffineMatrix.identity.scaled(2.0, 1.0).scaleStrokeWidth(1), 1.5); + expect(AffineMatrix.identity.scaled(2.0, 1.0).scaleStrokeWidth(2), 3); + expect( + AffineMatrix.identity.scaled(2.0, 1.0).rotated(1.2).scaleStrokeWidth(1), + 1.5); + expect( + AffineMatrix.identity.scaled(2.0, 1.0).rotated(1.2).scaleStrokeWidth(2), + 3); + }); + + test('Parse rotate and scale', () { + // Regression test for https://github.com/dnfield/flutter_svg/issues/801 + final AffineMatrix mat = parseTransform('rotate(-1 4 -12) scale(2)')!; + expect( + mat, + AffineMatrix.identity + .translated(4, -12) + .rotated(radians(-1)) + .translated(-4, 12) + .scaled(2), + ); + }); + + test('Identity matrix', () { + expect(AffineMatrix.identity.toMatrix4(), Matrix4.identity().storage); + }); + + test('Multiply', () { + const AffineMatrix matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + const AffineMatrix matrix2 = AffineMatrix(7, 8, 9, 10, 11, 12); + + final Matrix4 matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + final Matrix4 matrix4_2 = Matrix4.fromFloat64List(matrix2.toMatrix4()); + expect( + matrix1.multiplied(matrix2).toMatrix4(), + matrix4_1.multiplied(matrix4_2).storage, + ); + }); + + test('Scale', () { + const AffineMatrix matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final Matrix4 matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + expect( + matrix1.scaled(2, 3).toMatrix4(), + matrix4_1.scaled(2.0, 3.0).storage, + ); + + expect( + matrix1.scaled(2).toMatrix4(), + matrix4_1.scaled(2.0, 2.0).storage, + ); + }); + + test('Scale and multiply', () { + const AffineMatrix matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + const AffineMatrix matrix2 = AffineMatrix(7, 8, 9, 10, 11, 12); + + final Matrix4 matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + final Matrix4 matrix4_2 = Matrix4.fromFloat64List(matrix2.toMatrix4()); + expect( + matrix1.scaled(2, 3).multiplied(matrix2).toMatrix4(), + matrix4_1.scaled(2.0, 3.0).multiplied(matrix4_2).storage, + ); + }); + + test('Multiply handles the extra matrix4 scale value', () { + final AffineMatrix matrix1 = AffineMatrix.identity.scaled(2, 3); + final AffineMatrix matrix2 = AffineMatrix.identity.multiplied(matrix1); + + expect(matrix1, matrix2); + }); + + test('Translate', () { + const AffineMatrix matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final Matrix4 matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + matrix4_1.translate(2.0, 3.0); + expect( + matrix1.translated(2, 3).toMatrix4(), + matrix4_1.storage, + ); + }); + + test('Rotate', () { + const AffineMatrix matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final Matrix4 matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()) + ..rotateZ(31.0); + expect( + matrix1.rotated(31).toMatrix4(), + matrix4_1.storage, + ); + }); + + test('transformRect', () { + const double epsillon = .0000001; + const Rect rectangle20x20 = Rect.fromLTRB(10, 20, 30, 40); + + // Identity + expect( + AffineMatrix.identity.transformRect(rectangle20x20), + rectangle20x20, + ); + + // 2D Scaling + expect( + AffineMatrix.identity.scaled(2).transformRect(rectangle20x20), + const Rect.fromLTRB(20, 40, 60, 80), + ); + + // Rotation + final Rect rotatedRect = AffineMatrix.identity + .rotated(math.pi / 2.0) + .transformRect(rectangle20x20); + expect(rotatedRect.left + 40, lessThan(epsillon)); + expect(rotatedRect.top - 10, lessThan(epsillon)); + expect(rotatedRect.right + 20, lessThan(epsillon)); + expect(rotatedRect.bottom - 30, lessThan(epsillon)); + + // Translation + final Rect shiftedRect = + AffineMatrix.identity.translated(10, 20).transformRect(rectangle20x20); + + expect(shiftedRect.left, rectangle20x20.left + 10); + expect(shiftedRect.top, rectangle20x20.top + 20); + expect(shiftedRect.right, rectangle20x20.right + 10); + expect(shiftedRect.bottom, rectangle20x20.bottom + 20); + }); + + test('== and hashCode account for hidden field', () { + const AffineMatrix matrixA = AffineMatrix.identity; + const AffineMatrix matrixB = AffineMatrix(1, 0, 0, 1, 0, 0, 0); + + expect(matrixA != matrixB, true); + expect(matrixA.hashCode != matrixB.hashCode, true); + }); + + test('encodableInRect', () { + final AffineMatrix matrixA = AffineMatrix.identity.scaled(2, 3); + final AffineMatrix matrixB = AffineMatrix.identity.scaled(2, -2); + final AffineMatrix matrixC = AffineMatrix.identity.xSkewed(5); + final AffineMatrix matrixD = AffineMatrix.identity.ySkewed(5); + final AffineMatrix matrixE = AffineMatrix.identity.rotated(1.3); + + expect(matrixA.encodableInRect, true); + expect(matrixB.encodableInRect, false); + expect(matrixC.encodableInRect, false); + expect(matrixD.encodableInRect, false); + expect(matrixE.encodableInRect, false); + }); +} diff --git a/packages/vector_graphics_compiler/test/node_test.dart b/packages/vector_graphics_compiler/test/node_test.dart new file mode 100644 index 00000000000..072f4c8583a --- /dev/null +++ b/packages/vector_graphics_compiler/test/node_test.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('TextPosition uses computed transform', () { + final TextPositionNode node = TextPositionNode( + SvgAttributes.forTest( + x: DoubleOrPercentage.fromString('5'), + y: DoubleOrPercentage.fromString('3'), + dx: DoubleOrPercentage.fromString('2'), + dy: DoubleOrPercentage.fromString('1'), + transform: AffineMatrix.identity.translated(10, 10), + ), + reset: false, + ); + + final TextPosition position = node.computeTextPosition( + const Rect.fromLTWH(0, 0, 500, 500), + AffineMatrix.identity, + ); + + expect(position.x, 15); + expect(position.y, 13); + expect(position.dx, 12); + expect(position.dy, 11); + }); + + test('TextNode returns null for Paint if stroke and fill are missing', () { + final TextNode node = TextNode( + 'text', + SvgAttributes.empty, + ); + expect(node.computePaint(Rect.largest, AffineMatrix.identity), null); + }); + + test('PathNode returns null for Paint if stroke and fill are missing', () { + final PathNode node = PathNode( + Path(), + SvgAttributes.empty, + ); + expect(node.computePaint(Rect.largest, AffineMatrix.identity), null); + }); +} diff --git a/packages/vector_graphics_compiler/test/overdraw_optimizer_test.dart b/packages/vector_graphics_compiler/test/overdraw_optimizer_test.dart new file mode 100644 index 00000000000..61f9062aa3c --- /dev/null +++ b/packages/vector_graphics_compiler/test/overdraw_optimizer_test.dart @@ -0,0 +1,599 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/overdraw_optimizer.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; +import 'helpers.dart'; +import 'test_svg_strings.dart'; + +Node parseAndResolve(String source) { + final Node node = parseToNodeTree(source); + final ResolvingVisitor visitor = ResolvingVisitor(); + return node.accept(visitor, AffineMatrix.identity); +} + +void main() { + setUpAll(() { + if (!initializePathOpsFromFlutterCache()) { + fail('error in setup'); + } + }); + + test( + 'Basic case of two opaque shapes overlapping with a stroke (cannot be optimized yet)', + () { + final Node node = parseAndResolve(basicOverlapWithStroke); + final VectorInstructions instructions = parse(basicOverlapWithStroke); + + final List pathNodesOld = + queryChildren(node); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesOld.length, pathNodesNew.length); + + expect(instructions.paints, const [ + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color(0xff008000)), + fill: Fill(color: Color(0xffff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff0000ff))) + ]); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(99.0, 221.5), + LineToCommand(692.0, 221.5), + LineToCommand(692.0, 316.5), + LineToCommand(99.0, 316.5), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(367.0, 41.50001), + LineToCommand(448.0, 41.50001), + LineToCommand(448.0, 527.49999), + LineToCommand(367.0, 527.49999), + CloseCommand() + ], + ) + ]); + }); + + test('Basic case of two opaque shapes overlapping', () { + final Node node = parseAndResolve(basicOverlap); + final VectorInstructions instructions = parse(basicOverlap); + + final List pathNodesOld = + queryChildren(node); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesOld.length, pathNodesNew.length); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xffff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff0000ff))) + ]); + + expect(instructions.paths, [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(367.0, 221.5), + LineToCommand(99.0, 221.5), + LineToCommand(99.0, 316.5), + LineToCommand(367.0, 316.5), + LineToCommand(367.0, 221.5), + CloseCommand(), + MoveToCommand(448.0, 221.5), + LineToCommand(448.0, 316.5), + LineToCommand(692.0, 316.5), + LineToCommand(692.0, 221.5), + LineToCommand(448.0, 221.5), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(367.0, 41.50001), + LineToCommand(448.0, 41.50001), + LineToCommand(448.0, 527.49999), + LineToCommand(367.0, 527.49999), + CloseCommand() + ], + ) + ]); + }); + + test('Basic case of two shapes with opacity < 1.0 overlapping', () { + final Node node = parseAndResolve(opacityOverlap); + final VectorInstructions instructions = parse(opacityOverlap); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesNew.length, 3); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x7fff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x4c0000ff))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xa58a0075))) + ]); + + expect(instructions.paths, [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(343.0, 240.5), + LineToCommand(88.0, 240.5), + LineToCommand(88.0, 366.5), + LineToCommand(343.0, 366.5), + LineToCommand(343.0, 240.5), + CloseCommand(), + MoveToCommand(484.0, 240.5), + LineToCommand(484.0, 366.5), + LineToCommand(711.0, 366.5), + LineToCommand(711.0, 240.5), + LineToCommand(484.0, 240.5), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(484.0, 63.5), + LineToCommand(343.0, 63.5), + LineToCommand(343.0, 240.5), + LineToCommand(484.0, 240.5), + LineToCommand(484.0, 63.5), + CloseCommand(), + MoveToCommand(484.0, 366.5), + LineToCommand(343.0, 366.5), + LineToCommand(343.0, 565.5), + LineToCommand(484.0, 565.5), + LineToCommand(484.0, 366.5), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(343.0, 240.5), + LineToCommand(484.0, 240.5), + LineToCommand(484.0, 366.5), + LineToCommand(343.0, 366.5), + CloseCommand() + ], + ) + ]); + }); + + test('Solid shape overlapping semi-transparent shape', () { + final Node node = parseAndResolve(solidOverTrasnparent); + final VectorInstructions instructions = parse(solidOverTrasnparent); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesNew.length, 2); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x7fff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff0000ff))) + ]); + + expect(instructions.paths, [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(343.0, 240.5), + LineToCommand(88.0, 240.5), + LineToCommand(88.0, 366.5), + LineToCommand(343.0, 366.5), + LineToCommand(343.0, 240.5), + CloseCommand(), + MoveToCommand(484.0, 240.5), + LineToCommand(484.0, 366.5), + LineToCommand(711.0, 366.5), + LineToCommand(711.0, 240.5), + LineToCommand(484.0, 240.5), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(343.0, 63.5), + LineToCommand(484.0, 63.5), + LineToCommand(484.0, 565.50001), + LineToCommand(343.0, 565.50001), + CloseCommand() + ], + ) + ]); + }); + + test('Semi-transparent shape overlapping solid shape', () { + final Node node = parseAndResolve(transparentOverSolid); + final VectorInstructions instructions = parse(transparentOverSolid); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesNew.length, 3); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xffff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x7f0000ff))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff80007f))) + ]); + + expect(instructions.paths, [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(343.0, 240.5), + LineToCommand(88.0, 240.5), + LineToCommand(88.0, 366.5), + LineToCommand(343.0, 366.5), + LineToCommand(343.0, 240.5), + CloseCommand(), + MoveToCommand(484.0, 240.5), + LineToCommand(484.0, 366.5), + LineToCommand(711.0, 366.5), + LineToCommand(711.0, 240.5), + LineToCommand(484.0, 240.5), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(484.0, 63.5), + LineToCommand(343.0, 63.5), + LineToCommand(343.0, 240.5), + LineToCommand(484.0, 240.5), + LineToCommand(484.0, 63.5), + CloseCommand(), + MoveToCommand(484.0, 366.5), + LineToCommand(343.0, 366.5), + LineToCommand(343.0, 565.5), + LineToCommand(484.0, 565.5), + LineToCommand(484.0, 366.5), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(343.0, 240.5), + LineToCommand(484.0, 240.5), + LineToCommand(484.0, 366.5), + LineToCommand(343.0, 366.5), + CloseCommand() + ], + ) + ]); + }); + + test('Does not attempt to optimize overdraw when a mask is involved', () { + final Node node = parseAndResolve(''' + + + + + + + + + +'''); + + final Node result = OverdrawOptimizer().apply(node); + expect( + queryChildren(result), + queryChildren(node), + ); + }); + + test('Multiple opaque and semi-trasnparent shapes', () { + final Node node = parseAndResolve(complexOpacityTest); + final VectorInstructions instructions = parse(complexOpacityTest); + + final OverdrawOptimizer visitor = OverdrawOptimizer(); + final Node newNode = visitor.apply(node); + + final List pathNodesNew = + queryChildren(newNode); + + expect(pathNodesNew.length, 22); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff0000ff))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xffff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xccff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x99ff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x66ff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x33ff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff008000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xbfff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xbf008000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x7fff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x7f008000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x3fff0000))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x3f008000))) + ]); + + expect(instructions.paths, [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(150.0, 100.0), + LineToCommand(100.0, 100.0), + LineToCommand(100.0, 250.0), + LineToCommand(1100.0, 250.0), + LineToCommand(1100.0, 100.0), + LineToCommand(250.0, 100.0), + CubicToCommand(250.0, 127.59574890136719, 227.5957489013672, 150.0, + 200.0, 150.0), + CubicToCommand(172.4042510986328, 150.0, 150.0, 127.59574890136719, + 150.0, 100.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(200.0, 50.0), + CubicToCommand( + 227.5957489013672, 50.0, 250.0, 72.40425109863281, 250.0, 100.0), + CubicToCommand(250.0, 127.59574890136719, 227.5957489013672, 150.0, + 200.0, 150.0), + CubicToCommand(172.4042510986328, 150.0, 150.0, 127.59574890136719, + 150.0, 100.0), + CubicToCommand( + 150.0, 72.40425109863281, 172.4042510986328, 50.0, 200.0, 50.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(400.0, 50.0), + CubicToCommand( + 427.59576416015625, 50.0, 450.0, 72.40425109863281, 450.0, 100.0), + CubicToCommand(450.0, 127.59574890136719, 427.59576416015625, 150.0, + 400.0, 150.0), + CubicToCommand(372.40423583984375, 150.0, 350.0, 127.59574890136719, + 350.0, 100.0), + CubicToCommand( + 350.0, 72.40425109863281, 372.40423583984375, 50.0, 400.0, 50.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(600.0, 50.0), + CubicToCommand( + 627.5957641601562, 50.0, 650.0, 72.40425109863281, 650.0, 100.0), + CubicToCommand(650.0, 127.59574890136719, 627.5957641601562, 150.0, + 600.0, 150.0), + CubicToCommand(572.4042358398438, 150.0, 550.0, 127.59574890136719, + 550.0, 100.0), + CubicToCommand( + 550.0, 72.40425109863281, 572.4042358398438, 50.0, 600.0, 50.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(800.0, 50.0), + CubicToCommand( + 827.5957641601562, 50.0, 850.0, 72.40425109863281, 850.0, 100.0), + CubicToCommand(850.0, 127.59574890136719, 827.5957641601562, 150.0, + 800.0, 150.0), + CubicToCommand(772.4042358398438, 150.0, 750.0, 127.59574890136719, + 750.0, 100.0), + CubicToCommand( + 750.0, 72.40425109863281, 772.4042358398438, 50.0, 800.0, 50.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(1000.0, 50.0), + CubicToCommand( + 1027.595703125, 50.0, 1050.0, 72.40425109863281, 1050.0, 100.0), + CubicToCommand( + 1050.0, 127.59574890136719, 1027.595703125, 150.0, 1000.0, 150.0), + CubicToCommand(972.4042358398438, 150.0, 950.0, 127.59574890136719, + 950.0, 100.0), + CubicToCommand( + 950.0, 72.40425109863281, 972.4042358398438, 50.0, 1000.0, 50.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(200.0000457763672, 203.1529998779297), + CubicToCommand(194.55233764648438, 201.1146697998047, + 188.6553192138672, 200.0, 182.5, 200.0), + CubicToCommand( + 154.9042510986328, 200.0, 132.5, 222.4042510986328, 132.5, 250.0), + CubicToCommand(132.5, 277.59576416015625, 154.9042510986328, 300.0, + 182.5, 300.0), + CubicToCommand(188.65528869628906, 300.0, 194.55230712890625, + 298.88531494140625, 200.0, 296.8470153808594), + CubicToCommand(181.02427673339844, 289.7470703125, 167.5, + 271.4404602050781, 167.5, 250.0), + CubicToCommand(167.5, 228.55953979492188, 181.02427673339844, + 210.2529296875, 200.0000457763672, 203.1529998779297), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(217.5, 200.0), + CubicToCommand( + 245.0957489013672, 200.0, 267.5, 222.4042510986328, 267.5, 250.0), + CubicToCommand(267.5, 277.59576416015625, 245.0957489013672, 300.0, + 217.5, 300.0), + CubicToCommand(189.9042510986328, 300.0, 167.5, 277.59576416015625, + 167.5, 250.0), + CubicToCommand( + 167.5, 222.4042510986328, 189.9042510986328, 200.0, 217.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(382.5, 200.0), + CubicToCommand(410.09576416015625, 200.0, 432.5, 222.4042510986328, + 432.5, 250.0), + CubicToCommand(432.5, 277.59576416015625, 410.09576416015625, 300.0, + 382.5, 300.0), + CubicToCommand(354.90423583984375, 300.0, 332.5, 277.59576416015625, + 332.5, 250.0), + CubicToCommand(332.5, 222.4042510986328, 354.90423583984375, 200.0, + 382.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(417.5, 200.0), + CubicToCommand(445.09576416015625, 200.0, 467.5, 222.4042510986328, + 467.5, 250.0), + CubicToCommand(467.5, 277.59576416015625, 445.09576416015625, 300.0, + 417.5, 300.0), + CubicToCommand(389.90423583984375, 300.0, 367.5, 277.59576416015625, + 367.5, 250.0), + CubicToCommand(367.5, 222.4042510986328, 389.90423583984375, 200.0, + 417.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(582.5, 200.0), + CubicToCommand( + 610.0957641601562, 200.0, 632.5, 222.4042510986328, 632.5, 250.0), + CubicToCommand(632.5, 277.59576416015625, 610.0957641601562, 300.0, + 582.5, 300.0), + CubicToCommand(554.9042358398438, 300.0, 532.5, 277.59576416015625, + 532.5, 250.0), + CubicToCommand( + 532.5, 222.4042510986328, 554.9042358398438, 200.0, 582.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(617.5, 200.0), + CubicToCommand( + 645.0957641601562, 200.0, 667.5, 222.4042510986328, 667.5, 250.0), + CubicToCommand(667.5, 277.59576416015625, 645.0957641601562, 300.0, + 617.5, 300.0), + CubicToCommand(589.9042358398438, 300.0, 567.5, 277.59576416015625, + 567.5, 250.0), + CubicToCommand( + 567.5, 222.4042510986328, 589.9042358398438, 200.0, 617.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(817.5, 200.0), + CubicToCommand( + 845.0957641601562, 200.0, 867.5, 222.4042510986328, 867.5, 250.0), + CubicToCommand(867.5, 277.59576416015625, 845.0957641601562, 300.0, + 817.5, 300.0), + CubicToCommand(789.9042358398438, 300.0, 767.5, 277.59576416015625, + 767.5, 250.0), + CubicToCommand( + 767.5, 222.4042510986328, 789.9042358398438, 200.0, 817.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(782.5, 200.0), + CubicToCommand( + 810.0957641601562, 200.0, 832.5, 222.4042510986328, 832.5, 250.0), + CubicToCommand(832.5, 277.59576416015625, 810.0957641601562, 300.0, + 782.5, 300.0), + CubicToCommand(754.9042358398438, 300.0, 732.5, 277.59576416015625, + 732.5, 250.0), + CubicToCommand( + 732.5, 222.4042510986328, 754.9042358398438, 200.0, 782.5, 200.0), + CloseCommand() + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(982.5, 200.0), + CubicToCommand(1010.0957641601562, 200.0, 1032.5, 222.4042510986328, + 1032.5, 250.0), + CubicToCommand(1032.5, 277.59576416015625, 1010.0957641601562, 300.0, + 982.5, 300.0), + CubicToCommand(954.9042358398438, 300.0, 932.5, 277.59576416015625, + 932.5, 250.0), + CubicToCommand( + 932.5, 222.4042510986328, 954.9042358398438, 200.0, 982.5, 200.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(1017.5, 200.0), + CubicToCommand( + 1045.0957512247, 200.0, 1067.5, 222.4042487753, 1067.5, 250.0), + CubicToCommand( + 1067.5, 277.5957512247, 1045.0957512247, 300.0, 1017.5, 300.0), + CubicToCommand( + 989.9042487753, 300.0, 967.5, 277.5957512247, 967.5, 250.0), + CubicToCommand( + 967.5, 222.4042487753, 989.9042487753, 200.0, 1017.5, 200.0), + CloseCommand() + ], + ) + ]); + }); +} diff --git a/packages/vector_graphics_compiler/test/paint_test.dart b/packages/vector_graphics_compiler/test/paint_test.dart new file mode 100644 index 00000000000..6621269e39f --- /dev/null +++ b/packages/vector_graphics_compiler/test/paint_test.dart @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('Color tests', () { + expect( + const Color.fromRGBO(10, 15, 20, .1), + const Color.fromARGB(25, 10, 15, 20), + ); + + expect( + const Color.fromARGB(255, 10, 15, 20).withOpacity(.1), + const Color.fromARGB(25, 10, 15, 20), + ); + + const Color testColor = Color(0xFFABCDEF); + expect(testColor.r, 0xAB); + expect(testColor.g, 0xCD); + expect(testColor.b, 0xEF); + }); + + test('LinearGradient can be converted to local coordinates', () { + const LinearGradient gradient = LinearGradient( + id: 'test', + from: Point.zero, + to: Point(1, 1), + colors: [Color.opaqueBlack, Color(0xFFABCDEF)], + tileMode: TileMode.mirror, + offsets: [0.0, 1.0], + transform: AffineMatrix.identity, + ); + + final LinearGradient transformed = gradient.applyBounds( + const Rect.fromLTWH(5, 5, 100, 100), + AffineMatrix.identity, + ); + + expect(transformed.from, const Point(5, 5)); + expect(transformed.to, const Point(105, 105)); + }); + + test('LinearGradient applied bounds with userSpaceOnUse', () { + const LinearGradient gradient = LinearGradient( + id: 'test', + from: Point.zero, + to: Point(1, 1), + colors: [Color.opaqueBlack, Color(0xFFABCDEF)], + tileMode: TileMode.mirror, + offsets: [0.0, 1.0], + transform: AffineMatrix.identity, + unitMode: GradientUnitMode.userSpaceOnUse, + ); + + final LinearGradient transformed = gradient.applyBounds( + const Rect.fromLTWH(5, 5, 100, 100), + AffineMatrix.identity, + ); + + expect(transformed.from, Point.zero); + expect(transformed.to, const Point(1, 1)); + }); + + test('LinearGradient applied bounds with userSpaceOnUse and transformed', () { + final LinearGradient gradient = LinearGradient( + id: 'test', + from: Point.zero, + to: const Point(1, 1), + colors: const [Color.opaqueBlack, Color(0xFFABCDEF)], + tileMode: TileMode.mirror, + offsets: const [0.0, 1.0], + transform: AffineMatrix.identity.scaled(2), + unitMode: GradientUnitMode.userSpaceOnUse, + ); + + final LinearGradient transformed = gradient.applyBounds( + const Rect.fromLTWH(5, 5, 100, 100), + AffineMatrix.identity, + ); + + expect(transformed.from, Point.zero); + expect(transformed.to, const Point(2, 2)); + }); + + test('RadialGradient can be converted to local coordinates', () { + const RadialGradient gradient = RadialGradient( + id: 'test', + center: Point(0.5, 0.5), + radius: 10, + colors: [Color(0xFFFFFFAA), Color(0xFFABCDEF)], + tileMode: TileMode.clamp, + transform: AffineMatrix.identity, + focalPoint: Point(0.6, 0.6), + offsets: [.1, .9], + ); + + final RadialGradient transformed = gradient.applyBounds( + const Rect.fromLTWH(5, 5, 100, 100), + AffineMatrix.identity.translated(5, 5).scaled(100, 100), + ); + + expect(transformed.center, const Point(.5, .5)); + expect(transformed.focalPoint, const Point(.6, .6)); + expect( + transformed.transform, + AffineMatrix.identity + .translated(5, 5) + .scaled(100, 100) + .multiplied(AffineMatrix.identity.translated(5, 5).scaled(100, 100)), + ); + }); + + test('RadialGradient applied bounds with userSpaceOnUse', () { + const RadialGradient gradient = RadialGradient( + id: 'test', + center: Point(0.5, 0.5), + radius: 10, + colors: [Color(0xFFFFFFAA), Color(0xFFABCDEF)], + tileMode: TileMode.clamp, + transform: AffineMatrix.identity, + focalPoint: Point(0.6, 0.6), + offsets: [.1, .9], + unitMode: GradientUnitMode.userSpaceOnUse, + ); + + final RadialGradient transformed = gradient.applyBounds( + const Rect.fromLTWH(5, 5, 100, 100), + AffineMatrix.identity, + ); + + expect(transformed.center, const Point(0.5, 0.5)); + expect(transformed.focalPoint, const Point(0.6, 0.6)); + expect(transformed.transform, AffineMatrix.identity); + }); +} diff --git a/packages/vector_graphics_compiler/test/parser_test.dart b/packages/vector_graphics_compiler/test/parser_test.dart new file mode 100644 index 00000000000..24743c82f5f --- /dev/null +++ b/packages/vector_graphics_compiler/test/parser_test.dart @@ -0,0 +1,7680 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/numbers.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; +import 'test_svg_strings.dart'; + +void main() { + test('Reuse ID self-referentially', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + +'''); + + expect(instructions.paths.length, 1); + }); + + test('Self-referentially ID', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + +'''); + + expect(instructions.paths.length, 0); + }); + + test('Text transform but no xy', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + 東急電鉄路線図 + Tōkyū Railways route map + + +'''); + + expect(instructions.text, const [ + TextConfig( + '東急電鉄路線図', + 0.0, + 'Arimo,Liberation Sans,HammersmithOne,Helvetica,Arial,sans-serif', + FontWeight.w600, + 40.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'Tōkyū Railways route map', + 0.0, + 'Arimo,Liberation Sans,HammersmithOne,Helvetica,Arial,sans-serif', + FontWeight.w600, + 22.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + ]); + expect(instructions.textPositions, [ + TextPosition( + reset: true, transform: AffineMatrix.identity.translated(60, 45)), + TextPosition( + reset: true, transform: AffineMatrix.identity.translated(60, 75)), + ]); + }); + + test('Fill rule inheritence', () { + final VectorInstructions instructions = + parseWithoutOptimizers(inheritFillRule); + + expect(instructions.paints, const [ + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color(0xffff0000)), + fill: Fill(color: Color.opaqueBlack), + ), + ]); + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(60.0, 10.0), + LineToCommand(31.0, 100.0), + LineToCommand(108.0, 45.0), + LineToCommand(12.0, 45.0), + LineToCommand(89.0, 100.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(160.0, 10.0), + LineToCommand(131.0, 100.0), + LineToCommand(208.0, 45.0), + LineToCommand(112.0, 45.0), + LineToCommand(189.0, 100.0), + CloseCommand() + ], + fillType: PathFillType.evenOdd, + ) + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 0), + ]); + }); + + test('Text whitespace handling (number bubbles)', () { + final VectorInstructions instructions = + parseWithoutOptimizers(numberBubbles); + + expect( + instructions.textPositions, + const [ + TextPosition(reset: true), + TextPosition(x: 28.727, y: 12.0), + TextPosition(x: 52.727, y: 12.0), + TextPosition(x: 4.728, y: 12.0), + ], + ); + + expect(instructions.text, const [ + TextConfig( + '2', + 0.0, + 'AvenirNext-Medium, Avenir Next', + FontWeight.w400, + 11.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + '3', + 0.0, + 'AvenirNext-Medium, Avenir Next', + FontWeight.w400, + 11.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + '1', + 0.0, + 'AvenirNext-Medium, Avenir Next', + FontWeight.w400, + 11.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + ]); + }); + + test('None on fill', () { + const String svg = ''' + + + + + + + + + + + + + + + +'''; + + final VectorInstructions instructions = parseWithoutOptimizers(svg); + // Should _not_ contain a paint with an opaque black fill for the rect with class "frame-background". + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xff22c55e))), + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0xfff59e0b))), + ]); + }); + + test('text spacing', () { + const String svg = ''' + + + + π( D² - d² )( N - N + u + ) + + + +'''; + + final VectorInstructions instructions = parseWithoutOptimizers(svg); + + expect(instructions.textPositions, const [ + TextPosition(reset: true, x: 35.081, y: 15.0), + TextPosition(y: 15.0), + ]); + + expect(instructions.text, const [ + TextConfig( + 'π( D² - d² )( N - N', + 0.0, + 'OpenSans-Italic, Open Sans', + FontWeight.w400, + 14.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + ' u', + 0.0, + 'OpenSans-Italic, Open Sans', + FontWeight.w400, + 10.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + ' )', + 0.0, + 'OpenSans-Italic, Open Sans', + FontWeight.w400, + 14.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + ]); + }); + + test('stroke-opacity', () { + const String strokeOpacitySvg = ''' + + + +'''; + + final VectorInstructions instructions = + parseWithoutOptimizers(strokeOpacitySvg); + + expect( + instructions.paints.single, + const Paint(stroke: Stroke(color: Color(0x7fff0000))), + ); + }); + + test('text attributes are preserved', () { + final VectorInstructions instructions = parseWithoutOptimizers(textTspan); + expect( + instructions.text, + const [ + TextConfig( + 'Some text', + 0.0, + 'Roboto-Regular, Roboto', + FontWeight.w400, + 16.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'more text.', + 0.0, + 'Roboto-Regular, Roboto', + FontWeight.w400, + 16.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'Even more text', + 0.0, + 'Roboto-Regular, Roboto', + FontWeight.w400, + 16.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'text everywhere', + 0.0, + 'Roboto-Regular, Roboto', + FontWeight.w400, + 16.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'so many lines', + 0.0, + 'Roboto-Regular, Roboto', + FontWeight.w400, + 16.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + ], + ); + }); + + test('currentColor', () { + const String currentColorSvg = ''' + + + +'''; + + final VectorInstructions blueInstructions = parseWithoutOptimizers( + currentColorSvg, + theme: const SvgTheme(currentColor: Color(0xFF0000FF)), + ); + final VectorInstructions redInstructions = parseWithoutOptimizers( + currentColorSvg, + theme: const SvgTheme(currentColor: Color(0xFFFF0000)), + ); + + expect( + blueInstructions.paints.single, + const Paint(fill: Fill(color: Color(0xFF0000FF))), + ); + + expect( + redInstructions.paints.single, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + }); + + test('Opacity with a save layer does not continue to inherit', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + +'''); + + expect(instructions.paints, const [ + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x06202124))), + // The paint for the saveLayer. + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color(0x0a000000))), + // The paint for the path drawn in the saveLayer - must not be the same as + // the saveLayer otherwise the path will be drawn almost completely transparent. + Paint(blendMode: BlendMode.srcOver, fill: Fill(color: Color.opaqueBlack)), + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.saveLayer, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 2), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('Opacity on a default fill', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + +'''); + + expect(instructions.paints.single.fill!.color, const Color(0x66000000)); + }); + + test('Stroke width with scaling', () { + final VectorInstructions instructions = parseWithoutOptimizers( + signWithScaledStroke, + ); + + expect(instructions.paints, const [ + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xffffee44), join: StrokeJoin.round, width: 3.0)), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xff333333), join: StrokeJoin.round, width: 3.0), + fill: Fill(color: Color(0xffffee44))), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color(0xffccaa00), join: StrokeJoin.round), + fill: Fill(color: Color(0xffccaa00))), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color(0xff333333), join: StrokeJoin.round), + fill: Fill(color: Color(0xff555555))), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xff446699), join: StrokeJoin.round, width: 0.5)), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xffbbaa55), + join: StrokeJoin.round, + width: 0.49999999999999994)), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xff6688cc), + join: StrokeJoin.round, + width: 0.49999999999999994)), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xff333311), join: StrokeJoin.round, width: 0.5)), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xffffee44), join: StrokeJoin.round, width: 0.5), + fill: Fill(color: Color(0xff80a3cf))), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke( + color: Color(0xffffee44), join: StrokeJoin.round, width: 0.5), + fill: Fill(color: Color(0xff668899))) + ]); + }); + + test('Use handles stroke and fill correctly', () { + final VectorInstructions instructions = parseWithoutOptimizers( + useStar, + ); + + // These kinds of paths are verified elsewhere, and the FP math can vary + // by platform. + expect(instructions.paths.length, 4); + + expect( + instructions.paints, + const [ + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color.opaqueBlack, width: 12.0), + ), + Paint( + blendMode: BlendMode.srcOver, + stroke: Stroke(color: Color(0xff008000)), + fill: Fill(color: Color(0xffffbb44)), + ), + ], + ); + + expect( + instructions.commands, + const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 1), + ], + ); + }); + + test('Use preserves fill from shape', () { + final VectorInstructions instructions = parseWithoutOptimizers( + useColor, + ); + + expect( + instructions.paths, + [ + Path( + commands: const [ + MoveToCommand(60.0, 10.0), + LineToCommand(72.5, 27.5), + LineToCommand(47.5, 27.5), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(120.0, 10.0), + LineToCommand(132.5, 27.5), + LineToCommand(107.5, 27.5), + CloseCommand() + ], + ) + ], + ); + expect( + instructions.paints.single, + const Paint( + blendMode: BlendMode.srcOver, + fill: Fill(color: Color(0xffff0000)), + ), + ); + }); + + test('Image in defs', () { + final VectorInstructions instructions = parseWithoutOptimizers( + imageInDefs, + ); + expect(instructions.images.single.format, 0); + expect(instructions.images.single.data.length, 331); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.clip, objectId: 0), + DrawCommand(DrawCommandType.image, objectId: 0), + DrawCommand(DrawCommandType.restore) + ]); + }); + + test('Transformed clip', () { + final VectorInstructions instructions = parseWithoutOptimizers( + transformedClip, + ); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(0.0, 0.0), + LineToCommand(375.0, 0.0), + LineToCommand(375.0, 407.0), + LineToCommand(0.0, 407.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(360.0, 395.5), + LineToCommand(16.0, 395.5), + LineToCommand(188.0, 1.0), + LineToCommand(360.0, 395.5), + CloseCommand() + ], + ) + ]); + }); + + test('Zero width stroke', () { + final VectorInstructions instructions = parseWithoutOptimizers( + ''' + + +''', + ); + + expect(instructions.paints.single.stroke, null); + expect( + instructions.paints.single.fill, const Fill(color: Color(0xFFFFFFFF))); + }); + + test('text anchor', () { + final VectorInstructions instructions = parseWithoutOptimizers( + textAnchors, + ); + + expect(instructions.text, const [ + TextConfig( + 'Text anchor start', + 0.0, + 'Roboto', + FontWeight.w400, + 10.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'Text anchor middle', + 0.5, + 'Roboto', + FontWeight.w400, + 10.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + TextConfig( + 'Text anchor end', + 1.0, + 'Roboto', + FontWeight.w400, + 10.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ) + ]); + }); + + test('text decorations', () { + final VectorInstructions instructions = parseWithoutOptimizers( + textDecorations, + ); + + expect(instructions.text, const [ + TextConfig( + 'Overline text', + 0, + 'Roboto', + FontWeight.w400, + 55.0, + TextDecoration.overline, + TextDecorationStyle.solid, + Color(0xffff0000), + ), + TextConfig( + 'Strike text', + 0, + 'Roboto', + FontWeight.w400, + 55.0, + TextDecoration.lineThrough, + TextDecorationStyle.solid, + Color(0xff008000), + ), + TextConfig( + 'Underline text', + 0, + 'Roboto', + FontWeight.w400, + 55.0, + TextDecoration.underline, + TextDecorationStyle.double, + Color(0xff008000), + ) + ]); + }); + + test('Stroke property set but does not draw stroke', () { + final VectorInstructions instructions = parseWithoutOptimizers( + strokePropertyButNoStroke, + ); + expect(instructions.paths.single.commands, const [ + MoveToCommand(10.0, 20.0), + LineToCommand(110.0, 20.0), + LineToCommand(110.0, 120.0), + LineToCommand(10.0, 120.0), + CloseCommand(), + ]); + expect( + instructions.paints.single, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + }); + + test('Clip with use', () { + final VectorInstructions instructions = parseWithoutOptimizers( + basicClip, + ); + final VectorInstructions instructions2 = parseWithoutOptimizers( + useClip, + ); + expect(instructions, instructions2); + }); + + test('stroke-dasharray="none"', () { + final VectorInstructions instructions = parseWithoutOptimizers( + ''' + + + +''', + ); + + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xffff0000))), + Paint(stroke: Stroke(color: Color.opaqueBlack, width: 2.0)), + ]); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(1.0, 20.0), + LineToCommand(20.0, 20.0), + LineToCommand(20.0, 39.0), + LineToCommand(30.0, 30.0), + LineToCommand(1.0, 26.0), + CloseCommand(), + ], + ), + ]); + }); + + test('Dashed path', () { + final VectorInstructions instructions = parseWithoutOptimizers( + ''' + + +''', + ); + + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xffff0000))), + Paint(stroke: Stroke(color: Color.opaqueBlack, width: 2.0)), + ]); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(1.0, 20.0), + LineToCommand(20.0, 20.0), + LineToCommand(20.0, 39.0), + LineToCommand(30.0, 30.0), + LineToCommand(1.0, 26.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(1.0, 20.0), + LineToCommand(6.0, 20.0), + MoveToCommand(9.0, 20.0), + LineToCommand(13.999999999999998, 20.0), + MoveToCommand(18.999999999999996, 20.0), + LineToCommand(20.0, 20.0), + LineToCommand(20.0, 24.0), + MoveToCommand(20.0, 27.000000000000004), + LineToCommand(20.0, 32.0), + MoveToCommand(20.0, 37.0), + LineToCommand(20.0, 39.0), + LineToCommand(22.229882438741498, 36.99310580513265), + MoveToCommand(24.459764877482996, 34.9862116102653), + LineToCommand(28.17623560871883, 31.641387952153053), + MoveToCommand(27.47750617803373, 29.65206981765983), + LineToCommand(22.524400531816358, 28.96888283197467), + MoveToCommand(19.55253714408593, 28.55897064056358), + LineToCommand(14.599431497868558, 27.875783654878425), + MoveToCommand(9.646325851651186, 27.19259666919327), + LineToCommand(4.693220205433812, 26.509409683508114), + MoveToCommand(1.7213568177033882, 26.09949749209702), + LineToCommand(1.0, 26.0), + LineToCommand(1.0, 21.72818638368261) + ], + ), + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 1), + ]); + }); + + test('text with transform', () { + final VectorInstructions instructions = parseWithoutOptimizers( + 'a', + ); + expect(instructions.paints.single, const Paint(fill: Fill())); + expect( + instructions.textPositions.single, + TextPosition( + reset: true, + transform: AffineMatrix.identity + .translated(-100, 50) + .rotated(radians(10)) + .translated(100, -50), + ), + ); + expect( + instructions.text.single, + const TextConfig( + 'a', + 0, + null, + normalFontWeight, + 16, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ), + ); + }); + + test('Missing references', () { + final VectorInstructions instructions = parseWithoutOptimizers( + missingRefs, + ); + expect( + instructions.paints.single, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + expect( + instructions.paths.single, + PathBuilder().addRect(const Rect.fromLTWH(5, 5, 100, 100)).toPath(), + ); + expect( + instructions.commands.single, + const DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + ); + }); + + test('focal radial', () { + final VectorInstructions instructions = parseWithoutOptimizers( + focalRadial, + ); + + expect( + instructions.paints.single, + const Paint( + stroke: Stroke(color: Color.opaqueBlack), + fill: Fill( + color: Color(0xffffffff), + shader: RadialGradient( + id: 'url(#radial)', + center: Point(0.5, 0.5), + radius: 0.5, + colors: [ + Color(0xffff0000), + Color(0xff008000), + Color(0xff0000ff) + ], + offsets: [0.0, 0.5, 1.0], + tileMode: TileMode.clamp, + transform: AffineMatrix( + 120.0, + 0.0, + 0.0, + 120.0, + 10.0, + 10.0, + 120.0, + ), + focalPoint: Point(0.5, 0.15), + unitMode: GradientUnitMode.transformed, + ), + ), + ), + ); + expect( + instructions.paths.single, + PathBuilder().addRect(const Rect.fromLTWH(10, 10, 120, 120)).toPath(), + ); + }); + + test('Transformed userSpaceOnUse radial', () { + final VectorInstructions instructions = parseWithoutOptimizers( + xformUsosRadial, + ); + expect( + instructions.paints.single, + const Paint( + fill: Fill( + color: Color(0xffffffff), + shader: RadialGradient( + id: 'url(#paint0_radial)', + center: Point.zero, + radius: 1.0, + colors: [Color(0xcc47e9ff), Color(0x00414cbe)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + transform: AffineMatrix( + -433.0004488023628, + -350.99987486173313, + 350.99987486173313, + -433.0004488023628, + 432.9999999999999, + 547.0000000000001, + 557.396, + ), + unitMode: GradientUnitMode.transformed, + ), + ), + ), + ); + expect( + instructions.paths.single, + PathBuilder() + .addRect(const Rect.fromLTWH(667, 667, 667, 667)) + .toPath() + .transformed( + AffineMatrix.identity + .translated(667, 667) + .rotated(radians(180)) + .translated(-667, -667), + ), + ); + }); + + test('Transformed objectBoundingBox gradient onto transformed path', () { + final VectorInstructions instructions = parseWithoutOptimizers( + xformObbGradient, + ); + expect( + instructions.paints.single, + const Paint( + fill: Fill( + color: Color(0xffffffff), + shader: LinearGradient( + id: 'url(#paint1_linear)', + from: Point(405.5634918610405, 547.9898987322333), + to: Point(440.9188309203679, 866.1879502661797), + colors: [Color(0x7f0000ff), Color(0x19ff0000)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ), + ), + ), + ); + expect( + instructions.paths.single, + PathBuilder() + .addRect(const Rect.fromLTWH(300, 0, 500, 400)) + .toPath() + .transformed( + AffineMatrix.identity + .translated(0, 100) + .translated(250, 250) + .rotated(radians(45)) + .translated(-250, -250), + ), + ); + }, + // Currently skipped because the double values in Point are tested for + // exact equality, which makes this test fragile to host platform. + skip: Platform.isWindows); + + test('Opaque blend mode gets a save layer', () { + const String svg = ''' + + + + + + +'''; + final VectorInstructions instructions = parseWithoutOptimizers( + svg, + ); + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xffff0000))), + Paint(blendMode: BlendMode.screen, fill: Fill(color: Color.opaqueBlack)), + Paint(blendMode: BlendMode.screen, fill: Fill(color: Color(0xff008000))), + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.saveLayer, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 2), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('Stroke properties respected in toStroke', () { + const String svg = ''' + + + +'''; + final VectorInstructions instructions = parseWithoutOptimizers( + svg, + ); + expect( + instructions.paints.single, + const Paint( + stroke: Stroke( + color: Color(0xffff0000), + cap: StrokeCap.round, + join: StrokeJoin.round, + width: 2.7, + ), + ), + ); + }); + + test('gradients can handle inheriting unit mode', () { + final VectorInstructions instructions = parseWithoutOptimizers( + linearGradientThatInheritsUnitMode, + ); + expect(instructions.paints, const [ + Paint( + fill: Fill( + color: Color(0xffffffff), + shader: LinearGradient( + id: 'url(#a)', + from: Point(236.702, 9.99), + to: Point(337.966, 241.771), + colors: [ + Color(0xffffffff), + Color(0xb9c2c3c3), + Color(0x6a7d7e80), + Color(0x314b4c4e), + Color(0x0d2c2d30), + Color(0x00202124) + ], + offsets: [0.0, 0.229, 0.508, 0.739, 0.909, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ), + ), + ), + Paint( + fill: Fill( + color: Color(0xffffffff), + shader: LinearGradient( + id: 'url(#d)', + from: Point(0.0, 50.243), + to: Point(0.0, 330.779), + colors: [ + Color(0xffffffff), + Color(0xb9c2c3c3), + Color(0x6a7d7e80), + Color(0x314b4c4e), + Color(0x0d2c2d30), + Color(0x00202124) + ], + offsets: [0.0, 0.229, 0.508, 0.739, 0.909, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ), + ), + ) + ]); + }); + + test('group opacity results in save layer', () { + final VectorInstructions instructions = parseWithoutOptimizers( + groupOpacity, + ); + expect(instructions.paths, [ + PathBuilder().addOval(const Rect.fromCircle(80, 100, 50)).toPath(), + PathBuilder().addOval(const Rect.fromCircle(120, 100, 50)).toPath(), + ]); + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0x7f000000))), + Paint(fill: Fill(color: Color(0x7fff0000))), + Paint(fill: Fill(color: Color(0x7f008000))), + ]); + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.saveLayer, paintId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 2), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('xlink gradient Out of order', () { + final VectorInstructions instructions = parseWithoutOptimizers( + xlinkGradient, + ); + final VectorInstructions instructions2 = parseWithoutOptimizers( + xlinkGradientOoO, + ); + + expect(instructions.paints, instructions2.paints); + expect(instructions.paths, instructions2.paths); + expect(instructions.commands, instructions2.commands); + }); + + test('xlink use Out of order', () { + final VectorInstructions instructions = parseWithoutOptimizers( + simpleUseCircles, + ); + final VectorInstructions instructions2 = parseWithoutOptimizers( + simpleUseCirclesOoO, + ); + + // Use toSet to ignore ordering differences. + expect(instructions.paints.toSet(), instructions2.paints.toSet()); + expect(instructions.paths.toSet(), instructions2.paths.toSet()); + expect(instructions.commands.toSet(), instructions2.commands.toSet()); + }); + + test('xlink gradient with transform', () { + final VectorInstructions instructions = parseWithoutOptimizers( + xlinkGradient, + ); + expect(instructions.paths, [ + PathBuilder() + .addOval(const Rect.fromCircle(-83.533, 122.753, 74.461)) + .toPath() + .transformed( + const AffineMatrix(.63388, 0, 0, .63388, 100.15, -30.611)), + ]); + + expect(instructions.paints, const [ + Paint( + fill: Fill( + color: Color(0xffffffff), + shader: LinearGradient( + id: 'url(#b)', + from: Point(0.000763280000001032, 47.19967163999999), + to: Point(94.40007452, 47.19967163999999), + colors: [Color(0xff0f12cb), Color(0xfffded3a)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ), + ), + ) + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + ]); + }); + + test('Out of order def', () { + final VectorInstructions instructions = parseWithoutOptimizers( + outOfOrderGradientDef, + ); + expect(instructions.paths, [ + parseSvgPathData( + 'M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10z'), + ]); + expect(instructions.paints, const [ + Paint( + fill: Fill( + color: Color(0xffffffff), + shader: LinearGradient( + id: 'url(#paint0_linear)', + from: Point(10.0, 0.0), + to: Point(10.0, 19.852), + colors: [Color(0xff0000ff), Color(0xffffff00)], + offsets: [0.0, 1.0], + tileMode: TileMode.clamp, + unitMode: GradientUnitMode.transformed, + ), + ), + ) + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + ]); + }); + + test('Handles masks correctly', () { + final VectorInstructions instructions = parseWithoutOptimizers( + basicMask, + ); + expect( + instructions.paths, + [ + parseSvgPathData('M-10,110 110,110 110,-10z'), + PathBuilder().addOval(const Rect.fromCircle(50, 50, 50)).toPath(), + PathBuilder().addRect(const Rect.fromLTWH(0, 0, 100, 100)).toPath(), + parseSvgPathData( + 'M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z'), + ].map((Path path) => + path.transformed(AffineMatrix.identity.translated(10, 10))), + ); + + expect(instructions.paints, const [ + Paint(fill: Fill(color: Color(0xffffa500))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffffffff))), + ]); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.saveLayer, paintId: 1), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 1), + DrawCommand(DrawCommandType.mask), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 2), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 1), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.restore), + ]); + }); + + test('Handles viewBox transformations correctly', () { + const String svg = ''' + + + +'''; + final VectorInstructions instructions = parseWithoutOptimizers( + svg, + ); + expect(instructions.paths, [ + PathBuilder() + .addRect(const Rect.fromLTWH(11, 36, 31, 20)) + .toPath() + .transformed(AffineMatrix.identity.translated(10, 12)), + ]); + }); + + test('Parses rrects correctly', () { + const String svg = ''' + + + +'''; + final VectorInstructions instructions = parseWithoutOptimizers( + svg, + ); + expect(instructions.paths, [ + PathBuilder() + .addRRect(const Rect.fromLTWH(11, 36, 31, 20), 2.5, 2.5) + .toPath() + ]); + }); + + test('Path with empty paint does not draw anything', () { + final VectorInstructions instructions = parseWithoutOptimizers( + ''' + + +''', + key: 'emptyPath', + warningsAsErrors: true, + ); + expect(instructions.commands.isEmpty, true); + }); + + test('Use circles test', () { + final VectorInstructions instructions = parseWithoutOptimizers( + simpleUseCircles, + key: 'useCircles', + warningsAsErrors: true, + ); + + expect( + instructions.paints, + const [ + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff0000ff))), + Paint( + stroke: Stroke(color: Color(0xff0000ff)), + fill: Fill(color: Color(0xffffffff)), + ), + ], + ); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(5.0, 1.0), + CubicToCommand( + 7.2076600979759995, 1.0, 9.0, 2.792339902024, 9.0, 5.0), + CubicToCommand( + 9.0, 7.2076600979759995, 7.2076600979759995, 9.0, 5.0, 9.0), + CubicToCommand( + 2.792339902024, 9.0, 1.0, 7.2076600979759995, 1.0, 5.0), + CubicToCommand(1.0, 2.792339902024, 2.792339902024, 1.0, 5.0, 1.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(15.0, 1.0), + CubicToCommand(17.207660097976, 1.0, 19.0, 2.792339902024, 19.0, 5.0), + CubicToCommand( + 19.0, 7.2076600979759995, 17.207660097976, 9.0, 15.0, 9.0), + CubicToCommand( + 12.792339902024, 9.0, 11.0, 7.2076600979759995, 11.0, 5.0), + CubicToCommand(11.0, 2.792339902024, 12.792339902024, 1.0, 15.0, 1.0), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(25.0, 1.0), + CubicToCommand(27.207660097976, 1.0, 29.0, 2.792339902024, 29.0, 5.0), + CubicToCommand( + 29.0, 7.2076600979759995, 27.207660097976, 9.0, 25.0, 9.0), + CubicToCommand( + 22.792339902024, 9.0, 21.0, 7.2076600979759995, 21.0, 5.0), + CubicToCommand(21.0, 2.792339902024, 22.792339902024, 1.0, 25.0, 1.0), + CloseCommand() + ], + ), + ]); + + expect( + instructions.commands, + const [ + DrawCommand(DrawCommandType.path, + objectId: 0, paintId: 0, debugString: 'myCircle'), + DrawCommand(DrawCommandType.path, + objectId: 1, paintId: 1, debugString: 'myCircle'), + DrawCommand(DrawCommandType.path, + objectId: 2, paintId: 2, debugString: 'myCircle') + ], + ); + }); + + test('Use circles test without href', () { + final VectorInstructions instructions = parseWithoutOptimizers( + simpleUseCirclesWithoutHref, + key: 'useCirclesWithoutHref', + warningsAsErrors: true, + ); + + expect(instructions.paints, const [ + Paint( + fill: Fill(color: Color.opaqueBlack), + ), + ]); + + expect(instructions.paths, [ + Path( + commands: const [ + MoveToCommand(5.0, 1.0), + CubicToCommand( + 7.2076600979759995, 1.0, 9.0, 2.792339902024, 9.0, 5.0), + CubicToCommand( + 9.0, 7.2076600979759995, 7.2076600979759995, 9.0, 5.0, 9.0), + CubicToCommand( + 2.792339902024, 9.0, 1.0, 7.2076600979759995, 1.0, 5.0), + CubicToCommand(1.0, 2.792339902024, 2.792339902024, 1.0, 5.0, 1.0), + CloseCommand() + ], + ), + ]); + + expect( + instructions.commands, + const [ + DrawCommand(DrawCommandType.path, + objectId: 0, paintId: 0, debugString: 'myCircle'), + ], + ); + }); + + test('Parses pattern used as fill and stroke', () { + final VectorInstructions instructions = parseWithoutOptimizers( + starPatternCircles, + warningsAsErrors: true, + ); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.pattern, objectId: 0, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 1, patternId: 0), + DrawCommand(DrawCommandType.pattern, objectId: 0, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 2, patternId: 0) + ]); + }); + + test('Alternating pattern usage', () { + final VectorInstructions instructions = parseWithoutOptimizers( + alternatingPattern, + warningsAsErrors: true, + ); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.pattern, objectId: 0, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 1, paintId: 1, patternId: 0), + DrawCommand(DrawCommandType.pattern, objectId: 1, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 2), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 3, paintId: 1, patternId: 1), + DrawCommand(DrawCommandType.pattern, objectId: 0, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 4, paintId: 1, patternId: 0), + DrawCommand(DrawCommandType.pattern, objectId: 1, patternDataId: 0), + DrawCommand(DrawCommandType.path, objectId: 2, paintId: 2), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.path, objectId: 5, paintId: 1, patternId: 1) + ]); + }); + + test('Parses text with pattern as fill', () { + const String textWithPattern = ''' + + + + + + + Text +'''; + + final VectorInstructions instructions = parseWithoutOptimizers( + textWithPattern, + warningsAsErrors: true, + ); + + expect(instructions.commands, const [ + DrawCommand(DrawCommandType.pattern, objectId: 0), + DrawCommand(DrawCommandType.path, objectId: 0, paintId: 0), + DrawCommand(DrawCommandType.restore), + DrawCommand(DrawCommandType.textPosition, objectId: 0), + DrawCommand(DrawCommandType.text, objectId: 0, paintId: 1, patternId: 0) + ]); + + expect( + instructions.text, + const [ + TextConfig( + 'Text', + 0.0, + null, + FontWeight.w400, + 200.0, + TextDecoration.none, + TextDecorationStyle.solid, + Color.opaqueBlack, + ) + ], + ); + }); + + test('Defaults image height/width when not specified', () { + // 1x1 PNG image from png-pixel.com. + const String svgStr = ''' + + +'''; + + final VectorInstructions instructions = parseWithoutOptimizers( + svgStr, + key: 'image', + warningsAsErrors: true, + ); + + expect(instructions.drawImages.first.rect, const Rect.fromLTWH(0, 0, 1, 1)); + }); + + test('Other image formats', () { + // 1x1 PNG image from png-pixel.com. Claiming that it's JPEG and using "img" + // instead of "image" to make sure parser doesn't barf. Chrome is ok with + // this kind of nonsense. How far we have strayed. + const String svgStr = ''' + + +'''; + + final VectorInstructions instructions = parseWithoutOptimizers( + svgStr, + key: 'image', + warningsAsErrors: true, + ); + + expect(instructions.images.first.format, 1); + }); + + test('Ghostscript Tiger - dedupes paints', () { + final VectorInstructions instructions = parseWithoutOptimizers( + ghostscriptTiger, + key: 'ghostscriptTiger', + warningsAsErrors: true, + ); + + expect(instructions.paints.toSet(), ghostScriptTigerPaints.toSet()); + expect(instructions.paths, ghostScriptTigerPaths); + expect( + instructions.commands, + const [ + DrawCommand(DrawCommandType.path, + objectId: 0, paintId: 0, debugString: 'path8'), + DrawCommand(DrawCommandType.path, + objectId: 1, paintId: 0, debugString: 'path12'), + DrawCommand(DrawCommandType.path, + objectId: 2, paintId: 0, debugString: 'path16'), + DrawCommand(DrawCommandType.path, + objectId: 3, paintId: 0, debugString: 'path20'), + DrawCommand(DrawCommandType.path, + objectId: 4, paintId: 0, debugString: 'path24'), + DrawCommand(DrawCommandType.path, + objectId: 5, paintId: 0, debugString: 'path28'), + DrawCommand(DrawCommandType.path, + objectId: 6, paintId: 0, debugString: 'path32'), + DrawCommand(DrawCommandType.path, + objectId: 7, paintId: 0, debugString: 'path36'), + DrawCommand(DrawCommandType.path, + objectId: 8, paintId: 0, debugString: 'path40'), + DrawCommand(DrawCommandType.path, + objectId: 9, paintId: 0, debugString: 'path44'), + DrawCommand(DrawCommandType.path, + objectId: 10, paintId: 0, debugString: 'path48'), + DrawCommand(DrawCommandType.path, + objectId: 11, paintId: 0, debugString: 'path52'), + DrawCommand(DrawCommandType.path, + objectId: 12, paintId: 1, debugString: 'path56'), + DrawCommand(DrawCommandType.path, + objectId: 13, paintId: 2, debugString: 'path60'), + DrawCommand(DrawCommandType.path, + objectId: 14, paintId: 3, debugString: 'path64'), + DrawCommand(DrawCommandType.path, + objectId: 15, paintId: 4, debugString: 'path68'), + DrawCommand(DrawCommandType.path, + objectId: 16, paintId: 5, debugString: 'path72'), + DrawCommand(DrawCommandType.path, + objectId: 17, paintId: 6, debugString: 'path76'), + DrawCommand(DrawCommandType.path, + objectId: 18, paintId: 7, debugString: 'path80'), + DrawCommand(DrawCommandType.path, + objectId: 19, paintId: 8, debugString: 'path84'), + DrawCommand(DrawCommandType.path, + objectId: 20, paintId: 9, debugString: 'path88'), + DrawCommand(DrawCommandType.path, + objectId: 21, paintId: 10, debugString: 'path92'), + DrawCommand(DrawCommandType.path, + objectId: 22, paintId: 11, debugString: 'path96'), + DrawCommand(DrawCommandType.path, + objectId: 23, paintId: 12, debugString: 'path100'), + DrawCommand(DrawCommandType.path, + objectId: 24, paintId: 13, debugString: 'path104'), + DrawCommand(DrawCommandType.path, + objectId: 25, paintId: 14, debugString: 'path108'), + DrawCommand(DrawCommandType.path, + objectId: 26, paintId: 15, debugString: 'path112'), + DrawCommand(DrawCommandType.path, + objectId: 27, paintId: 16, debugString: 'path116'), + DrawCommand(DrawCommandType.path, + objectId: 28, paintId: 15, debugString: 'path120'), + DrawCommand(DrawCommandType.path, + objectId: 29, paintId: 16, debugString: 'path124'), + DrawCommand(DrawCommandType.path, + objectId: 30, paintId: 16, debugString: 'path128'), + DrawCommand(DrawCommandType.path, + objectId: 31, paintId: 16, debugString: 'path132'), + DrawCommand(DrawCommandType.path, + objectId: 32, paintId: 16, debugString: 'path136'), + DrawCommand(DrawCommandType.path, + objectId: 33, paintId: 16, debugString: 'path140'), + DrawCommand(DrawCommandType.path, + objectId: 34, paintId: 16, debugString: 'path144'), + DrawCommand(DrawCommandType.path, + objectId: 35, paintId: 15, debugString: 'path148'), + DrawCommand(DrawCommandType.path, + objectId: 36, paintId: 17, debugString: 'path152'), + DrawCommand(DrawCommandType.path, + objectId: 37, paintId: 18, debugString: 'path156'), + DrawCommand(DrawCommandType.path, + objectId: 38, paintId: 19, debugString: 'path160'), + DrawCommand(DrawCommandType.path, + objectId: 39, paintId: 20, debugString: 'path164'), + DrawCommand(DrawCommandType.path, + objectId: 40, paintId: 21, debugString: 'path168'), + DrawCommand(DrawCommandType.path, + objectId: 41, paintId: 22, debugString: 'path172'), + DrawCommand(DrawCommandType.path, + objectId: 42, paintId: 23, debugString: 'path176'), + DrawCommand(DrawCommandType.path, + objectId: 43, paintId: 21, debugString: 'path180'), + DrawCommand(DrawCommandType.path, + objectId: 44, paintId: 21, debugString: 'path184'), + DrawCommand(DrawCommandType.path, + objectId: 45, paintId: 21, debugString: 'path188'), + DrawCommand(DrawCommandType.path, + objectId: 46, paintId: 21, debugString: 'path192'), + DrawCommand(DrawCommandType.path, + objectId: 47, paintId: 21, debugString: 'path196'), + DrawCommand(DrawCommandType.path, + objectId: 48, paintId: 21, debugString: 'path200'), + DrawCommand(DrawCommandType.path, + objectId: 49, paintId: 24, debugString: 'path204'), + DrawCommand(DrawCommandType.path, + objectId: 50, paintId: 24, debugString: 'path208'), + DrawCommand(DrawCommandType.path, + objectId: 51, paintId: 21, debugString: 'path212'), + DrawCommand(DrawCommandType.path, + objectId: 52, paintId: 24, debugString: 'path216'), + DrawCommand(DrawCommandType.path, + objectId: 53, paintId: 24, debugString: 'path220'), + DrawCommand(DrawCommandType.path, + objectId: 54, paintId: 25, debugString: 'path224'), + DrawCommand(DrawCommandType.path, + objectId: 55, paintId: 21, debugString: 'path228'), + DrawCommand(DrawCommandType.path, + objectId: 56, paintId: 21, debugString: 'path232'), + DrawCommand(DrawCommandType.path, + objectId: 57, paintId: 21, debugString: 'path236'), + DrawCommand(DrawCommandType.path, + objectId: 58, paintId: 15, debugString: 'path240'), + DrawCommand(DrawCommandType.path, + objectId: 59, paintId: 21, debugString: 'path244'), + DrawCommand(DrawCommandType.path, + objectId: 60, paintId: 21, debugString: 'path248'), + DrawCommand(DrawCommandType.path, + objectId: 61, paintId: 21, debugString: 'path252'), + DrawCommand(DrawCommandType.path, + objectId: 62, paintId: 21, debugString: 'path256'), + DrawCommand(DrawCommandType.path, + objectId: 63, paintId: 21, debugString: 'path260'), + DrawCommand(DrawCommandType.path, + objectId: 64, paintId: 26, debugString: 'path264'), + DrawCommand(DrawCommandType.path, + objectId: 65, paintId: 26, debugString: 'path268'), + DrawCommand(DrawCommandType.path, + objectId: 66, paintId: 3, debugString: 'path272'), + DrawCommand(DrawCommandType.path, + objectId: 67, paintId: 27, debugString: 'path276'), + DrawCommand(DrawCommandType.path, + objectId: 68, paintId: 28, debugString: 'path280'), + DrawCommand(DrawCommandType.path, + objectId: 69, paintId: 29, debugString: 'path284'), + DrawCommand(DrawCommandType.path, + objectId: 70, paintId: 30, debugString: 'path288'), + DrawCommand(DrawCommandType.path, + objectId: 71, paintId: 14, debugString: 'path292'), + DrawCommand(DrawCommandType.path, + objectId: 72, paintId: 16, debugString: 'path296'), + DrawCommand(DrawCommandType.path, + objectId: 73, paintId: 15, debugString: 'path300'), + DrawCommand(DrawCommandType.path, + objectId: 74, paintId: 31, debugString: 'path304'), + DrawCommand(DrawCommandType.path, + objectId: 75, paintId: 32, debugString: 'path308'), + DrawCommand(DrawCommandType.path, + objectId: 76, paintId: 14, debugString: 'path312'), + DrawCommand(DrawCommandType.path, + objectId: 77, paintId: 15, debugString: 'path316'), + DrawCommand(DrawCommandType.path, + objectId: 78, paintId: 3, debugString: 'path320'), + DrawCommand(DrawCommandType.path, + objectId: 79, paintId: 14, debugString: 'path324'), + DrawCommand(DrawCommandType.path, + objectId: 80, paintId: 33, debugString: 'path328'), + DrawCommand(DrawCommandType.path, + objectId: 81, paintId: 34, debugString: 'path332'), + DrawCommand(DrawCommandType.path, + objectId: 82, paintId: 35, debugString: 'path336'), + DrawCommand(DrawCommandType.path, + objectId: 83, paintId: 14, debugString: 'path340'), + DrawCommand(DrawCommandType.path, + objectId: 84, paintId: 16, debugString: 'path344'), + DrawCommand(DrawCommandType.path, + objectId: 85, paintId: 15, debugString: 'path348'), + DrawCommand(DrawCommandType.path, + objectId: 86, paintId: 31, debugString: 'path352'), + DrawCommand(DrawCommandType.path, + objectId: 87, paintId: 15, debugString: 'path356'), + DrawCommand(DrawCommandType.path, + objectId: 88, paintId: 15, debugString: 'path360'), + DrawCommand(DrawCommandType.path, + objectId: 89, paintId: 15, debugString: 'path364'), + DrawCommand(DrawCommandType.path, + objectId: 90, paintId: 36, debugString: 'path368'), + DrawCommand(DrawCommandType.path, + objectId: 91, paintId: 37, debugString: 'path372'), + DrawCommand(DrawCommandType.path, + objectId: 92, paintId: 38, debugString: 'path376'), + DrawCommand(DrawCommandType.path, + objectId: 93, paintId: 16, debugString: 'path380'), + DrawCommand(DrawCommandType.path, + objectId: 94, paintId: 14, debugString: 'path384'), + DrawCommand(DrawCommandType.path, + objectId: 95, paintId: 39, debugString: 'path388'), + DrawCommand(DrawCommandType.path, + objectId: 96, paintId: 16, debugString: 'path392'), + DrawCommand(DrawCommandType.path, + objectId: 97, paintId: 16, debugString: 'path396'), + DrawCommand(DrawCommandType.path, + objectId: 98, paintId: 16, debugString: 'path400'), + DrawCommand(DrawCommandType.path, + objectId: 99, paintId: 16, debugString: 'path404'), + DrawCommand(DrawCommandType.path, + objectId: 100, paintId: 16, debugString: 'path408'), + DrawCommand(DrawCommandType.path, + objectId: 101, paintId: 15, debugString: 'path412'), + DrawCommand(DrawCommandType.path, + objectId: 102, paintId: 15, debugString: 'path416'), + DrawCommand(DrawCommandType.path, + objectId: 103, paintId: 3, debugString: 'path420'), + DrawCommand(DrawCommandType.path, + objectId: 104, paintId: 3, debugString: 'path424'), + DrawCommand(DrawCommandType.path, + objectId: 105, paintId: 3, debugString: 'path428'), + DrawCommand(DrawCommandType.path, + objectId: 106, paintId: 3, debugString: 'path432'), + DrawCommand(DrawCommandType.path, + objectId: 107, paintId: 3, debugString: 'path436'), + DrawCommand(DrawCommandType.path, + objectId: 108, paintId: 3, debugString: 'path440'), + DrawCommand(DrawCommandType.path, + objectId: 109, paintId: 15, debugString: 'path444'), + DrawCommand(DrawCommandType.path, + objectId: 110, paintId: 40, debugString: 'path448'), + DrawCommand(DrawCommandType.path, + objectId: 111, paintId: 40, debugString: 'path452'), + DrawCommand(DrawCommandType.path, + objectId: 112, paintId: 40, debugString: 'path456'), + DrawCommand(DrawCommandType.path, + objectId: 113, paintId: 40, debugString: 'path460'), + DrawCommand(DrawCommandType.path, + objectId: 114, paintId: 15, debugString: 'path464'), + DrawCommand(DrawCommandType.path, + objectId: 115, paintId: 41, debugString: 'path468'), + DrawCommand(DrawCommandType.path, + objectId: 116, paintId: 31, debugString: 'path472'), + DrawCommand(DrawCommandType.path, + objectId: 117, paintId: 32, debugString: 'path476'), + DrawCommand(DrawCommandType.path, + objectId: 118, paintId: 15, debugString: 'path480'), + DrawCommand(DrawCommandType.path, + objectId: 119, paintId: 15, debugString: 'path484'), + DrawCommand(DrawCommandType.path, + objectId: 120, paintId: 15, debugString: 'path488'), + DrawCommand(DrawCommandType.path, + objectId: 121, paintId: 42, debugString: 'path492'), + DrawCommand(DrawCommandType.path, + objectId: 122, paintId: 43, debugString: 'path496'), + DrawCommand(DrawCommandType.path, + objectId: 123, paintId: 39, debugString: 'path500'), + DrawCommand(DrawCommandType.path, + objectId: 124, paintId: 14, debugString: 'path504'), + DrawCommand(DrawCommandType.path, + objectId: 125, paintId: 39, debugString: 'path508'), + DrawCommand(DrawCommandType.path, + objectId: 126, paintId: 15, debugString: 'path512'), + DrawCommand(DrawCommandType.path, + objectId: 127, paintId: 15, debugString: 'path516'), + DrawCommand(DrawCommandType.path, + objectId: 128, paintId: 15, debugString: 'path520'), + DrawCommand(DrawCommandType.path, + objectId: 129, paintId: 15, debugString: 'path524'), + DrawCommand(DrawCommandType.path, + objectId: 130, paintId: 15, debugString: 'path528'), + DrawCommand(DrawCommandType.path, + objectId: 131, paintId: 15, debugString: 'path532'), + DrawCommand(DrawCommandType.path, + objectId: 132, paintId: 15, debugString: 'path536'), + DrawCommand(DrawCommandType.path, + objectId: 133, paintId: 15, debugString: 'path540'), + DrawCommand(DrawCommandType.path, + objectId: 134, paintId: 15, debugString: 'path544'), + DrawCommand(DrawCommandType.path, + objectId: 135, paintId: 14, debugString: 'path548'), + DrawCommand(DrawCommandType.path, + objectId: 136, paintId: 14, debugString: 'path552'), + DrawCommand(DrawCommandType.path, + objectId: 137, paintId: 16, debugString: 'path556'), + DrawCommand(DrawCommandType.path, + objectId: 138, paintId: 15, debugString: 'path560'), + DrawCommand(DrawCommandType.path, + objectId: 139, paintId: 16, debugString: 'path564'), + DrawCommand(DrawCommandType.path, + objectId: 140, paintId: 15, debugString: 'path568'), + DrawCommand(DrawCommandType.path, + objectId: 141, paintId: 15, debugString: 'path572'), + DrawCommand(DrawCommandType.path, + objectId: 142, paintId: 15, debugString: 'path576'), + DrawCommand(DrawCommandType.path, + objectId: 143, paintId: 15, debugString: 'path580'), + DrawCommand(DrawCommandType.path, + objectId: 144, paintId: 15, debugString: 'path584'), + DrawCommand(DrawCommandType.path, + objectId: 145, paintId: 15, debugString: 'path588'), + DrawCommand(DrawCommandType.path, + objectId: 146, paintId: 15, debugString: 'path592'), + DrawCommand(DrawCommandType.path, + objectId: 147, paintId: 15, debugString: 'path596'), + DrawCommand(DrawCommandType.path, + objectId: 148, paintId: 15, debugString: 'path600'), + DrawCommand(DrawCommandType.path, + objectId: 149, paintId: 15, debugString: 'path604'), + DrawCommand(DrawCommandType.path, + objectId: 150, paintId: 15, debugString: 'path608'), + DrawCommand(DrawCommandType.path, + objectId: 151, paintId: 15, debugString: 'path612'), + DrawCommand(DrawCommandType.path, + objectId: 152, paintId: 15, debugString: 'path616'), + DrawCommand(DrawCommandType.path, + objectId: 153, paintId: 15, debugString: 'path620'), + DrawCommand(DrawCommandType.path, + objectId: 154, paintId: 15, debugString: 'path624'), + DrawCommand(DrawCommandType.path, + objectId: 155, paintId: 15, debugString: 'path628'), + DrawCommand(DrawCommandType.path, + objectId: 156, paintId: 15, debugString: 'path632'), + DrawCommand(DrawCommandType.path, + objectId: 157, paintId: 39, debugString: 'path636'), + DrawCommand(DrawCommandType.path, + objectId: 158, paintId: 39, debugString: 'path640'), + DrawCommand(DrawCommandType.path, + objectId: 159, paintId: 16, debugString: 'path644'), + DrawCommand(DrawCommandType.path, + objectId: 160, paintId: 15, debugString: 'path648'), + DrawCommand(DrawCommandType.path, + objectId: 161, paintId: 15, debugString: 'path652'), + DrawCommand(DrawCommandType.path, + objectId: 162, paintId: 44, debugString: 'path656'), + DrawCommand(DrawCommandType.path, + objectId: 163, paintId: 44, debugString: 'path660'), + DrawCommand(DrawCommandType.path, + objectId: 164, paintId: 44, debugString: 'path664'), + DrawCommand(DrawCommandType.path, + objectId: 165, paintId: 44, debugString: 'path668'), + DrawCommand(DrawCommandType.path, + objectId: 166, paintId: 44, debugString: 'path672'), + DrawCommand(DrawCommandType.path, + objectId: 167, paintId: 44, debugString: 'path676'), + DrawCommand(DrawCommandType.path, + objectId: 168, paintId: 44, debugString: 'path680'), + DrawCommand(DrawCommandType.path, + objectId: 169, paintId: 44, debugString: 'path684'), + DrawCommand(DrawCommandType.path, + objectId: 170, paintId: 16, debugString: 'path688'), + DrawCommand(DrawCommandType.path, + objectId: 171, paintId: 15, debugString: 'path692'), + DrawCommand(DrawCommandType.path, + objectId: 172, paintId: 15, debugString: 'path696'), + DrawCommand(DrawCommandType.path, + objectId: 173, paintId: 15, debugString: 'path700'), + DrawCommand(DrawCommandType.path, + objectId: 174, paintId: 15, debugString: 'path704'), + DrawCommand(DrawCommandType.path, + objectId: 175, paintId: 15, debugString: 'path708'), + DrawCommand(DrawCommandType.path, + objectId: 176, paintId: 15, debugString: 'path712'), + DrawCommand(DrawCommandType.path, + objectId: 177, paintId: 15, debugString: 'path716'), + DrawCommand(DrawCommandType.path, + objectId: 178, paintId: 15, debugString: 'path720'), + DrawCommand(DrawCommandType.path, + objectId: 179, paintId: 15, debugString: 'path724'), + DrawCommand(DrawCommandType.path, + objectId: 180, paintId: 15, debugString: 'path728'), + DrawCommand(DrawCommandType.path, + objectId: 181, paintId: 44, debugString: 'path732'), + DrawCommand(DrawCommandType.path, + objectId: 182, paintId: 15, debugString: 'path736'), + DrawCommand(DrawCommandType.path, + objectId: 183, paintId: 44, debugString: 'path740'), + DrawCommand(DrawCommandType.path, + objectId: 184, paintId: 44, debugString: 'path744'), + DrawCommand(DrawCommandType.path, + objectId: 185, paintId: 44, debugString: 'path748'), + DrawCommand(DrawCommandType.path, + objectId: 186, paintId: 44, debugString: 'path752'), + DrawCommand(DrawCommandType.path, + objectId: 187, paintId: 44, debugString: 'path756'), + DrawCommand(DrawCommandType.path, + objectId: 188, paintId: 44, debugString: 'path760'), + DrawCommand(DrawCommandType.path, + objectId: 189, paintId: 44, debugString: 'path764'), + DrawCommand(DrawCommandType.path, + objectId: 190, paintId: 44, debugString: 'path768'), + DrawCommand(DrawCommandType.path, + objectId: 191, paintId: 44, debugString: 'path772'), + DrawCommand(DrawCommandType.path, + objectId: 192, paintId: 44, debugString: 'path776'), + DrawCommand(DrawCommandType.path, + objectId: 193, paintId: 44, debugString: 'path780'), + DrawCommand(DrawCommandType.path, + objectId: 194, paintId: 44, debugString: 'path784'), + DrawCommand(DrawCommandType.path, + objectId: 195, paintId: 44, debugString: 'path788'), + DrawCommand(DrawCommandType.path, + objectId: 196, paintId: 44, debugString: 'path792'), + DrawCommand(DrawCommandType.path, + objectId: 197, paintId: 44, debugString: 'path796'), + DrawCommand(DrawCommandType.path, + objectId: 198, paintId: 44, debugString: 'path800'), + DrawCommand(DrawCommandType.path, + objectId: 199, paintId: 44, debugString: 'path804'), + DrawCommand(DrawCommandType.path, + objectId: 200, paintId: 44, debugString: 'path808'), + DrawCommand(DrawCommandType.path, + objectId: 201, paintId: 44, debugString: 'path812'), + DrawCommand(DrawCommandType.path, + objectId: 202, paintId: 44, debugString: 'path816'), + DrawCommand(DrawCommandType.path, + objectId: 203, paintId: 44, debugString: 'path820'), + DrawCommand(DrawCommandType.path, + objectId: 204, paintId: 44, debugString: 'path824'), + DrawCommand(DrawCommandType.path, + objectId: 205, paintId: 44, debugString: 'path828'), + DrawCommand(DrawCommandType.path, + objectId: 206, paintId: 44, debugString: 'path832'), + DrawCommand(DrawCommandType.path, + objectId: 207, paintId: 44, debugString: 'path836'), + DrawCommand(DrawCommandType.path, + objectId: 208, paintId: 15, debugString: 'path840'), + DrawCommand(DrawCommandType.path, + objectId: 209, paintId: 15, debugString: 'path844'), + DrawCommand(DrawCommandType.path, + objectId: 210, paintId: 15, debugString: 'path848'), + DrawCommand(DrawCommandType.path, + objectId: 211, paintId: 15, debugString: 'path852'), + DrawCommand(DrawCommandType.path, + objectId: 212, paintId: 15, debugString: 'path856'), + DrawCommand(DrawCommandType.path, + objectId: 213, paintId: 15, debugString: 'path860'), + DrawCommand(DrawCommandType.path, + objectId: 214, paintId: 16, debugString: 'path864'), + DrawCommand(DrawCommandType.path, + objectId: 215, paintId: 16, debugString: 'path868'), + DrawCommand(DrawCommandType.path, + objectId: 216, paintId: 16, debugString: 'path872'), + DrawCommand(DrawCommandType.path, + objectId: 217, paintId: 16, debugString: 'path876'), + DrawCommand(DrawCommandType.path, + objectId: 218, paintId: 16, debugString: 'path880'), + DrawCommand(DrawCommandType.path, + objectId: 219, paintId: 16, debugString: 'path884'), + DrawCommand(DrawCommandType.path, + objectId: 220, paintId: 16, debugString: 'path888'), + DrawCommand(DrawCommandType.path, + objectId: 221, paintId: 16, debugString: 'path892'), + DrawCommand(DrawCommandType.path, + objectId: 222, paintId: 16, debugString: 'path896'), + DrawCommand(DrawCommandType.path, + objectId: 223, paintId: 16, debugString: 'path900'), + DrawCommand(DrawCommandType.path, + objectId: 224, paintId: 16, debugString: 'path904'), + DrawCommand(DrawCommandType.path, + objectId: 225, paintId: 16, debugString: 'path908'), + DrawCommand(DrawCommandType.path, + objectId: 226, paintId: 16, debugString: 'path912'), + DrawCommand(DrawCommandType.path, + objectId: 227, paintId: 16, debugString: 'path916'), + DrawCommand(DrawCommandType.path, + objectId: 228, paintId: 16, debugString: 'path920'), + DrawCommand(DrawCommandType.path, + objectId: 229, paintId: 16, debugString: 'path924'), + DrawCommand(DrawCommandType.path, + objectId: 230, paintId: 16, debugString: 'path928'), + DrawCommand(DrawCommandType.path, + objectId: 231, paintId: 16, debugString: 'path932'), + DrawCommand(DrawCommandType.path, + objectId: 232, paintId: 16, debugString: 'path936'), + DrawCommand(DrawCommandType.path, + objectId: 233, paintId: 16, debugString: 'path940'), + DrawCommand(DrawCommandType.path, + objectId: 234, paintId: 16, debugString: 'path944'), + DrawCommand(DrawCommandType.path, + objectId: 235, paintId: 16, debugString: 'path948'), + DrawCommand(DrawCommandType.path, + objectId: 236, paintId: 45, debugString: 'path952'), + DrawCommand(DrawCommandType.path, + objectId: 237, paintId: 45, debugString: 'path956'), + DrawCommand(DrawCommandType.path, + objectId: 238, paintId: 45, debugString: 'path960'), + DrawCommand(DrawCommandType.path, + objectId: 239, paintId: 45, debugString: 'path964'), + ], + ); + }); +} + +const List ghostScriptTigerPaints = [ + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.303691181256463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack), + fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffe87f3a))), + Paint(fill: Fill(color: Color(0xffea8c4d))), + Paint(fill: Fill(color: Color(0xffec9961))), + Paint(fill: Fill(color: Color(0xffeea575))), + Paint(fill: Fill(color: Color(0xfff1b288))), + Paint(fill: Fill(color: Color(0xfff3bf9c))), + Paint(fill: Fill(color: Color(0xfff5ccb0))), + Paint(fill: Fill(color: Color(0xfff8d8c4))), + Paint(fill: Fill(color: Color(0xfffae5d7))), + Paint(fill: Fill(color: Color(0xfffcf2eb))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffe5668c))), + Paint(fill: Fill(color: Color(0xffb23259))), + Paint(fill: Fill(color: Color(0xffa5264c))), + Paint( + stroke: Stroke(color: Color.opaqueBlack), + fill: Fill(color: Color(0xffff727f))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint(fill: Fill(color: Color(0xffcc3f4c))), + Paint(stroke: Stroke(color: Color(0xffa51926), width: 3.5312926)), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint(stroke: Stroke(color: Color(0xffa5264c), width: 3.5312926)), + Paint(stroke: Stroke(color: Color(0xffa5264c), width: 3.5312926)), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint(stroke: Stroke(color: Color(0xffa5264c), width: 3.5312926)), + Paint(stroke: Stroke(color: Color(0xffa5264c), width: 3.5312926)), + Paint(fill: Fill(color: Color(0xffb2b2b2))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint(fill: Fill()), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.88282315), + fill: Fill(color: Color(0xffffffcc))), + Paint(fill: Fill(color: Color(0xffe5e5b2))), + Paint(fill: Fill(color: Color(0xffe5e5b2))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffea8e51))), + Paint(fill: Fill(color: Color(0xffefaa7c))), + Paint(fill: Fill(color: Color(0xfff4c6a8))), + Paint(fill: Fill(color: Color(0xfff9e2d3))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff99cc32))), + Paint(fill: Fill(color: Color(0xff659900))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffeb955c))), + Paint(fill: Fill(color: Color(0xfff2b892))), + Paint(fill: Fill(color: Color(0xfff8dcc8))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff99cc32))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff323232))), + Paint(fill: Fill(color: Color(0xff666666))), + Paint(fill: Fill(color: Color(0xff999999))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xff992600))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill(color: Color(0xffcc7226))), + Paint(fill: Fill()), + Paint(stroke: Stroke(color: Color(0xff4c0000), width: 3.5312926)), + Paint(stroke: Stroke(color: Color(0xff4c0000), width: 3.5312926)), + Paint(stroke: Stroke(color: Color(0xff4c0000), width: 3.5312926)), + Paint(stroke: Stroke(color: Color(0xff4c0000), width: 3.5312926)), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff4c0000))), + Paint(fill: Fill(color: Color(0xff99cc32))), + Paint(fill: Fill(color: Color(0xff659900))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffe59999))), + Paint(fill: Fill(color: Color(0xffb26565))), + Paint(fill: Fill(color: Color(0xff992600))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xff992600))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xff992600))), + Paint(fill: Fill(color: Color(0xff992600))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill()), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint( + stroke: Stroke(color: Color.opaqueBlack, width: 0.17656463), + fill: Fill(color: Color(0xffffffff))), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill()), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(fill: Fill(color: Color(0xffcccccc))), + Paint(stroke: Stroke(color: Color.opaqueBlack)), + Paint(stroke: Stroke(color: Color.opaqueBlack)), + Paint(stroke: Stroke(color: Color.opaqueBlack)), + Paint(stroke: Stroke(color: Color.opaqueBlack)) +]; + +final List ghostScriptTigerPaths = [ + Path( + commands: const [ + MoveToCommand(108.96861750999997, 403.8269183955), + CubicToCommand(108.96861750999997, 403.8269183955, 109.14518213999997, + 407.17105248769997, 107.67969571099997, 407.137505208), + CubicToCommand(106.231865745, 407.1039579283, 77.18698410999997, + 322.2205120558, 40.93826557099999, 326.1808567067), + CubicToCommand(40.93826557099999, 326.1808567067, 72.331456785, + 313.1980594628, 108.96861751, 403.8269183955), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(115.20134894899999, 398.4893696306), + CubicToCommand(115.20134894899999, 398.4893696306, 114.230243484, + 401.6957833114, 112.87069583299998, 401.1678550677), + CubicToCommand(111.511148182, 400.6416924703, 113.064916926, + 310.9362665525, 77.646052148, 302.3305064863), + CubicToCommand(77.646052148, 302.3305064863, 111.58177403399998, + 300.8049880831, 115.20134894899999, 398.4893696306), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(163.73190315079998, 473.225646217), + CubicToCommand(163.73190315079998, 473.225646217, 166.62050049759995, + 474.920666665, 165.79064673659997, 476.121306149), + CubicToCommand(164.95902732929994, 477.30428917, 78.14043311199995, + 454.70401653, 61.437419113999965, 487.121282598), + CubicToCommand(61.437419113999965, 487.121282598, 67.93499749799997, + 453.768223991, 163.73190315079998, 473.225646217), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(158.77220269409997, 491.25289494000003), + CubicToCommand(158.77220269409997, 491.25289494000003, 162.04924222689996, + 491.95915346000004, 161.63608099269996, 493.354014037), + CubicToCommand(161.22468540479997, 494.748874614, 71.69582411699997, + 500.646133256, 66.06341241999996, 536.665317776), + CubicToCommand(66.06341241999996, 536.665317776, 61.71992252199999, + 502.976786372, 158.7722026941, 491.25289494000003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(151.33706612479997, 481.506527364), + CubicToCommand(151.33706612479997, 481.506527364, 154.47638524619998, + 482.67185392199997, 153.86547162639997, 483.99608864699997), + CubicToCommand(153.25455800659998, 485.32032337199996, 63.82104161899997, + 478.09883000499997, 52.99762979999997, 512.899718578), + CubicToCommand(52.99762979999997, 512.899718578, 53.61560600499996, + 478.928683766, 151.33706612479997, 481.506527364), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(132.43405683699996, 449.354108241), + CubicToCommand(132.43405683699996, 449.354108241, 134.74705348999998, + 451.79070013499995, 133.61703985799997, 452.708836211), + CubicToCommand(132.48702622599995, 453.62697228700006, 55.257657064, + 407.97442155420003, 30.27376191899998, 434.54033578400004), + CubicToCommand(30.27376191899998, 434.54033578400004, 45.705510581, + 404.26479867790005, 132.43405683699996, 449.354108241), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(119.12108373499998, 456.752166238), + CubicToCommand(119.12108373499998, 456.752166238, 121.68127086999999, + 458.906254724, 120.67485247899998, 459.94798604100004), + CubicToCommand(119.65077762499999, 460.989717358, 37.74244576799998, + 424.3737443886, 15.936713962999931, 453.59165936100004), + CubicToCommand(15.936713962999931, 453.59165936100004, 27.837170024999978, + 421.76941609610003, 119.12108373499996, 456.75216623800003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(114.53040335499998, 463.956003142), + CubicToCommand(114.53040335499998, 463.956003142, 117.35543743499994, + 465.721649442, 116.49027074799997, 466.904632463), + CubicToCommand(115.62510406099997, 468.06995902100005, 29.496877546999997, + 442.96246863500005, 11.875727472999927, 474.86769727600006), + CubicToCommand(11.875727472999927, 474.86769727600006, 19.326754858999948, + 441.72651622500007, 114.53040335499995, 463.956003142), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(133.47578815399999, 465.03304738500003), + CubicToCommand(133.47578815399999, 465.03304738500003, 135.45331201, + 467.734486224, 134.21735959999998, 468.511370596), + CubicToCommand(132.98140718999997, 469.270598505, 62.231959948999986, + 414.09768292260003, 34.05224500100002, 437.241774623), + CubicToCommand(34.05224500100002, 437.241774623, 53.24482028199998, + 409.1962487938, 133.47578815399999, 465.03304738500003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(98.55130434, 413.917587), + CubicToCommand(98.55130434, 413.917587, 99.61069211999998, 417.09575034, + 98.19817508000003, 417.4488796), + CubicToCommand(96.78565804000004, 417.80200886, 46.28817386000003, + 343.64486426, 12.387764900000036, 357.06377614), + CubicToCommand(12.387764900000036, 357.06377614, 39.22558866000003, + 336.2291498, 98.55130434, 413.917587), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(99.78725674999998, 426.2400325277), + CubicToCommand(99.78725674999998, 426.2400325277, 101.49993366099997, + 429.1162703504, 100.19335539899998, 429.7642625425), + CubicToCommand(98.886777137, 430.41402038089996, 33.59317696299996, + 368.8918407037, 3.382968769999991, 389.30624322430003), + CubicToCommand(3.382968769999991, 389.30624322430003, 25.100418260000026, + 363.1746779843, 99.78725674999998, 426.2400325277), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(99.57537919399996, 433.957672505), + CubicToCommand(99.57537919399996, 433.957672505, 101.55290304999997, + 436.659111344, 100.31695063999996, 437.435995716), + CubicToCommand(99.08099822999998, 438.19522362500004, 28.331550988999993, + 383.0223080426, 0.15183604099996728, 406.1611028041), + CubicToCommand(0.15183604099996728, 406.1611028041, 19.344411321999985, + 378.1208739138, 99.57537919399996, 433.957672505), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(95.67330087099995, 436.97692767800004), + CubicToCommand(96.55612402099996, 447.659087793, 98.30411385799997, + 459.259383984, 101.37633841999997, 464.76820044), + CubicToCommand(101.37633841999997, 464.76820044, 95.02001173999994, + 486.66221456, 110.55769917999996, 509.96874572), + CubicToCommand(110.55769917999996, 509.96874572, 109.85144065999995, + 522.68139908, 112.67647473999997, 528.33146724), + CubicToCommand(112.67647473999997, 528.33146724, 119.73905993999998, + 543.1628961600001, 128.21416217999996, 544.5754132), + CubicToCommand(135.06486982399997, 545.723083295, 150.47366508409996, + 551.143617436, 167.88470324839997, 553.615522256), + CubicToCommand(167.88470324839997, 553.615522256, 198.13375565999996, + 578.47582216, 192.48368749999997, 601.0760948), + CubicToCommand(192.48368749999997, 601.0760948, 191.77742897999997, + 630.03269412, 185.42110229999997, 632.8577282), + CubicToCommand(185.42110229999997, 632.8577282, 205.90259937999997, + 613.0824896400001, 188.95239489999997, 642.74534748), + LineToCommand(181.18355117999997, 675.93949792), + CubicToCommand(181.18355117999997, 675.93949792, 226.38409645999997, + 637.80153784, 198.84001417999997, 670.2894297600001), + LineToCommand(181.18355117999997, 716.1962335600001), + CubicToCommand(181.18355117999997, 716.1962335600001, 215.79021865999997, + 683.7083416400001, 203.07756529999997, 698.5397705600001), + LineToCommand(197.42749713999996, 714.0774580000001), + CubicToCommand(197.42749713999996, 714.0774580000001, 273.70341729999996, + 666.0518786400002, 219.32151125999997, 718.31500912), + CubicToCommand(219.32151125999997, 718.31500912, 233.44668165999997, + 711.9586824400001, 241.21552537999997, 716.90249208), + CubicToCommand(241.21552537999997, 716.90249208, 253.22192021999996, + 714.7837165200001, 251.80940317999995, 717.6087506), + CubicToCommand(251.80940317999995, 717.6087506, 215.08396014, + 735.9714721199999, 208.72763345999994, 768.45936404), + CubicToCommand(208.72763345999994, 768.45936404, 223.55906237999994, + 750.80290104, 217.90899421999995, 769.87188108), + LineToCommand(218.61525273999996, 790.35337816), + CubicToCommand(218.61525273999996, 790.35337816, 225.67783793999996, + 752.2154180800001, 224.97157941999996, 818.60371896), + CubicToCommand(224.97157941999996, 818.60371896, 258.87198837999995, + 786.82208556, 238.39049129999995, 823.5475286000001), + LineToCommand(238.39049129999995, 853.2103864400001), + CubicToCommand(238.39049129999995, 853.2103864400001, 265.22831505999994, + 824.2537871200001, 253.92817873999996, 846.85405976), + CubicToCommand(253.92817873999996, 846.85405976, 271.58464174, + 831.31637232, 264.52205654, 858.15419608), + CubicToCommand(264.52205654, 858.15419608, 263.1095395, 876.5169175999999, + 270.87838322, 856.74167904), + CubicToCommand(270.87838322, 856.74167904, 299.12872402, 802.71290226, + 288.53484621999996, 848.9728353200001), + CubicToCommand(288.53484621999996, 848.9728353200001, 287.12232917999995, + 882.87324428, 295.59743141999996, 856.74167904), + CubicToCommand(295.59743141999996, 856.74167904, 296.30368993999997, + 875.1044005599999, 312.5476359, 887.81705392), + CubicToCommand(312.5476359, 887.81705392, 310.42886033999997, + 798.12222188, 333.02913298, 861.68548868), + LineToCommand(340.09171818, 890.642088), + CubicToCommand(340.09171818, 890.642088, 345.03552781999997, 874.39814204, + 344.32926929999996, 865.21678128), + LineToCommand(358.45443969999997, 879.34195168), + CubicToCommand(358.45443969999997, 879.34195168, 385.29226345999996, + 839.08521604, 379.64219529999997, 862.3917471999999), + CubicToCommand(379.64219529999997, 862.3917471999999, 366.22328342, + 890.642088, 369.0483175, 899.11719024), + CubicToCommand(369.0483175, 899.11719024, 398.71117533999995, 837.672699, + 400.8299509, 834.84766492), + CubicToCommand(400.8299509, 834.84766492, 397.29865829999994, + 909.71106804, 416.36763834, 846.14780124), + CubicToCommand(416.36763834, 846.14780124, 426.25525761999995, + 867.33555684, 421.31144797999997, 875.1044005599999), + CubicToCommand(421.31144797999997, 875.1044005599999, 435.43661837999997, + 860.9792301599999, 434.02410133999996, 855.329162), + CubicToCommand(434.02410133999996, 855.329162, 442.14607431999997, + 840.85086234, 447.08988395999995, 864.8636520199999), + CubicToCommand(447.08988395999995, 864.8636520199999, 450.2680473, + 881.4607272399999, 453.09308138, 875.8106590799999), + CubicToCommand(453.09308138, 875.8106590799999, 460.15566658, + 918.1861702799999, 462.27444214, 877.92943464), + CubicToCommand(462.27444214, 877.92943464, 465.09947622, + 853.9166449599999, 452.38682286, 833.4351478799999), + CubicToCommand(452.38682286, 833.4351478799999, 453.7993399, + 827.7850797199999, 448.85553026, 820.7224945199999), + CubicToCommand(448.85553026, 820.7224945199999, 472.86831994, 858.8604546, + 460.15566658, 808.00984116), + CubicToCommand(460.15566658, 808.00984116, 479.93267078630004, + 822.13501156, 482.0514463463, 822.13501156), + CubicToCommand(482.0514463463, 822.13501156, 458.03689102, 781.1720174, + 473.57457846, 789.64711964), + CubicToCommand(473.57457846, 789.64711964, 464.39321770000004, + 771.28439812, 496.1766167463, 792.47215372), + CubicToCommand(496.1766167463, 792.47215372, 467.9245103, 764.22181292, + 499.00165082629997, 781.1720174000001), + CubicToCommand(499.00165082629997, 781.1720174000001, 513.12505558, + 792.47215372, 499.70790934629997, 774.81569072), + CubicToCommand(499.70790934629997, 774.81569072, 474.28083698, + 746.56534992, 513.12505558, 778.34698332), + CubicToCommand(513.12505558, 778.34698332, 533.60655266, 807.30358264, + 535.0190697, 812.24739228), + CubicToCommand(535.0190697, 812.24739228, 517.3626067, 760.69052032, + 509.59376298, 755.7467106800001), + CubicToCommand(509.59376298, 755.7467106800001, 524.4251919000001, + 691.47718536, 597.16981946, 719.02126764), + CubicToCommand(597.16981946, 719.02126764, 609.1762143000001, 749.390384, + 616.94505802, 716.90249208), + CubicToCommand(616.94505802, 716.90249208, 639.54533066, 705.60235576, + 659.3205692199999, 754.3341936400001), + CubicToCommand(659.3205692199999, 754.3341936400001, 666.38315442, + 730.32140396, 664.97063738, 725.3775943200001), + CubicToCommand(664.97063738, 725.3775943200001, 676.97703222, + 727.4963698800001, 675.56451518, 725.3775943200001), + CubicToCommand(675.56451518, 725.3775943200001, 698.87104634, + 733.14643804, 700.9898218999999, 731.7339210000001), + CubicToCommand(700.9898218999999, 731.7339210000001, 712.99621674, + 743.7403158400001, 713.70247526, 737.38398916), + CubicToCommand(713.70247526, 737.38398916, 729.9464212199999, + 742.3277988000001, 726.4151286199999, 735.97147212), + CubicToCommand(726.4151286199999, 735.97147212, 741.95281606, + 763.5155544000002, 742.6590745799999, 769.87188108), + LineToCommand(746.8966257, 745.15283288), + LineToCommand(750.4279182999999, 750.09664252), + CubicToCommand(750.4279182999999, 750.09664252, 753.2529523799999, + 736.67773064, 751.8404353399999, 734.55895508), + CubicToCommand(750.4279183, 732.44017952, 787.15336134, 746.56534992, + 795.6284635799999, 783.2907929600001), + LineToCommand(799.1597561799999, 798.12222188), + CubicToCommand(799.1597561799999, 798.12222188, 809.7536339799999, + 771.99065664, 806.9285998999999, 764.92807144), + CubicToCommand(806.9285998999999, 764.92807144, 816.1099606599998, + 766.3405884800001, 816.81621918, 774.1094322), + CubicToCommand(816.81621918, 774.1094322, 823.8788043799999, 733.14643804, + 815.40370214, 722.55256024), + CubicToCommand(815.40370214, 722.55256024, 823.1725458599999, 721.1400432, + 825.2913214199999, 727.4963698800001), + LineToCommand(825.2913214199999, 714.7837165200001), + CubicToCommand(825.2913214199999, 714.7837165200001, 838.0039747799999, + 716.1962335600001, 838.0039747799999, 711.9586824400001), + CubicToCommand(838.0039747799999, 711.9586824400001, 845.7728184999999, + 704.89609724, 849.3041110999999, 713.3711994800001), + CubicToCommand(849.3041110999999, 713.3711994800001, 827.4100969799999, + 651.22044972, 859.8979888999999, 685.1208586800001), + CubicToCommand(859.8979888999999, 685.1208586800001, 872.6106422599998, + 704.18983872, 866.2543155799999, 670.9956882800001), + CubicToCommand(859.8979889, 637.80153784, 852.8354036999999, 634.97650376, + 861.3105059399999, 634.27024524), + CubicToCommand(861.3105059399999, 634.27024524, 862.7230229799999, + 627.9139185600001, 859.19173038, 625.08888448), + CubicToCommand(855.6604377799999, 622.2638504, 861.3105059399999, + 625.08888448, 861.3105059399999, 625.08888448), + CubicToCommand(861.3105059399999, 625.08888448, 869.7856081799999, + 632.15146968, 860.60424742, 593.30725108), + CubicToCommand(860.60424742, 593.30725108, 871.9043837399998, + 596.13228516, 850.7166281399999, 544.5754132000001), + CubicToCommand(850.7166281399999, 544.5754132000001, 855.6604377799999, + 540.33786208, 848.5978525799999, 525.50643316), + CubicToCommand(848.5978525799999, 525.50643316, 862.7230229799999, + 533.2752768800001, 867.6668326199999, 530.4502428000001), + CubicToCommand(867.6668326199999, 530.4502428000001, 866.9605741, + 527.62520872, 861.3105059399999, 520.5626235200001), + CubicToCommand(861.3105059399999, 520.5626235200001, 823.1725458599999, + 423.8052062800001, 859.19173038, 462.6494248800001), + CubicToCommand(859.19173038, 462.6494248800001, 880.114639035, + 486.57393224500004, 868.8145027149999, 446.31719660500005), + CubicToCommand(868.8145027149999, 446.31719660500005, 852.7294649219999, + 403.92579458830005, 854.106669036, 396.3405780835001), + LineToCommand(95.6733008709999, 436.9769276780001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(854.106669036, 396.6937073435), + CubicToCommand(855.201369742, 397.0132893238, 859.103448065, + 398.997875765, 861.31050594, 401.91119216000004), + CubicToCommand(861.31050594, 401.91119216000004, 873.31690078, + 420.98017219999997, 864.13554002, 388.49228028000005), + CubicToCommand(864.13554002, 388.49228028000005, 847.8915940600001, + 337.64166684, 863.4292815000001, 357.4169054), + CubicToCommand(863.4292815000001, 357.4169054, 874.0231593000001, + 370.12955876, 868.37309114, 346.11676908000004), + CubicToCommand(861.557696422, 317.11249730990005, 857.0729548200002, + 305.86003344, 857.0729548200002, 305.86003344), + CubicToCommand(857.0729548200002, 305.86003344, 877.5544519000002, + 314.33513568, 830.2351310600002, 244.41554220000003), + LineToCommand(845.7728185000002, 250.77186888000003), + CubicToCommand(845.7728185000002, 250.77186888000003, 811.1661510200001, + 180.85227540000002, 773.0281909400002, 171.67091464000003), + LineToCommand(758.9030205400002, 161.07703684), + CubicToCommand(758.9030205400002, 161.07703684, 826.7038384600002, + 93.98247744000003, 804.1035658200003, 29.006693600000034), + CubicToCommand(804.1035658200003, 29.006693600000034, 792.0971709800002, + 19.825332840000016, 775.1469665000002, 36.069278800000035), + CubicToCommand(775.1469665000002, 36.069278800000035, 763.8468301800002, + 44.54438104000002, 753.2529523800002, 41.719346960000024), + CubicToCommand(753.2529523800002, 41.719346960000024, 698.8710463400002, + 43.83812252000004, 695.3397537400002, 43.83812252000004), + CubicToCommand(691.8084611400002, 43.83812252000004, 630.3639699000003, + -21.843919839999984, 514.5375726200002, 9.231455040000014), + CubicToCommand(514.5375726200002, 9.231455040000014, 505.35621186000014, + 12.762747640000015, 497.5891337863002, 10.643972080000054), + CubicToCommand(497.5891337863002, 10.643972080000054, 465.09947622000016, + -17.60636871999995, 378.9359367800002, 22.650366920000067), + CubicToCommand(378.9359367800002, 22.650366920000067, 361.2794737800002, + 26.181659520000068, 358.4544397000002, 26.181659520000068), + CubicToCommand(355.62940562000017, 26.181659520000068, 350.6855959800002, + 26.181659520000068, 336.5604255800002, 37.481795840000075), + CubicToCommand(322.4352551800002, 48.78193216000008, 321.7289966600002, + 50.19444920000009, 318.1977040600002, 53.01948328000006), + CubicToCommand(318.1977040600002, 53.01948328000006, 289.2411047400002, + 72.79472184000008, 280.7660025000002, 74.20723888000006), + CubicToCommand(280.7660025000002, 74.20723888000006, 260.2845054200002, + 85.50737520000007, 252.51566170000018, 103.16383820000004), + LineToCommand(246.15933502000019, 105.28261376000006), + CubicToCommand(246.15933502000019, 105.28261376000006, 243.3343009400002, + 117.99526712000008, 242.62804242000018, 120.11404268000007), + CubicToCommand(242.62804242000018, 120.11404268000007, 234.1529401800002, + 126.47036936000006, 232.7404231400002, 136.3579886400001), + CubicToCommand(232.7404231400002, 136.3579886400001, 217.20273570000018, + 146.95186644000006, 217.90899422000018, 154.72071016000007), + CubicToCommand(217.90899422000018, 154.72071016000007, 215.0839601400002, + 163.90207092000009, 213.6714431000002, 172.37717316000007), + CubicToCommand(213.6714431000002, 172.37717316000007, 200.9587897400002, + 180.85227540000005, 202.3713067800002, 185.7960850400001), + CubicToCommand(202.3713067800002, 185.7960850400001, 188.9523949000002, + 210.51513324000007, 191.0711704600002, 222.52152808000008), + CubicToCommand(191.0711704600002, 222.52152808000008, 179.77103414000018, + 221.81526956000008, 174.82722450000017, 226.05282068000008), + CubicToCommand(174.82722450000017, 226.05282068000008, 173.4147074600002, + 234.5279229200001, 170.58967338000016, 235.23418144000007), + CubicToCommand(170.58967338000016, 235.23418144000007, 165.64586374000018, + 237.35295700000006, 169.88341486000016, 244.41554220000006), + CubicToCommand(169.88341486000016, 244.41554220000006, 167.05838078000016, + 249.35935184000007, 166.35212226000016, 252.18438592000007), + CubicToCommand(166.35212226000016, 252.18438592000007, 167.76463930000014, + 257.12819556000005, 159.99579558000016, 267.0158148400001), + CubicToCommand(159.99579558000016, 267.0158148400001, 148.69565926000016, + 300.20996528000006, 152.22695186000016, 309.3913260400001), + CubicToCommand(152.22695186000016, 309.3913260400001, 152.93321038000016, + 317.8664282800001, 147.98940074000015, 320.69146236000006), + CubicToCommand(147.98940074000015, 320.69146236000006, 141.63307406000015, + 319.98520384000005, 156.46450298000013, 341.17295944000006), + CubicToCommand(156.46450298000013, 341.17295944000006, 157.87702002000015, + 343.2917350000001, 152.22695186000013, 347.52928612000005), + CubicToCommand(152.22695186000013, 347.52928612000005, 121.85783550000014, + 353.8856128000001, 117.62028438000013, 382.84221212000006), + CubicToCommand(117.62028438000013, 382.84221212000006, 93.60749470000013, + 408.9737773600001, 93.60749470000013, 418.15513812000006), + CubicToCommand(93.60749470000013, 422.2249528415001, 94.08421920100014, + 427.78144174760007, 95.32017161100012, 435.9175398980001), + CubicToCommand(95.32017161100012, 435.9175398980001, 94.31375322000014, + 450.6430300400001, 143.04559110000014, 452.0555470800001), + CubicToCommand(191.7774289800001, 453.46806412000007, 854.1066690360002, + 396.6937073435, 854.1066690360002, 396.6937073435), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(120.79844771999998, 436.16473038000004), + CubicToCommand(76.65729022, 366.59826616, 102.08259694, 466.18071748, + 102.08259694, 466.18071748), + CubicToCommand(117.62028437999999, 526.9189502, 346.44804486, + 460.53064931999995, 346.44804486, 460.53064931999995), + CubicToCommand(346.44804486, 460.53064931999995, 644.4891403, + 406.85500179999997, 664.2643788600001, 399.7924166), + CubicToCommand(684.03961742, 392.72983139999997, 852.12914518, + 404.02996772, 852.12914518, 404.02996772), + LineToCommand(842.2415258999999, 374.36710988000004), + CubicToCommand(727.8276456599999, 292.44112156, 693.9272367, + 333.40411572000005, 669.91444702, 326.34153052), + CubicToCommand(645.9016573399999, 319.27894532000005, 650.13920846, + 336.2291498, 644.4891402999999, 337.64166684), + CubicToCommand(638.8390721399999, 339.05418388, 569.62573718, + 295.26615564, 558.3256008599999, 296.67867268000003), + CubicToCommand(547.0254645399999, 298.09118972, 502.28398729799994, + 256.1553244487, 528.66274302, 312.21636012), + CubicToCommand(556.9130838199999, 372.24833432, 425.54899909999995, + 381.42969508, 395.88614125999993, 361.65445652), + CubicToCommand(366.22328342, 341.87921796, 408.59879462, + 394.14234844000003, 408.59879462, 394.14234844000003), + CubicToCommand(441.08668653999996, 429.45527444000004, 380.34845382, + 399.7924166, 380.34845382, 399.7924166), + CubicToCommand(319.6102211, 377.19214396000007, 277.2347099, + 422.39268924000004, 271.58464173999994, 423.80520628), + CubicToCommand(265.93457357999995, 425.21772332, 257.45947133999994, + 430.86779148000005, 256.0469542999999, 419.56765516), + CubicToCommand(254.63443725999994, 408.26751884, 241.37443354699997, + 378.7794599837, 185.42110229999994, 425.21772332), + CubicToCommand(150.10817629999994, 454.5274519, 125.74225735999997, + 415.6832333, 125.74225735999997, 415.6832333), + LineToCommand(120.79844771999996, 436.16473038000004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(560.6385975129999, 299.7614911198), + CubicToCommand(549.338461193, 301.1740081598, 504.544014562, + 259.25933064410003, 530.9757396729999, 315.2991785598), + CubicToCommand(560.285468253, 377.4499283198, 427.861995753, + 384.5125135198, 398.19913791299996, 364.7372749598), + CubicToCommand(368.53451442669996, 344.9620363998, 410.91179127299995, + 397.2251668798, 410.91179127299995, 397.2251668798), + CubicToCommand(443.39968319299993, 432.545155465, 382.65968482669996, + 402.87523503980003, 382.65968482669996, 402.87523503980003), + CubicToCommand(321.9214521067, 380.2749623998, 279.54594090669997, + 425.4755076798, 273.8958727467, 426.8880247198), + CubicToCommand(268.24580458669993, 428.3005417598, 259.7707023467, + 433.957672505, 258.35818530669997, 422.65047359979997), + CubicToCommand(256.94566826669995, 411.3503372798, 243.91696421899994, + 382.15714135559995, 187.73233330669996, 428.3005417598), + CubicToCommand(150.23706847989993, 458.92391118699993, 126.41320295399996, + 421.0455011131, 126.41320295399996, 421.0455011131), + LineToCommand(120.76313479399994, 438.901482145), + CubicToCommand(76.62197729399998, 368.6216968198, 103.23026703499997, + 471.583595158, 103.23026703499997, 471.583595158), + CubicToCommand(118.78561093799996, 532.321827878, 348.76104151299995, + 463.620530345, 348.76104151299995, 463.620530345), + CubicToCommand(348.76104151299995, 463.620530345, 646.802136953, + 409.9378202398, 666.5773755129999, 402.8752350398), + CubicToCommand(686.3526140729999, 395.8126498398, 852.906029552, + 406.98389397989996, 852.906029552, 406.98389397989996), + LineToCommand(843.142005513, 376.4223221732), + CubicToCommand(728.7281252729999, 294.4963338532, 696.240233353, + 336.4869341598, 672.2274436729999, 329.4243489598), + CubicToCommand(648.214653993, 322.3617637598, 652.452205113, + 339.31196823979997, 646.802136953, 340.7244852798), + CubicToCommand(641.1520687929999, 342.1370023198, 571.9387338329999, + 298.3489740798, 560.6385975129999, 299.7614911198), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(562.951594166, 302.8425439133), + CubicToCommand(551.651457846, 304.2550609533, 507.96936838399995, + 261.8283460106, 533.2887363259999, 318.3802313533), + CubicToCommand(561.892206386, 382.2983930596, 430.17322675969996, + 387.5953319596, 400.5103689197, 367.8200933996), + CubicToCommand(370.8475110797, 348.0448548396, 413.2230222797, + 400.3079853196, 413.2230222797, 400.3079853196), + CubicToCommand(445.7109141997, 435.617380027, 384.9726814797, + 405.95805347960004, 384.9726814797, 405.95805347960004), + CubicToCommand(324.2344487597, 383.3577808396, 281.85717191339995, + 428.5583261196, 276.20710375339996, 429.9708431596), + CubicToCommand(270.5570355934, 431.3833601996, 262.0819333534, + 437.029897067, 260.6694163134, 425.7332920396), + CubicToCommand(259.2568992734, 414.4331557196, 246.459494891, + 385.5348227275, 190.0435643134, 431.3833601996), + CubicToCommand(150.3641950135, 463.320370474, 127.084148548, + 426.4077689262, 127.084148548, 426.4077689262), + LineToCommand(120.727821868, 441.62057744699996), + CubicToCommand(78.70543992799998, 372.7639030396, 104.395593593, + 476.968816373, 104.395593593, 476.968816373), + CubicToCommand(119.93328103299999, 537.707049093, 351.07403816600004, + 466.692754907, 351.07403816600004, 466.692754907), + CubicToCommand(351.07403816600004, 466.692754907, 649.115133606, + 413.0206386796, 668.890372166, 405.9580534796), + CubicToCommand(688.6656107260001, 398.8954682796, 853.665257461, + 409.9378202398, 853.665257461, 409.9378202398), + LineToCommand(844.0424851260001, 378.4775344664), + CubicToCommand(729.628604886, 296.5515461464, 698.553230006, + 339.5679869533, 674.5404403260001, 332.5054017533), + CubicToCommand(650.527650646, 325.4428165533, 654.765201766, + 342.3930210333, 649.115133606, 343.8073037196), + CubicToCommand(643.465065446, 345.2198207596, 574.251730486, + 301.4300268733, 562.9515941660001, 302.8425439133), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(565.264590819, 305.9253623531), + CubicToCommand(553.964454499, 307.3378793931, 510.30002149999996, + 264.9058675115, 535.601732979, 321.4630497931), + CubicToCommand(565.264590819, 387.7736622359, 431.54160264219996, + 390.0495803166, 402.8215999264, 370.9011461931), + CubicToCommand(373.15874208639997, 351.1259076331, 415.5342532864, + 403.3890381131, 415.5342532864, 403.3890381131), + CubicToCommand(448.0221452064, 438.70726105200004, 387.28391248639997, + 409.03910627310006, 387.28391248639997, 409.03910627310006), + CubicToCommand(326.5456797664, 386.4388336331, 284.1701685664, + 431.644675852, 278.5201004064, 433.057192892), + CubicToCommand(272.8700322464, 434.469709932, 264.3949300064, + 440.11977809200005, 262.9824129664, 428.8143448331), + CubicToCommand(261.56989592639997, 417.5142085131, 249.00379120929995, + 388.91426974570004, 192.35479532009995, 434.469709932), + CubicToCommand(150.49308719339996, 467.716829761, 127.75509414199996, + 431.768271093, 127.75509414199996, 431.768271093), + LineToCommand(120.69250894199996, 444.35732921199997), + CubicToCommand(82.20141960199996, 379.3762484331, 105.54326368799997, + 482.354037588, 105.54326368799997, 482.354037588), + CubicToCommand(121.08095112799995, 543.092270308, 353.38703481899995, + 469.78263593199995, 353.38703481899995, 469.78263593199995), + CubicToCommand(353.38703481899995, 469.78263593199995, 651.428130259, + 416.1016914731, 671.2033688189999, 409.0391062731), + CubicToCommand(690.9786073789999, 401.9765210731, 854.4421418329999, + 412.8917464997, 854.4421418329999, 412.8917464997), + LineToCommand(844.9429647389999, 380.5327467596), + CubicToCommand(730.529084499, 298.60499279329997, 700.866226659, + 342.6508053931, 676.853436979, 335.5882201931), + CubicToCommand(652.840647299, 328.5256349931, 657.078198419, + 345.4758394731, 651.428130259, 346.8883565131), + CubicToCommand(645.778062099, 348.3008735531, 576.564727139, + 304.5128453131, 565.264590819, 305.9253623531), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(567.577587472, 309.0081807929), + CubicToCommand(556.2774511519999, 310.42069783290003, 513.495841303, + 267.5967124727, 537.914729632, 324.5458682329), + CubicToCommand(567.577587472, 393.75920319290003, 434.7956887731, + 393.75920319290003, 405.13283093309997, 373.9839646329), + CubicToCommand(375.46997309309995, 354.2087260729, 417.84548429309996, + 406.4718565529, 417.84548429309996, 406.4718565529), + CubicToCommand(450.3333762131, 441.779485614, 389.59514349309995, + 412.12192471289995, 389.59514349309995, 412.12192471289995), + CubicToCommand(328.85691077309997, 389.5216520729, 286.48139957309996, + 434.71690041399995, 280.8313314131, 436.12941745399996), + CubicToCommand(275.1812632531, 437.541934494, 266.7061610131, + 443.19200265399996, 265.29364397309996, 431.89186633399993), + CubicToCommand(263.88112693309995, 420.59702695289997, 251.5480875276, + 392.2919511176, 194.66779197309998, 437.541934494), + CubicToCommand(150.62197937329998, 472.11328904799996, 128.44369619899996, + 437.135835845, 128.44369619899996, 437.135835845), + LineToCommand(120.67485247899995, 447.076424514), + CubicToCommand(85.71505573899992, 385.6354645665999, 106.70859024599994, + 487.75691526599996, 106.70859024599994, 487.75691526599996), + CubicToCommand(122.24627768599996, 548.495147986, 355.700031472, + 472.8548604939999, 355.700031472, 472.8548604939999), + CubicToCommand(355.700031472, 472.8548604939999, 653.741126912, + 419.18450991289995, 673.516365472, 412.12192471289995), + CubicToCommand(693.2916040319999, 405.05933951289995, 855.219026205, + 415.84567275959995, 855.219026205, 415.84567275959995), + LineToCommand(845.843444352, 382.58619340649994), + CubicToCommand(731.429564112, 300.66020508649996, 703.179223312, + 345.7336238329, 679.166433632, 338.6710386328999), + CubicToCommand(655.153643952, 331.6084534329, 659.3911950720001, + 348.55865791289995, 653.741126912, 349.97117495289996), + CubicToCommand(648.091058752, 351.3836919928999, 578.877723792, + 307.59566375289995, 567.577587472, 309.00818079289996), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(569.890584125, 312.08923358640004), + CubicToCommand(558.5904478049999, 313.5017506264, 512.736613394, + 272.0955792451, 540.227726285, 327.6269210264), + CubicToCommand(574.128135245, 396.1339974664, 437.10691977979997, + 396.84025598640005, 407.4440619398, 377.0650174264), + CubicToCommand(377.7812040998, 357.2897788664, 420.1567152998, + 409.5529093464, 420.1567152998, 409.5529093464), + CubicToCommand(452.6446072198, 444.86936663899996, 391.9063744998, + 415.2029775064, 391.9063744998, 415.2029775064), + CubicToCommand(331.1681417798, 392.6027048664, 288.7926305798, + 437.806781439, 283.1425624198, 439.21929847900003), + CubicToCommand(277.49249425979997, 440.631815519, 269.0173920198, + 446.281883679, 267.6048749798, 434.981747359), + CubicToCommand(266.1923579398, 423.6780797464, 254.0906181996, + 395.66963248950003, 196.9790229798, 440.631815519), + CubicToCommand(150.75087155319997, 476.527404798, 129.114641793, + 442.485744134, 129.114641793, 442.485744134), + LineToCommand(120.63953955299999, 449.813176279), + CubicToCommand(88.85790615299999, 391.1901878264, 107.85626034100002, + 493.142136481, 107.85626034100002, 493.142136481), + CubicToCommand(123.39394778100001, 553.880369201, 358.013028125, + 475.944741519, 358.013028125, 475.944741519), + CubicToCommand(358.013028125, 475.944741519, 656.054123565, + 422.26556270640003, 675.829362125, 415.20297750640003), + CubicToCommand(695.6046006849999, 408.14039230640003, 855.978254114, + 418.79783337320004, 855.978254114, 418.79783337320004), + LineToCommand(846.743923965, 384.64140569970004), + CubicToCommand(732.330043725, 302.71541737970006, 705.492219965, + 348.8146766264, 681.479430285, 341.75209142640006), + CubicToCommand(657.4666406050001, 334.6895062264, 661.7041917250001, + 351.6397107064, 656.054123565, 353.05222774640004), + CubicToCommand(650.404055405, 354.46474478640005, 581.190720445, + 310.67671654640003, 569.890584125, 312.08923358640004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(572.203580778, 315.1702863799), + CubicToCommand(560.903444458, 316.5828034199, 514.3786644529999, + 275.5138704819, 542.540722938, 330.7079738199), + CubicToCommand(578.559907458, 401.33559146619996, 439.41815078649995, + 399.92307442620006, 409.7552929465, 380.1478358662), + CubicToCommand(380.0924351065, 360.3725973062, 422.4679463065, + 412.63572778620005, 422.4679463065, 412.63572778620005), + CubicToCommand(454.95583822649996, 447.941591201, 394.2176055065, + 418.2857959462, 394.2176055065, 418.2857959462), + CubicToCommand(333.4793727865, 395.6855233062, 291.1038615865, + 440.879006001, 285.4537934265, 442.291523041), + CubicToCommand(279.8037252665, 443.704040081, 271.3286230265, + 449.354108241, 269.9161059865, 438.05397192099997), + CubicToCommand(268.5035889465, 426.7608981862, 256.6331488716, + 399.04731386139997, 199.29025398649998, 443.704040081), + CubicToCommand(150.87799808679998, 480.923864085, 129.785587387, + 447.85330888600004, 129.785587387, 447.85330888600004), + LineToCommand(120.60422662699997, 452.53227158100003), + CubicToCommand(92.353885827, 399.21681590620005, 109.02158689899997, + 498.545014159, 109.02158689899997, 498.545014159), + CubicToCommand(124.55927433899998, 559.283246879, 360.308368315, + 479.016966081, 360.308368315, 479.016966081), + CubicToCommand(360.308368315, 479.016966081, 658.349463755, + 425.34838114620004, 678.1247023149999, 418.2857959462), + CubicToCommand(697.899940875, 411.22321074620004, 856.7374820230001, + 421.7517596331, 856.7374820230001, 421.7517596331), + LineToCommand(847.626747115, 386.6966179929), + CubicToCommand(733.2128668749999, 304.7706296729, 707.7875601549999, + 351.8974950662, 683.774770475, 344.8349098662), + CubicToCommand(659.761980795, 337.7705590199, 663.999531915, + 354.72252914620003, 658.349463755, 356.1350461862), + CubicToCommand(652.6993955949999, 357.5475632262, 583.486060635, + 313.7577693399, 572.185924315, 315.1702863799), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(574.498920968, 318.2531048197), + CubicToCommand(563.198784648, 319.6656218597, 514.749450176, + 279.6295920072, 544.836063128, 333.7907922597), + CubicToCommand(583.680281728, 403.7103857397, 441.7293817932, + 403.0041272197, 412.0665239532, 383.22888865970003), + CubicToCommand(382.4036661132, 363.4536500997, 424.7791773132, + 415.7167805797, 424.7791773132, 415.7167805797), + CubicToCommand(457.2670692332, 451.031472226, 396.5288365132, + 421.3668487397, 396.5288365132, 421.3668487397), + CubicToCommand(335.79060379320003, 398.76657609970005, 293.41509259320003, + 443.96888702600006, 287.7650244332, 445.381404066), + CubicToCommand(282.1149562732, 446.793921106, 273.6398540332, + 452.443989266, 272.2273369932, 441.14385294600004), + CubicToCommand(270.8148199532, 429.8419509797, 259.1774451899, + 402.4267608796, 201.60148499320002, 446.793921106), + CubicToCommand(151.0068902667, 485.32032337199996, 130.456532981, + 453.22087363800006, 130.456532981, 453.22087363800006), + LineToCommand(120.56891370100001, 455.26902334600004), + CubicToCommand(95.14360698100003, 405.1229027797, 110.16925699400002, + 503.93023537399995, 110.16925699400002, 503.93023537399995), + CubicToCommand(125.70694443400001, 564.668468094, 362.621364968, + 482.10684710600003, 362.621364968, 482.10684710600003), + CubicToCommand(362.621364968, 482.10684710600003, 660.662460408, + 428.42943393970006, 680.437698968, 421.3668487397), + CubicToCommand(700.212937528, 414.30426353969995, 857.532022858, + 424.705685893, 857.532022858, 424.705685893), + LineToCommand(848.527226728, 388.75006463980003), + CubicToCommand(734.1133464879999, 306.8240763198, 710.100556808, + 354.97854785970003, 686.087767128, 347.91596265969997), + CubicToCommand(662.074977448, 340.8533774597, 666.3125285680001, + 357.8035819397, 660.662460408, 359.2160989797), + CubicToCommand(655.012392248, 360.6286160197, 585.799057288, + 316.8405877797, 574.498920968, 318.2531048197), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(576.811917621, 321.3359232595), + CubicToCommand(565.5117813009999, 322.7484402995, 517.062446829, + 282.7106448007, 547.149059781, 336.8736106995), + CubicToCommand(585.993278381, 406.7932041795, 444.04237844619996, + 406.0869456595, 414.37952060619995, 386.3117070995), + CubicToCommand(384.7166627662, 366.5364685395, 427.0921739662, + 418.79959901949996, 427.0921739662, 418.79959901949996), + CubicToCommand(459.5800658862, 454.103696788, 398.8418331662, + 424.4496671795, 398.8418331662, 424.4496671795), + CubicToCommand(338.10183479989996, 401.8493945395, 295.72632359989996, + 447.041111588, 290.07625543989997, 448.453628628), + CubicToCommand(284.4261872799, 449.86614566799994, 275.95108503989997, + 455.516213828, 274.53856799989995, 444.216077508), + CubicToCommand(273.12605095989994, 432.915941188, 261.72174150819995, + 405.80444225149995, 203.91271599989997, 449.86614566799994), + CubicToCommand(151.13578244659996, 489.71678265899993, 131.14513503799995, + 458.570781927, 131.14513503799995, 458.570781927), + LineToCommand(120.55125723799995, 457.98811864799995), + CubicToCommand(96.52081109499994, 411.3821189132, 111.33458355199997, + 509.333113052, 111.33458355199997, 509.333113052), + CubicToCommand(126.87227099199995, 570.071345772, 364.93436162099994, + 485.179071668, 364.93436162099994, 485.179071668), + CubicToCommand(364.93436162099994, 485.179071668, 662.975457061, + 431.51225237949996, 682.750695621, 424.44966717949995), + CubicToCommand(702.5259341809999, 417.38708197949995, 858.291250767, + 427.65961215289997, 858.291250767, 427.65961215289997), + LineToCommand(849.445362804, 390.80527693299996), + CubicToCommand(735.013826101, 308.87928861299997, 712.413553461, + 358.0613662994999, 688.400763781, 350.9987810995), + CubicToCommand(664.387974101, 343.9361958994999, 668.6255252210001, + 360.88640037949995, 662.975457061, 362.29891741949996), + CubicToCommand(657.325388901, 363.71143445949997, 588.1120539409999, + 319.92340621949995, 576.811917621, 321.33592325949996), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(579.1249142739999, 324.416976053), + CubicToCommand(567.824777954, 325.829493093, 520.064045539, + 285.41914622490003, 549.462056434, 339.954663493), + CubicToCommand(588.306275034, 411.993032533, 446.35360945289995, + 409.167998453, 416.69075161289993, 389.392759893), + CubicToCommand(387.0278937729, 369.617521333, 429.4034049729, + 421.880651813, 429.4034049729, 421.880651813), + CubicToCommand(461.89129689289996, 457.193577813, 401.1530641729, + 427.53071997300003, 401.1530641729, 427.53071997300003), + CubicToCommand(340.4148314529, 404.93044733299996, 298.0393202529, + 450.130992613, 292.38925209289994, 451.543509653), + CubicToCommand(286.73741828659996, 452.956026693, 278.26231604659995, + 458.60609485299995, 276.84979900659994, 447.30595853299997), + CubicToCommand(275.4372819665999, 436.005822213, 264.26427218019995, + 409.1821236234, 206.22394700659993, 452.956026693), + CubicToCommand(151.26467462649995, 494.113241946, 131.81608063199994, + 463.938346679, 131.81608063199994, 463.938346679), + LineToCommand(120.51594431199993, 460.72487041299996), + CubicToCommand(97.56254241199994, 418.34935921299996, 112.48225364699994, + 514.7183342669999, 112.48225364699994, 514.7183342669999), + CubicToCommand(128.01994108699995, 575.456566987, 367.2473582739999, + 488.26895269299996, 367.2473582739999, 488.26895269299996), + CubicToCommand(367.2473582739999, 488.26895269299996, 665.288453714, + 434.593305173, 685.0636922739999, 427.530719973), + CubicToCommand(704.8389308339999, 420.468134773, 859.0681351389999, + 430.6135384127999, 859.0681351389999, 430.6135384127999), + LineToCommand(850.328185954, 392.86048922619995), + CubicToCommand(735.914305714, 310.93273525989997, 714.7265501139999, + 361.14241909299994, 690.7137604339999, 354.07983389299994), + CubicToCommand(666.700970754, 347.01724869299994, 670.9385218739999, + 363.96745317299997, 665.288453714, 365.379970213), + CubicToCommand(659.6383855539999, 366.79248725299993, 590.4250505939999, + 323.004459013, 579.1249142739999, 324.416976053), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(581.437910927, 327.4980288465), + CubicToCommand(570.137774607, 328.9105458865, 524.283940196, + 287.5167340293, 551.775053087, 343.0357162865), + CubicToCommand(589.2067546469999, 418.60714357280006, 448.66484045959993, + 412.2508168928, 419.00198261959997, 392.4755783328), + CubicToCommand(389.33912477959996, 372.7003397728, 431.71463597959996, + 424.9634702528, 431.71463597959996, 424.9634702528), + CubicToCommand(464.2025278996, 460.283458838, 403.46429517959996, + 430.61353841280004, 403.46429517959996, 430.61353841280004), + CubicToCommand(342.7260624596, 408.0132657728, 300.35055125959997, + 453.22087363800006, 294.7004830996, 454.633390678), + CubicToCommand(289.05041493959993, 456.04590771799997, 280.5753126996, + 461.695975878, 279.16279565959996, 450.39583955800003), + CubicToCommand(277.75027861959995, 439.095703238, 266.80856849849994, + 412.56157064160004, 208.53694365959996, 456.04590771799997), + CubicToCommand(151.39180116009993, 498.509701233, 132.48702622599995, + 469.28825496800005, 132.48702622599995, 469.28825496800005), + LineToCommand(120.48063138599994, 463.46162217799997), + CubicToCommand(97.88035874599996, 422.49156543280003, 113.64758020499994, + 520.121211945, 113.64758020499994, 520.121211945), + CubicToCommand(129.18526764499993, 580.8594446650001, 369.56035492699993, + 491.358833718, 369.56035492699993, 491.358833718), + CubicToCommand(369.56035492699993, 491.358833718, 667.6014503669999, + 437.68318619800004, 687.3766889269999, 430.61353841280004), + CubicToCommand(707.151927487, 423.55095321280004, 859.8273630479999, + 433.56923031900004, 859.8273630479999, 433.56923031900004), + LineToCommand(851.2286655669999, 394.91393587310006), + CubicToCommand(736.814785327, 312.98794755310007, 717.0395467669999, + 364.22523753280007, 693.026757087, 357.16265233280006), + CubicToCommand(669.0139674069999, 350.0983014865001, 673.2515185269999, + 367.05027161280003, 667.6014503669999, 368.46278865280004), + CubicToCommand(661.9513822069998, 369.8753056928001, 592.7380472469999, + 326.08551180650005, 581.4379109269998, 327.49802884650006), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(120.44531845999998, 466.18071748), + CubicToCommand(97.84504582, 427.33649888, 114.79525029999996, + 525.50643316, 114.79525029999996, 525.50643316), + CubicToCommand(130.33293773999998, 586.24466588, 371.87335157999996, + 494.43105828, 371.87335157999996, 494.43105828), + CubicToCommand(371.87335157999996, 494.43105828, 669.91444702, + 440.75541076, 689.68968558, 433.69282555999996), + CubicToCommand(709.46492414, 426.63024036, 860.60424742, 436.51785964, + 860.60424742, 436.51785964), + LineToCommand(852.1291451799999, 396.96738252), + CubicToCommand(737.7152649399999, 315.04139419999996, 719.35254342, + 367.30452468, 695.3397537399999, 360.24193948), + CubicToCommand(671.3269640599999, 353.17935428, 675.56451518, + 370.12955875999995, 669.9144470199999, 371.54207579999996), + CubicToCommand(664.2643788599999, 372.95459284000003, 595.0510438999999, + 329.1665646, 583.7509075799999, 330.57908163999997), + CubicToCommand(572.45077126, 331.99159868, 527.9211715739999, + 289.95685721589996, 554.0880497399999, 346.11676908), + CubicToCommand(593.338366989, 430.3504571141, 446.8091461982999, + 412.5527424101, 421.3114479799999, 395.55486548), + CubicToCommand(391.6485901399999, 375.77962691999994, 434.0241013399999, + 428.0427573999999, 434.0241013399999, 428.0427573999999), + CubicToCommand(466.51199325999994, 463.3556834, 405.7737605399999, + 433.69282555999996, 405.7737605399999, 433.69282555999996), + CubicToCommand(345.0355278199999, 411.09255292, 302.6600166199999, + 456.2930981999999, 297.0099484599999, 457.70561523999993), + CubicToCommand(291.3598802999999, 459.11813227999994, 282.8847780599999, + 464.76820044, 281.4722610199999, 453.46806411999995), + CubicToCommand(280.0597439799999, 442.1679277999999, 269.3510991704999, + 415.93748636719994, 210.8464090199999, 459.11813227999994), + CubicToCommand(151.5206933399999, 502.90616051999996, 133.1579718199999, + 474.65581971999995, 133.1579718199999, 474.65581971999995), + LineToCommand(120.44531845999987, 466.18071747999994), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(193.89620453999999, 519.15010648), + CubicToCommand(193.89620453999999, 519.15010648, 181.18355117999997, + 539.63160356, 217.90899421999995, 562.93813472), + CubicToCommand(217.90899421999995, 562.93813472, 220.38089903999997, + 565.4100395400001, 188.59926563999997, 557.99432508), + CubicToCommand(188.59926563999997, 557.99432508, 177.65225857999997, + 554.46303248, 174.82722449999997, 536.10031096), + CubicToCommand(174.82722449999997, 536.10031096, 166.35212226, + 528.3314672399999, 157.87702001999997, 518.44384796), + CubicToCommand(149.40191778, 508.55622868, 193.89620453999999, + 519.15010648, 193.89620453999999, 519.15010648), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(441.08668653999996, 435.1053426), + CubicToCommand(441.08668653999996, 435.1053426, 472.33509475739993, + 482.459976366, 471.27923826999995, 490.89976568), + CubicToCommand(468.98389808, 509.2624872, 468.63076881999996, + 526.21269168, 474.28083698, 533.27527688), + CubicToCommand(479.9326707863, 540.33786208, 495.4703582263, + 598.9573192400001, 495.4703582263, 598.9573192400001), + CubicToCommand(495.4703582263, 598.9573192400001, 494.7640997063, + 601.0760948, 516.65634818, 533.9815354), + CubicToCommand(516.65634818, 533.9815354, 537.13784526, 505.7311946, + 501.82491926, 473.24330268), + CubicToCommand(501.82491926, 473.24330268, 439.67416949999995, + 422.39268924, 441.08668654, 435.1053426), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(229.20913054, 566.46942732), + CubicToCommand(229.20913054, 566.46942732, 248.98436909999998, + 579.18208068, 223.55906237999997, 634.27024524), + LineToCommand(234.85919869999998, 630.03269412), + CubicToCommand(234.85919869999998, 630.03269412, 233.44668165999997, + 649.80793268, 227.79661349999998, 654.0454838), + LineToCommand(240.50926685999997, 648.39541564), + CubicToCommand(240.50926685999997, 648.39541564, 248.98436909999998, + 662.52058604, 241.92178389999998, 670.9956882800001), + CubicToCommand(241.92178389999998, 670.9956882800001, 271.58464173999994, + 685.1208586800001, 270.1721247, 696.4209950000001), + CubicToCommand(270.1721247, 696.4209950000001, 281.47226101999996, + 682.2958246000001, 274.40967581999996, 670.9956882800001), + CubicToCommand(267.34709061999996, 659.69555196, 254.63443725999997, + 666.75813716, 256.0469543, 634.27024524), + LineToCommand(240.50926685999997, 639.9203134), + CubicToCommand(240.50926685999997, 639.9203134, 250.39688613999996, + 624.38262596, 250.39688613999996, 613.0824896400001), + LineToCommand(236.27171573999996, 617.32004076), + CubicToCommand(236.27171573999996, 617.32004076, 263.5844983547, + 570.389162106, 244.74681797999995, 567.88194436), + CubicToCommand(234.15294017999997, 566.46942732, 229.20913053999993, + 566.46942732, 229.20913053999993, 566.46942732), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(286.41607065999995, 596.13228516), + CubicToCommand(286.41607065999995, 596.13228516, 291.3598803, + 588.36344144, 286.41607065999995, 589.77595848), + CubicToCommand(281.47226101999996, 591.18847552, 226.38409645999997, + 617.32004076, 215.79021866, 634.27024524), + CubicToCommand(215.79021866, 634.27024524, 276.52845138, 591.18847552, + 286.41607066, 596.13228516), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(304.77879218, 610.25745556), + CubicToCommand(304.77879218, 610.25745556, 309.72260181999997, + 602.48861184, 304.77879218, 603.90112888), + CubicToCommand(299.83498254, 605.31364592, 244.74681797999997, + 631.44521116, 234.15294017999997, 648.39541564), + CubicToCommand(234.15294017999997, 648.39541564, 294.89117289999996, + 605.31364592, 304.77879218, 610.25745556), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(328.08532334, 583.4196318), + CubicToCommand(328.08532334, 583.4196318, 333.02913298, 575.65078808, + 328.08532334, 577.06330512), + CubicToCommand(323.14151369999996, 578.47582216, 268.05334913999997, + 604.6073874, 257.45947133999994, 621.55759188), + CubicToCommand(257.45947133999994, 621.55759188, 318.19770406, + 578.47582216, 328.08532333999995, 583.4196318), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(287.12232917999995, 660.40181048), + CubicToCommand(287.12232917999995, 660.40181048, 287.12232917999995, + 649.80793268, 282.17851953999997, 651.22044972), + CubicToCommand(277.2347099, 652.63296676, 213.67144309999998, + 683.7083416400001, 203.07756529999997, 700.65854612), + CubicToCommand(203.07756529999997, 700.65854612, 277.2347099, + 655.4580008400001, 287.12232917999995, 660.40181048), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(289.24110473999997, 641.3328304400001), + CubicToCommand(289.24110473999997, 641.3328304400001, 291.3598803, + 632.8577282, 286.41607065999995, 634.27024524), + CubicToCommand(282.88477806, 634.27024524, 236.27171574, + 654.7517423200001, 225.67783793999996, 671.7019468), + CubicToCommand(225.67783793999996, 671.7019468, 277.94096842, + 633.56398672, 289.24110473999997, 641.3328304400001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(263.81579802, 725.37759432), + LineToCommand(246.15933501999996, 738.7965062), + CubicToCommand(246.15933501999996, 738.7965062, 264.52205654, + 725.37759432, 270.87838322, 727.4963698800001), + CubicToCommand(270.87838322, 727.4963698800001, 258.87198837999995, + 747.27160844, 257.45947133999994, 756.4529692), + CubicToCommand(257.45947133999994, 756.4529692, 275.82219286, + 733.85269656, 285.70981213999994, 734.55895508), + CubicToCommand(285.70981213999994, 734.55895508, 299.12872402, + 735.2652136, 299.12872402, 754.3341936400001), + CubicToCommand(299.12872402, 754.3341936400001, 309.01634329999996, + 735.97147212, 314.66641145999995, 736.67773064), + CubicToCommand(314.66641145999995, 736.67773064, 316.78518701999997, + 747.97786696, 314.66641145999995, 759.9842618), + CubicToCommand(314.66641145999995, 759.9842618, 321.72899665999995, + 746.56534992, 328.79158185999995, 749.390384), + CubicToCommand(328.79158185999995, 749.390384, 340.09171818, 745.8590914, + 338.67920114, 766.3405884800001), + CubicToCommand(338.67920114, 766.3405884800001, 338.67920114, 784.70331, + 337.26668409999996, 789.64711964), + CubicToCommand(337.26668409999996, 789.64711964, 347.15430338, + 743.0340573200001, 351.39185449999997, 742.3277988), + CubicToCommand(351.39185449999997, 742.3277988, 365.51702489999997, + 740.2090232400001, 373.99212714, 755.7467106800001), + CubicToCommand(373.99212714, 755.7467106800001, 366.92954194, 742.3277988, + 375.40464418, 745.8590914), + CubicToCommand(375.40464418, 745.8590914, 394.47362422, 748.68412548, + 400.12369237999997, 760.69052032), + CubicToCommand(400.12369237999997, 760.69052032, 388.11729754, + 739.50276472, 398.00491681999995, 745.1528328799999), + LineToCommand(412.13008721999995, 756.4529691999999), + CubicToCommand(412.13008721999995, 756.4529691999999, 426.96151613999996, + 793.8846707599998, 430.49280874, 796.7097048399999), + CubicToCommand(430.49280874, 796.7097048399999, 417.07389686, + 758.5717447599999, 419.89893093999996, 758.5717447599999), + CubicToCommand(419.89893093999996, 758.5717447599999, 416.36763834, + 737.3839891599999, 425.54899909999995, 763.5155543999999), + CubicToCommand(425.54899909999995, 763.5155543999999, 419.89893093999996, + 738.7965062, 429.78655022, 740.20902324), + CubicToCommand(439.67416949999995, 741.62154028, 447.44301321999995, + 759.2780032799999, 462.2744421399999, 755.0404521599999), + CubicToCommand(462.2744421399999, 755.0404521599999, 479.2264122662999, + 764.9280714399999, 482.75770486629995, 642.7453474799999), + LineToCommand(263.8175636663, 725.3775943199998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(272.29090025999994, 561.52561768), + CubicToCommand(272.29090025999994, 561.52561768, 298.4224655, + 550.93173988, 369.04831749999994, 561.52561768), + CubicToCommand(369.04831749999994, 561.52561768, 381.76097086, + 562.2318762, 393.76736569999997, 546.69418876), + CubicToCommand(405.77376054, 531.15650132, 453.09308137999994, + 518.44384796, 464.3932177, 521.97514056), + LineToCommand(481.34518782629993, 533.27527688), + LineToCommand(482.75770486629995, 535.39405244), + CubicToCommand(482.75770486629995, 535.39405244, 504.64995333999997, + 553.75677396, 505.3562118599999, 567.17568584), + CubicToCommand(506.0624703799999, 580.5945977199999, 479.9326707862999, + 665.3456201199999, 462.98070065999997, 693.5959609199999), + CubicToCommand(446.03049618, 721.8463017199999, 429.0802917, + 743.7403158399999, 395.1798827399999, 739.50276472), + CubicToCommand(395.1798827399999, 739.50276472, 358.45443969999997, + 732.44017952, 313.25389441999994, 739.50276472), + CubicToCommand(313.25389441999994, 739.50276472, 261.69702245999997, + 736.6777306399999, 256.75321281999993, 722.5525602399999), + CubicToCommand(251.80940317999995, 708.4273898399999, 276.52845138, + 681.5895660799999, 276.52845138, 681.5895660799999), + CubicToCommand(276.52845138, 681.5895660799999, 284.2972951, + 666.7581371599999, 282.17851953999997, 641.33283044), + CubicToCommand(280.05974397999995, 615.90752372, 280.76600249999996, + 566.4694273199999, 272.29090025999994, 561.52561768), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(311.13511886, 565.05691028), + CubicToCommand(325.96654778, 597.5448022, 273.70341729999996, + 712.66494096, 273.70341729999996, 712.66494096), + CubicToCommand(270.1721247, 715.48997504, 296.05649945799996, + 726.172135155, 313.96015294, 721.8463017199999), + CubicToCommand(333.28691733979997, 717.184995488, 404.3612435, + 724.6713358, 404.3612435, 724.6713358), + CubicToCommand(446.03049618, 697.1272535200001, 468.63076881999996, + 618.7325578, 468.63076881999996, 618.7325578), + CubicToCommand(468.63076881999996, 618.7325578, 486.9952559863, + 576.3570466, 455.91811545999997, 570.7069784400001), + CubicToCommand(424.84274058, 565.05691028, 311.13511886, 565.05691028, + 311.13511886, 565.05691028), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(307.54909122469996, 619.61538095), + CubicToCommand(313.4216308185, 597.032764773, 316.2184145577, + 576.198138433, 311.13511886, 565.05691028), + CubicToCommand(311.13511886, 565.05691028, 421.31144797999997, + 576.3570466, 441.08668653999996, 539.63160356), + CubicToCommand(448.5747924983, 525.735967179, 474.6357318863, 579.8883392, + 473.92770771999994, 596.83854368), + CubicToCommand(473.92770771999994, 596.83854368, 362.69199082, + 622.2638504, 336.56042558, 602.48861184), + LineToCommand(307.54909122469996, 619.6153809499999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(315.37266997999996, 648.39541564), + CubicToCommand(315.37266997999996, 648.39541564, 318.90396258, 661.108069, + 314.66641145999995, 668.1706542000001), + CubicToCommand(314.66641145999995, 668.1706542000001, 311.84137738, + 669.5831712400001, 309.72260181999997, 670.28942976), + CubicToCommand(309.72260181999997, 670.28942976, 311.84137738, + 676.64575644, 322.43525517999996, 679.4707905199999), + CubicToCommand(322.43525517999996, 679.4707905199999, 325.96654778, + 687.23963424, 330.20409889999996, 687.94589276), + CubicToCommand(334.44165002, 688.65215128, 342.91675225999995, + 698.53977056, 349.97933745999995, 696.4209950000001), + CubicToCommand(357.04192265999995, 694.30221944, 376.81716122, + 687.23963424, 376.81716122, 687.23963424), + CubicToCommand(376.81716122, 687.23963424, 386.70478049999997, + 681.58956608, 402.24246794, 687.94589276), + CubicToCommand(402.24246794, 687.94589276, 406.43587790249995, + 686.53337572, 407.18627757999997, 679.47079052), + CubicToCommand(408.06910072999995, 671.17225291, 413.54260425999996, + 664.6393616, 417.07389686, 661.108069), + CubicToCommand(420.60518945999996, 657.5767764, 437.55539394, + 634.97650376, 435.43661837999997, 634.27024524), + CubicToCommand(433.31784281999995, 633.5639867200001, 315.37266997999996, + 648.39541564, 315.37266997999996, 648.39541564), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(307.60382625999995, 562.93813472), + CubicToCommand(307.60382625999995, 562.93813472, 302.66001661999996, + 602.48861184, 308.31008477999995, 617.32004076), + CubicToCommand(313.96015294, 632.15146968, 312.5476359, 635.68276228, + 311.13511886, 642.7453474800001), + CubicToCommand(309.72260181999997, 649.80793268, 317.49144554, + 667.46439568, 327.37906482, 678.05827348), + LineToCommand(348.56682042, 680.88330756), + CubicToCommand(348.56682042, 680.88330756, 375.40464418, 674.52698088, + 391.64859013999995, 679.47079052), + CubicToCommand(391.64859013999995, 679.47079052, 407.52881296219994, + 681.836756562, 413.54260425999996, 655.4580008400001), + CubicToCommand(413.54260425999996, 655.4580008400001, 422.0177065, + 644.15786452, 434.73035985999996, 639.21405488), + CubicToCommand(447.44301322, 634.2702452400001, 460.15566658, + 560.8193591600001, 453.09308137999994, 546.6941887600001), + CubicToCommand(446.03049618, 532.5690183600001, 420.60518945999996, + 524.80017464, 392.35484865999996, 552.34425692), + CubicToCommand(364.10450785999996, 579.8883392, 360.57321526, + 550.22548136, 307.60382625999995, 562.93813472), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(310.42886033999997, 695.0084779599999), + CubicToCommand(310.42886033999997, 695.0084779599999, 309.01634329999996, + 691.47718536, 301.24749957999995, 690.77092684), + CubicToCommand(301.24749957999995, 690.77092684, 261.69702245999997, + 684.41460016, 246.86559353999996, 662.52058604), + CubicToCommand(246.86559353999996, 662.52058604, 234.85919869999998, + 652.6329667599999, 242.62804241999999, 673.11446384), + CubicToCommand(242.62804241999999, 673.11446384, 260.99076393999997, + 709.1336483599999, 272.99715877999995, 714.077458), + CubicToCommand(272.99715877999995, 714.077458, 301.95375809999996, + 721.1400432, 310.42886033999997, 695.0084779599999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(451.57815685459997, 582.060084149), + CubicToCommand(452.7417177663, 568.093821916, 456.19002499019996, + 552.891607273, 453.09308138, 546.69418876), + CubicToCommand(441.7117253302, 523.935007953, 411.74341068030003, + 533.45184151, 392.35484866, 552.34425692), + CubicToCommand(364.10450786, 579.8883391999999, 360.57321526, + 550.22548136, 307.60382626, 562.93813472), + CubicToCommand(307.60382626, 562.93813472, 304.5227734665, + 587.5865570679999, 306.0059163585, 605.2783329939999), + CubicToCommand(306.0059163585, 605.2783329939999, 371.87335157999996, + 584.83214884, 373.28586862, 594.7197681199999), + CubicToCommand(373.28586862, 594.7197681199999, 376.1109027, 589.06969996, + 392.35484866, 589.06969996), + CubicToCommand(408.59879462, 589.06969996, 448.7531227746, 587.003893789, + 451.57815685459997, 582.060084149), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(375.40464418, 564.35065176), + CubicToCommand(375.40464418, 564.35065176, 383.87974641999995, 572.825754, + 377.52341974, 589.77595848), + CubicToCommand(377.52341974, 589.77595848, 352.09811301999997, + 618.02629928, 355.62940562, 642.74534748) + ], + ), + Path( + commands: const [ + MoveToCommand(290.65362178, 714.077458), + CubicToCommand(290.65362178, 714.077458, 282.88477806, 691.47718536, + 298.4224655, 703.4835802), + LineToCommand(304.77879218, 709.8399068800001), + CubicToCommand(302.66001661999996, 712.6649409600001, 292.77239734, + 719.7275261600001, 290.65362178, 714.077458), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(299.552479132, 716.19623356), + CubicToCommand(299.552479132, 716.19623356, 293.337404156, 698.116015448, + 305.76755410799996, 707.7211313199999), + LineToCommand(310.85261545199995, 712.8061926639999), + CubicToCommand(302.801268324, 715.0662199279999, 310.85261545199995, + 719.586274456, 299.552479132, 716.19623356), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(308.027581372, 716.19623356), + CubicToCommand(308.027581372, 716.19623356, 301.812506396, 698.116015448, + 314.24265634799997, 707.7211313199999), + LineToCommand(319.32771769199996, 712.8061926639999), + CubicToCommand(313.39514612399995, 715.0662199279999, 319.32771769199996, + 719.586274456, 308.027581372, 716.19623356), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(319.68084695199997, 716.5493628199999), + CubicToCommand(319.68084695199997, 716.5493628199999, 313.465771976, + 698.4691447079999, 325.89592192799995, 708.07426058), + CubicToCommand(325.89592192799995, 708.07426058, 333.63474966089996, + 712.1882164589999, 330.9827489183, 713.159321924), + CubicToCommand(325.754670224, 715.0662199279999, 330.9827489183, + 719.9394037159999, 319.68084695199997, 716.5493628199999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(331.12223497599996, 716.408111116), + CubicToCommand(331.12223497599996, 716.408111116, 324.90716, + 698.327893004, 337.3390755983, 707.933008876), + LineToCommand(342.4241369423, 713.0180702199999), + CubicToCommand(340.7291164943, 715.2780974839999, 342.4241369423, + 719.798152012, 331.12223497599996, 716.408111116), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(342.91675225999995, 717.6087506), + CubicToCommand(342.91675225999995, 717.6087506, 334.44165002, + 695.7147364799999, 350.68559597999996, 707.0148728), + LineToCommand(357.04192265999995, 713.3711994800001), + CubicToCommand(354.9231471, 716.1962335600001, 357.04192265999995, + 721.84630172, 342.91675225999995, 717.6087506), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(292.77239734, 687.23963424), + CubicToCommand(292.77239734, 687.23963424, 316.07892849999996, + 682.2958246000001, 326.6728063, 687.94589276), + CubicToCommand(326.6728063, 687.94589276, 337.26668409999996, + 690.06466832, 339.38545966, 689.3584098), + CubicToCommand(341.50423522, 688.6521512800001, 347.15430338, + 687.94589276, 347.15430338, 687.94589276) + ], + ), + Path( + commands: const [ + MoveToCommand(352.80437154, 702.77732168), + CubicToCommand(352.80437154, 702.77732168, 373.99212714, 678.764532, + 395.17988274, 686.53337572), + CubicToCommand(407.5676571808, 691.071086711, 405.77376053999996, + 685.12085868, 407.18627757999997, 680.17704904), + CubicToCommand(408.59879462, 675.2332394, 408.95192388, 667.81752494, + 417.78015538, 662.52058604) + ], + ), + Path( + commands: const [ + MoveToCommand(383.1734879, 674.52698088), + CubicToCommand(383.1734879, 674.52698088, 376.1109027, 655.45800084, + 371.16709305999996, 678.05827348), + CubicToCommand(366.22328342, 700.65854612, 360.57321526, 707.0148728, + 357.74818117999996, 711.9586824400001), + CubicToCommand(357.74818117999996, 711.9586824400001, 357.74818117999996, + 721.1400432, 372.57961009999997, 720.43378468), + CubicToCommand(372.57961009999997, 720.43378468, 391.64859013999995, + 719.7275261600001, 392.35484866, 714.7837165200001), + CubicToCommand(393.06110718, 709.8399068800001, 390.2360731, 689.3584098, + 383.1734879, 674.52698088), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(407.8925361, 687.23963424), + CubicToCommand(407.8925361, 687.23963424, 414.24886277999997, + 683.0020831200001, 418.4864139, 685.1208586800001) + ], + ), + Path( + commands: const [ + MoveToCommand(419.36923705, 658.28303492), + CubicToCommand(419.36923705, 658.28303492, 424.48961132, 649.63136805, + 432.96471355999995, 648.21885101) + ], + ), + Path( + commands: const [ + MoveToCommand(279.35348546, 723.2588187599999), + CubicToCommand(279.35348546, 723.2588187599999, 311.13511886, + 728.90888692, 318.90396258, 726.0838528400001), + LineToCommand(319.6102211, 729.61514544), + LineToCommand(282.88477806, 727.4963698800001), + CubicToCommand(282.88477806, 727.4963698800001, 262.40328098, 717.6087506, + 279.35348545999994, 723.25881876), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(304.07253366, 558.7005836), + LineToCommand(338.67920114, 560.11310064), + CubicToCommand(338.67920114, 560.11310064, 351.39185449999997, + 614.4950066800001, 345.03552781999997, 627.9139185600001), + CubicToCommand(345.03552781999997, 627.9139185600001, 342.91675225999995, + 632.8577282000001, 337.97294261999997, 622.97010892), + CubicToCommand(337.97294261999997, 622.97010892, 305.4850507, + 565.05691028, 299.83498254, 561.5256176800001), + CubicToCommand(294.18491437999995, 557.9943250800001, 301.95375809999996, + 558.7005836000001, 304.07253366, 558.7005836000001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(167.94120392999997, 553.9333385900001), + CubicToCommand(167.94120392999997, 553.9333385900001, 183.655456, + 556.9349373, 205.90259937999997, 561.5256176800001), + CubicToCommand(205.90259937999997, 561.5256176800001, 214.37770161999998, + 601.0760948000001, 220.02776977999997, 609.55119704), + CubicToCommand(225.67783793999996, 618.02629928, 219.32151125999997, + 618.0262992800001, 212.96518457999997, 613.0824896400001), + CubicToCommand(206.60885789999998, 608.13868, 180.47729265999996, + 583.4196318, 176.94600005999996, 575.6507880800001), + CubicToCommand(173.41470745999996, 567.88194436, 167.94120392999997, + 553.9333385900001, 167.94120392999997, 553.9333385900001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(206.53999769429998, 561.914059866), + CubicToCommand(206.53999769429998, 561.914059866, 216.78074623429995, + 564.650811631, 218.56228335099996, 568.552889954), + CubicToCommand(220.34205482139998, 572.47262474, 216.43997649839997, + 578.281601067, 216.43997649839997, 578.281601067), + CubicToCommand(216.43997649839997, 578.281601067, 214.67433019839996, + 584.1258903199999, 212.55202334579997, 580.312094312), + CubicToCommand(210.42971649319998, 576.480641841, 205.3587803196, + 562.955791183, 206.53999769429998, 561.914059866), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(206.60885789999998, 561.52561768), + CubicToCommand(206.60885789999998, 561.52561768, 212.96518457999997, + 570.70697844, 219.32151125999997, 570.70697844), + CubicToCommand(225.67783794, 570.70697844, 226.35231482659998, + 569.983063457, 231.32790609999998, 571.0601077), + CubicToCommand(239.44987907999996, 572.825754, 238.74362055999998, + 569.2944613999999, 250.39688614, 571.41323696), + CubicToCommand(255.05819237199995, 572.2607471839999, 259.57824689999995, + 570.70697844, 264.52205654, 572.825754), + CubicToCommand(269.46586618, 574.94452956, 275.11593433999997, + 573.53201252, 277.2347099, 570.0007199199999), + CubicToCommand(279.35348545999994, 566.46942732, 287.82858769999996, + 559.05371286, 287.82858769999996, 559.05371286), + CubicToCommand(287.82858769999996, 559.05371286, 265.22831506, + 562.2318762, 260.28450541999996, 563.64439324), + CubicToCommand(260.28450541999996, 563.64439324, 220.73402829999998, + 565.7631687999999, 206.60885789999998, 561.52561768), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(285.35668288, 561.87874694), + CubicToCommand(285.35668288, 561.87874694, 273.968264245, + 568.0585089900001, 273.262005725, 572.29606011), + CubicToCommand(272.555747205, 576.53361123, 282.53164879999997, + 583.06650254, 282.53164879999997, 583.06650254), + CubicToCommand(282.53164879999997, 583.06650254, 287.387176125, + 591.18847552, 288.44656390499995, 586.9509244), + CubicToCommand(289.50595168499996, 582.71337328, 286.76919992, + 562.5850054599999, 285.35668288, 561.87874694), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(219.17143132449996, 571.519175738), + CubicToCommand(219.17143132449996, 571.519175738, 231.54331494859997, + 591.276757835, 231.92646019569997, 571.483862812), + CubicToCommand(231.92646019569997, 571.483862812, 232.9099251848, + 569.259148474, 229.80238769679997, 569.223835548), + CubicToCommand(219.07608642429994, 569.100240307, 221.76163444659997, + 561.843434014, 219.17143132449996, 571.519175738), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(231.84524046589996, 571.960587313), + CubicToCommand(231.84524046589996, 571.960587313, 245.83092480819997, + 591.71816941, 244.70797376139998, 571.801679146), + CubicToCommand(244.70797376139998, 571.801679146, 244.72033328549998, + 571.2190158669999, 241.62515532159998, 570.954168922), + CubicToCommand(233.24363233549997, 570.212597476, 233.85278030899997, + 562.2318762, 231.84524046589996, 571.960587313), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(244.58084722779998, 571.978243776), + CubicToCommand(244.58084722779998, 571.978243776, 258.6353917758, + 590.747063945, 257.4541744011, 573.673264224), + CubicToCommand(257.4541744011, 573.673264224, 257.6642863108, + 571.5015192750001, 254.7439073306, 570.971825385), + CubicToCommand(247.87201193099997, 569.718216512, 247.49946056169998, + 563.9975225000001, 244.58084722779998, 571.978243776), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(256.72143118659994, 572.11949548), + CubicToCommand(256.72143118659994, 572.11949548, 270.6700369566, + 592.530366708, 271.284481869, 575.262345894), + CubicToCommand(271.284481869, 575.262345894, 274.18720438619994, + 572.825754, 271.1043859464, 572.437311814), + CubicToCommand(260.831855773, 571.130733552, 262.24084152039995, + 563.273607517, 256.72143118659994, 572.11949548), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(192.85094193039998, 578.352226919), + LineToCommand(179.32962256499997, 575.65078808), + CubicToCommand(174.73894218499998, 566.82255658, 171.03108495499995, + 555.963831835, 171.03108495499995, 555.963831835), + CubicToCommand(171.03108495499995, 555.963831835, 182.24293895999995, + 557.729478135, 204.31351770999996, 562.6732877750001), + CubicToCommand(204.31351770999996, 562.6732877750001, 205.86022386879995, + 568.535233491, 208.45925522239997, 578.758325568), + LineToCommand(192.85094193039996, 578.352226919), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(307.73801537879996, 570.124315161), + CubicToCommand(304.79644864299996, 565.692542948, 302.47109246589997, + 562.602661923, 301.32342237089995, 561.87874694), + CubicToCommand(296.00353006899996, 558.559331896, 303.3186026899, + 559.2126210270001, 305.3120173626, 559.2126210270001), + LineToCommand(337.8952541828, 560.554512215), + CubicToCommand(337.8952541828, 560.554512215, 338.820452844, + 564.509559927, 340.02815491319996, 570.4951008840001), + CubicToCommand(340.02815491319996, 570.4951008840001, 322.21631503879996, + 566.9461518210001, 307.73801537879996, 570.124315161), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(402.383719644, 326.2020444623), + CubicToCommand(451.3292007263, 333.1940038103, 496.3884943023, + 270.2663696783, 499.4960317903, 253.173147848), + CubicToCommand(502.601803632, 236.081691664, 484.7352287223, 215.10581362, + 484.7352287223, 215.10581362), + CubicToCommand(487.06588183829996, 209.667623016, 478.5201537463, + 184.807323112, 469.1975412823, 168.4927513), + CubicToCommand(459.87492881829996, 152.178179488, 431.799387002, + 153.8979189842, 400.8299509, 152.178179488), + CubicToCommand(372.862113508, 150.624410744, 340.232969884, 191.79928246, + 337.902316768, 194.906819948), + CubicToCommand(335.571663652, 198.01435743599998, 346.44804486, + 265.6050634463, 348.778697976, 275.7045602823), + CubicToCommand(351.109351092, 285.8040571183, 346.44804486, + 332.4171194383, 346.44804486, 332.4171194383), + CubicToCommand(406.903774172, 316.3497381083, 353.440004208, + 319.2100851143, 402.38371964399994, 326.2020444623), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(339.1877072744, 196.0509587504), + CubicToCommand(336.89942966959995, 199.1019955568, 347.57805849199997, + 265.4638117423, 349.86633609679996, 275.3796813631), + CubicToCommand(352.1546137016, 285.2955509839, 347.57805849199997, + 331.0611030799, 347.57805849199997, 331.0611030799), + CubicToCommand(405.2652544056, 315.3821639359, 354.4428913064, + 318.0941966527, 402.4967210072, 324.9590294671), + CubicToCommand(450.5523163543, 331.8238622815, 494.7923500471, + 270.0403669519, 497.84338685349996, 253.2578988704), + CubicToCommand(500.8944236599, 236.4771964352, 483.35096202309995, + 215.882697992, 483.35096202309995, 215.882697992), + CubicToCommand(485.6392396279, 210.5433835808, 477.24888841029997, + 186.1350891296, 468.09577799109996, 170.117145896), + CubicToCommand(458.94266757189996, 154.0992026624, 431.3791631826, + 155.7889261715, 400.9712026039999, 154.0992026624), + CubicToCommand(373.51187134639997, 152.5736842592, 341.4759848792, + 192.999921944, 339.18770727439994, 196.0509587504), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(340.4730977808, 197.1950975528), + CubicToCommand(338.2271956872, 200.1896336776, 348.70807212399995, + 265.3225600383, 350.9539742176, 275.05480244390003), + CubicToCommand(353.1998763112, 284.78704484950003, 348.70807212399995, + 329.70508672150004, 348.70807212399995, 329.70508672150004), + CubicToCommand(404.15642852919996, 313.53176661350005, 355.44577840479997, + 316.97830819110004, 402.60972237039994, 323.7160144719), + CubicToCommand(449.7754319823, 330.4537207527, 493.19620579189996, + 269.8143642255, 496.19074191669995, 253.3426498928), + CubicToCommand(499.18527804149994, 236.87270120640002, 481.96669532389996, + 216.65958236400002, 481.96669532389996, 216.65958236400002), + CubicToCommand(484.2125974175, 211.4191441456, 475.97762307429997, + 187.4628551472, 466.99401469989994, 171.741540492), + CubicToCommand(458.01040632549996, 156.02022583680002, 430.95717371689994, + 157.6781677125, 401.11245430799994, 156.02022583680002), + CubicToCommand(374.16162918479995, 154.52295777440003, 342.71899987439997, + 194.20056142800001, 340.47309778079995, 197.1950975528), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(341.75848828719995, 198.3392363552), + CubicToCommand(339.5549617048, 201.27727179840002, 349.838085756, + 265.1813083343, 352.0416123384, 274.7299235247), + CubicToCommand(354.24513892079995, 284.2785387151, 349.838085756, + 328.3490703631, 349.838085756, 328.3490703631), + CubicToCommand(401.81165024279994, 312.3876278111, 356.4486655032, + 315.8624197295, 402.7227237336, 322.4729994767), + CubicToCommand(448.99854761029997, 329.0835792239, 491.60006153669997, + 269.5883614991, 494.5380969799, 253.4291665615), + CubicToCommand(497.4761324231, 237.26820597760002, 480.5824286247, + 217.436466736, 480.5824286247, 217.436466736), + CubicToCommand(482.7859552071, 212.2949047104, 474.70635773829997, + 188.79062116480003, 465.8922514087, 173.36593508800001), + CubicToCommand(457.0781450791, 157.9412490112, 430.5351842512, + 159.56740925350002, 401.253706012, 157.9412490112), + CubicToCommand(374.8113870232, 156.47223128960002, 343.9620148696, + 195.401200912, 341.7584882872, 198.3392363552), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(343.04387879359996, 199.4833751576), + CubicToCommand(340.8827277224, 202.36490991920002, 350.968099388, + 265.0400566303, 353.12925045919997, 274.4050446055), + CubicToCommand(355.29040153039995, 283.7700325807, 350.968099388, + 326.9930540047, 350.968099388, 326.9930540047), + CubicToCommand(400.1731304764, 311.2434890087, 357.4515526016, + 314.7465312679, 402.8357250968, 321.22998448149997), + CubicToCommand(448.2216632383, 327.71343769509997, 490.0039172815, + 269.3623587727, 492.8854520431, 253.51215193759998), + CubicToCommand(495.76698680469997, 237.66371074879999, 479.1981619255, + 218.21335110799998, 479.1981619255, 218.21335110799998), + CubicToCommand(481.3593129967, 213.1706652752, 473.43509240230003, + 190.1183871824, 464.7904881175, 174.990329684), + CubicToCommand(456.14588383269995, 159.86227218559998, 430.1131947855, + 161.45665079449998, 401.394957716, 159.86227218559998), + CubicToCommand(375.46114486159996, 158.4215048048, 345.2050298648, + 196.601840396, 343.0438787936, 199.4833751576), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(402.94872646, 319.98520384), + CubicToCommand(447.44301322, 326.34153052, 488.4077730263, 269.1345904, + 491.23280710629996, 253.59690296), + CubicToCommand(494.0578411862999, 238.05921552, 477.81389522629996, + 218.99023548, 477.81389522629996, 218.99023548), + CubicToCommand(479.9326707863, 214.04642583999998, 472.16206142, + 191.4461532, 463.68695918, 176.61472428), + CubicToCommand(455.21185693999996, 161.78329536, 429.69120531979996, + 163.34765798179998, 401.53620942, 161.78329536), + CubicToCommand(376.11090269999994, 160.37077832, 346.44804486, + 197.80247988, 344.32926929999996, 200.62751396), + CubicToCommand(342.21049373999995, 203.45254804, 352.09811301999997, + 264.89703928, 354.21688858, 274.07840004), + CubicToCommand(356.33566413999995, 283.2597608, 352.09811301999997, + 325.635272, 352.09811301999997, 325.635272), + CubicToCommand(397.12209366999997, 310.45071382000003, 358.45443969999997, + 313.62887716, 402.94872646, 319.98520384), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(484.87648042629996, 259.95322964), + CubicToCommand(484.87648042629996, 259.95322964, 435.78974764, + 273.37214152, 415.30825056, 270.54710744), + CubicToCommand(415.30825056, 270.54710744, 387.41103902, 258.89384186, + 371.87335157999996, 297.3849312), + CubicToCommand(371.87335157999996, 297.3849312, 365.51702489999997, + 310.09758456, 361.9857323, 313.62887716), + CubicToCommand(358.45443969999997, 317.16016976000003, 484.87648042629996, + 259.95322964, 484.87648042629996, 259.95322964), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(491.58593636629996, 256.06880778), + CubicToCommand(491.58593636629996, 256.06880778, 440.38042801999995, + 277.60969264, 422.72396502, 276.90343412), + CubicToCommand(422.72396502, 276.90343412, 393.76736569999997, + 268.78146114000003, 378.93593677999996, 294.55989712), + CubicToCommand(378.93593677999996, 294.55989712, 364.10450785999996, + 310.80384308, 358.45443969999997, 313.62887716), + CubicToCommand(358.45443969999997, 313.62887716, 357.74818117999996, + 316.45391124, 369.0483175, 309.39132604), + LineToCommand(387.41103902, 318.57268680000004), + CubicToCommand(387.41103902, 318.57268680000004, 413.54260425999996, + 335.52289128, 430.49280874, 307.27255048), + CubicToCommand(430.49280874, 307.27255048, 437.55539394, 287.49731192, + 437.55539394, 283.96601932000004), + CubicToCommand(437.55539394, 280.43472672, 474.9870955, + 270.54710744000005, 477.81389522629996, 269.84084892000004), + CubicToCommand(480.63892930629993, 269.13459040000004, 492.29219488629997, + 261.71887594000003, 491.58593636629996, 256.06880778000004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(407.8925361, 319.4802289982), + CubicToCommand(395.7590147264, 319.4802289982, 380.97525825649996, + 312.6560060487, 380.97525825649996, 301.62248232), + CubicToCommand(380.97525825649996, 290.5907242376, 395.7590147264, + 279.5289501681, 407.8925361, 279.5289501681), + CubicToCommand(420.0295887662, 279.5289501681, 429.8677699498, + 288.47194867760004, 429.8677699498, 299.50370676), + CubicToCommand(429.8677699498, 310.5372304887, 420.02958876619994, + 319.4802289982, 407.8925361, 319.4802289982), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(401.4955995551, 290.02218612900003), + CubicToCommand(392.9392775853, 291.2969827576, 383.95390356459995, + 293.9507491465, 384.08103009819996, 293.5693695457), + CubicToCommand(386.8001254002, 285.413849286, 398.0314015145, + 279.5289501681, 407.8925361, 279.5289501681), + CubicToCommand(415.4777526048, 279.5289501681, 422.1660207892, + 283.0213985495, 426.114005916, 288.3359939125), + CubicToCommand(426.114005916, 288.3359939125, 416.7278301852, + 287.7533306335, 401.4955995551, 290.02218612900003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(422.72396502, 289.61608748000003), + CubicToCommand(422.72396502, 289.61608748000003, 414.9551213, + 283.96601932, 414.9551213, 287.85044118), + CubicToCommand(414.9551213, 287.85044118, 421.31144797999997, 295.6192849, + 422.72396502, 289.61608748000003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(405.06750202, 303.9637293138), + CubicToCommand(400.6551519163, 303.9637293138, 397.07795251249996, + 300.38652991000004, 397.07795251249996, 295.97241416), + CubicToCommand(397.07795251249996, 291.5600640563, 400.6551519163, + 287.9828646525, 405.06750202, 287.9828646525), + CubicToCommand(409.48161776999996, 287.9828646525, 413.0588171738, + 291.5600640563, 413.0588171738, 295.97241415999997), + CubicToCommand(413.0588171738, 300.38652991, 409.48161776999996, + 303.9637293138, 405.06750202, 303.9637293138), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(221.44028681999998, 280.43472672), + CubicToCommand(221.44028681999998, 280.43472672, 215.79021865999997, + 243.00302516, 220.02776977999997, 235.23418144000001), + CubicToCommand(220.02776977999997, 235.23418144000001, 239.09674981999999, + 217.57771844, 238.39049129999998, 211.22139176000002), + CubicToCommand(238.39049129999998, 211.22139176000002, 237.68423277999997, + 179.43975836, 235.56545721999998, 178.02724132), + CubicToCommand(233.44668165999997, 176.61472428, 220.02776977999997, + 166.02084648, 209.43389197999997, 177.32098280000002), + CubicToCommand(209.43389197999997, 177.32098280000002, 191.07117045999996, + 209.1026162, 192.48368749999997, 220.40275252), + LineToCommand(192.48368749999997, 223.93404512), + CubicToCommand(192.48368749999997, 223.93404512, 179.06477561999998, + 223.2277866, 176.23974153999998, 226.7590792), + CubicToCommand(176.23974153999998, 226.7590792, 174.12096597999997, + 235.94043996, 172.00219041999995, 236.64669848), + CubicToCommand(172.00219041999995, 236.64669848, 167.05838077999996, + 240.8842496, 170.58967337999997, 245.82805924000002), + CubicToCommand(170.58967337999997, 245.82805924000002, 167.05838077999996, + 250.06561036, 167.76463929999997, 257.12819556), + LineToCommand(181.18355117999997, 264.19078076), + CubicToCommand(181.18355117999997, 264.19078076, 184.71484377999997, + 289.61608748000003, 203.78382381999995, 298.79744824), + CubicToCommand(212.32248932679994, 302.9096384727, 217.90899421999995, + 291.02860452, 221.44028681999995, 280.43472672), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(219.67464051999997, 277.185937528), + CubicToCommand(219.67464051999997, 277.185937528, 214.58957917599997, + 243.497406124, 218.40337518399997, 236.505446776), + CubicToCommand(218.40337518399997, 236.505446776, 235.56545721999998, + 220.61463007600003, 234.92982455199996, 214.893936064), + CubicToCommand(234.92982455199996, 214.893936064, 234.29419188399999, + 186.290466004, 232.38729387999996, 185.019200668), + CubicToCommand(230.480395876, 183.747935332, 218.40337518399997, + 174.213445312, 208.86888516399998, 184.38356800000003), + CubicToCommand(208.86888516399998, 184.38356800000003, 192.342435796, + 212.98703806, 193.613701132, 223.157160748), + LineToCommand(193.613701132, 226.335324088), + CubicToCommand(193.613701132, 226.335324088, 181.53668043999997, + 225.69969142000002, 178.99414976799997, 228.87785476000002), + CubicToCommand(178.99414976799997, 228.87785476000002, 177.08725176399997, + 237.141079444, 175.18035375999997, 237.776712112), + CubicToCommand(175.18035375999997, 237.776712112, 170.73092508399998, + 241.59050812, 173.90908842399998, 246.039936796), + CubicToCommand(173.90908842399998, 246.039936796, 170.73092508399998, + 249.853732804, 171.36655775199998, 256.210059484), + LineToCommand(183.443578444, 262.566386164), + CubicToCommand(183.443578444, 262.566386164, 186.621741784, + 285.44916221200003, 203.78382381999998, 293.712386896), + CubicToCommand(211.46791651759997, 297.4114158945, 216.49647718, + 286.72042754800003, 219.67464051999997, 277.185937528), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(234.77091638499996, 179.775231157), + CubicToCommand(232.84636191799996, 178.256775339, 219.62167113099997, + 168.068996188, 209.292640276, 179.0866291), + CubicToCommand(209.292640276, 179.0866291, 191.388986794, 210.073721665, + 192.766190908, 221.091354577), + LineToCommand(192.766190908, 224.53436486200002), + CubicToCommand(192.766190908, 224.53436486200002, 179.682751825, + 223.845762805, 176.928343597, 227.28877309), + CubicToCommand(176.928343597, 227.28877309, 174.862537426, 236.240599831, + 172.796731255, 236.929201888), + CubicToCommand(172.796731255, 236.929201888, 167.976516856, 241.06081423, + 171.419527141, 245.881028629), + CubicToCommand(171.419527141, 245.881028629, 167.976516856, 250.012640971, + 168.665118913, 256.898661541), + LineToCommand(181.748557996, 263.784682111), + CubicToCommand(181.748557996, 263.784682111, 185.19156828099997, + 288.574356163, 203.78382381999998, 297.526182904), + CubicToCommand(212.10708047819998, 301.534200005, 217.55586495999998, + 289.951560277, 220.99887524499997, 279.622529422), + CubicToCommand(220.99887524499997, 279.622529422, 215.490058789, + 243.126620401, 219.62167113099997, 235.551997774), + CubicToCommand(219.62167113099997, 235.551997774, 238.21392666999998, + 218.33694634900002, 237.52532461299998, 212.139527836), + CubicToCommand(237.52532461299998, 212.139527836, 236.83672255599998, + 181.152435271, 234.77091638499996, 179.775231157), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(233.97637554999997, 181.523220994), + CubicToCommand(232.24604217599997, 179.898826398, 219.21557248199997, + 170.117145896, 209.15138857199997, 180.8522754), + CubicToCommand(209.15138857199997, 180.8522754, 191.706803128, + 211.04482713000002, 193.04869431599997, 221.779956634), + LineToCommand(193.04869431599997, 225.134684604), + CubicToCommand(193.04869431599997, 225.134684604, 180.30072802999996, + 224.46373901, 177.61694565399998, 227.81846698), + CubicToCommand(177.61694565399998, 227.81846698, 175.60410887199998, + 236.540759702, 173.59127208999996, 237.211705296), + CubicToCommand(173.59127208999996, 237.211705296, 168.89465293199999, + 241.23737886, 172.24938090199996, 245.933998018), + CubicToCommand(172.24938090199996, 245.933998018, 168.89465293199996, + 249.959671582, 169.56559852599997, 256.669127522), + LineToCommand(182.31356481199995, 263.378583462), + CubicToCommand(182.31356481199995, 263.378583462, 185.66829278199998, + 287.532624846, 203.78382381999995, 296.254917568), + CubicToCommand(211.89520292219996, 300.1605271836, 217.20273569999995, + 288.874516034, 220.55746366999995, 278.810332124), + CubicToCommand(220.55746366999995, 278.810332124, 215.18989891799998, + 243.250215642, 219.21557248199997, 235.869814108), + CubicToCommand(219.21557248199997, 235.869814108, 237.33110351999994, + 219.096174258, 236.66015792599995, 213.057663912), + CubicToCommand(236.66015792599995, 213.057663912, 235.98921233199997, + 182.86511218200002, 233.97637554999994, 181.52322099399998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(233.18183471499998, 183.27121083100002), + CubicToCommand(231.645722434, 181.54087745700002, 218.80947383299997, + 172.165295604, 209.01013686799996, 182.6179217), + CubicToCommand(209.01013686799996, 182.6179217, 192.02461946199998, + 212.015932595, 193.331197724, 222.468558691), + LineToCommand(193.331197724, 225.735004346), + CubicToCommand(193.331197724, 225.735004346, 180.91870423499998, + 225.081715215, 178.30554771099997, 228.34816087000002), + CubicToCommand(178.30554771099997, 228.34816087000002, 176.34568031799998, + 236.840919573, 174.38581292499998, 237.49420870400002), + CubicToCommand(174.38581292499998, 237.49420870400002, 169.81278900799998, + 241.41394349, 173.079234663, 245.986967407), + CubicToCommand(173.079234663, 245.986967407, 169.81278900799998, + 249.906702193, 170.466078139, 256.439593503), + LineToCommand(182.87857162799997, 262.972484813), + CubicToCommand(182.87857162799997, 262.972484813, 186.14501728299996, + 286.490893529, 203.78382381999998, 294.983652232), + CubicToCommand(211.68155971989998, 298.7868543622, 216.84960644, + 287.797471791, 220.116052095, 277.998134826), + CubicToCommand(220.116052095, 277.998134826, 214.88973904699998, + 243.373810883, 218.80947383299997, 236.187630442), + CubicToCommand(218.80947383299997, 236.187630442, 236.44828037, + 219.85540216700002, 235.79499123899998, 213.975799988), + CubicToCommand(235.79499123899998, 213.975799988, 235.14170210799998, + 184.577789093, 233.18183471499998, 183.27121083100002), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(219.67464051999997, 277.009372898), + CubicToCommand(219.67464051999997, 277.009372898, 214.58957917599997, + 243.497406124, 218.40337518399997, 236.505446776), + CubicToCommand(218.40337518399997, 236.505446776, 235.56545721999998, + 220.614630076, 234.92982455199996, 214.893936064), + CubicToCommand(234.92982455199996, 214.893936064, 234.29419188399999, + 186.290466004, 232.38729387999996, 185.019200668), + CubicToCommand(231.04540269199998, 183.182928516, 218.40337518399997, + 174.213445312, 208.86888516399998, 184.38356800000003), + CubicToCommand(208.86888516399998, 184.38356800000003, 192.342435796, + 212.98703806, 193.613701132, 223.157160748), + LineToCommand(193.613701132, 226.335324088), + CubicToCommand(193.613701132, 226.335324088, 181.53668043999997, + 225.69969142000002, 178.99414976799997, 228.87785476000002), + CubicToCommand(178.99414976799997, 228.87785476000002, 177.08725176399997, + 237.141079444, 175.18035375999997, 237.776712112), + CubicToCommand(175.18035375999997, 237.776712112, 170.73092508399998, + 241.59050812, 173.90908842399998, 246.039936796), + CubicToCommand(173.90908842399998, 246.039936796, 170.73092508399998, + 249.853732804, 171.36655775199998, 256.210059484), + LineToCommand(183.443578444, 262.566386164), + CubicToCommand(183.443578444, 262.566386164, 186.621741784, + 285.44916221200003, 203.78382381999998, 293.712386896), + CubicToCommand(211.46791651759997, 297.4114158945, 216.49647718, + 286.543862918, 219.67464051999997, 277.009372898), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(214.20113698999995, 265.95642706), + CubicToCommand(214.20113698999995, 265.95642706, 176.06317690999995, + 247.9468348, 174.47409523999997, 246.53431776), + CubicToCommand(174.47409523999997, 246.53431776, 190.54147656999996, + 261.01261742, 191.95399360999997, 261.01261742), + CubicToCommand(193.36651065, 261.01261742, 214.20113698999998, + 265.95642706, 214.20113698999998, 265.95642706), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(184.00858526, 255.00942), + CubicToCommand(184.00858526, 255.00942, 216.49647718, 261.36574668000003, + 216.49647718, 269.1345904), + CubicToCommand(216.49647718, 274.2761524256, 216.06742512909997, + 297.9693601253, 206.60885789999998, 295.26615564), + CubicToCommand(191.77742897999997, 291.02860452, 198.13375565999996, + 265.6032978, 184.00858526, 255.00942), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(198.84001417999997, 261.71887594000003), + CubicToCommand(198.84001417999997, 261.71887594000003, 214.69198666139997, + 264.32143858620003, 216.49647717999997, 269.1345904), + CubicToCommand(217.55586495999995, 271.95962448, 218.72648845689997, + 286.6286139404, 209.08076271999997, 288.5566997), + CubicToCommand(201.04354076239994, 290.16520347930003, 197.10614951339997, + 272.118532647, 198.84001417999997, 261.71887594000003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(350.67676774849997, 336.8453603587), + CubicToCommand(349.7992415374, 333.7696045041, 352.11400383669996, + 334.0009041694, 355.27627636, 333.05098646), + CubicToCommand(358.80756895999997, 331.99159868, 380.34845382, + 325.28214274, 381.76097086, 320.69146236), + CubicToCommand(383.1734879, 316.10078197999997, 406.48001905999996, + 323.86962570000003, 406.48001905999996, 323.86962570000003), + CubicToCommand(409.6581824, 325.28214274, 417.42702612, 329.87282312, + 417.42702612, 329.87282312), + CubicToCommand(425.90212836, 331.99159868, 437.55539394, 332.6978572, + 437.55539394, 332.6978572), + CubicToCommand(441.79294505999997, 334.4635035, 447.79614247999996, + 339.40731314, 447.79614247999996, 339.40731314), + CubicToCommand(473.57457846, 357.41690539999996, 495.4703582263, + 344.70425204, 495.4703582263, 344.70425204), + CubicToCommand(530.78151858, 333.05098646, 520.18764078, + 302.68187009999997, 520.18764078, 302.68187009999997), + CubicToCommand(514.89070188, 286.7910534, 520.54077004, + 280.78785597999996, 520.54077004, 280.78785597999996), + CubicToCommand(520.8938993, 274.07840003999996, 533.60655266, + 285.37853636, 533.60655266, 285.37853636), + CubicToCommand(538.19723304, 292.79425082, 539.60975008, + 301.62248231999996, 539.60975008, 301.62248231999996), + CubicToCommand(553.73492048, 321.39772087999995, 547.73172306, + 289.96921674, 547.73172306, 289.96921674), + CubicToCommand(548.08485232, 288.20357043999996, 543.14104268, + 281.84724375999997, 543.14104268, 279.7284682), + CubicToCommand(543.14104268, 277.60969264, 539.96287934, 271.60649522, + 539.96287934, 271.60649522), + CubicToCommand(534.66594044, 265.6032978, 538.90349156, 253.2437737, + 538.90349156, 253.2437737), + CubicToCommand(542.0816549, 228.87785476, 538.19723304, 232.0560181, + 538.19723304, 232.0560181), + CubicToCommand(536.07845748, 228.87785476, 519.83451152, 246.53431776, + 519.83451152, 246.53431776), + CubicToCommand(515.95008966, 252.53751517999999, 505.35621186000003, + 255.36254925999998, 505.35621186000003, 255.36254925999998), + CubicToCommand(500.4141678663, 258.5407126, 494.4109704463, 256.06880778, + 494.4109704463, 256.06880778), + CubicToCommand(489.8202900663, 255.36254925999998, 479.93267078630004, + 267.72207335999997, 479.93267078630004, 267.72207335999997), + CubicToCommand(484.87648042629996, 267.36894409999996, 489.1140315463, + 275.13778781999997, 493.35158266630003, 275.49091708), + CubicToCommand(497.58913378629995, 275.84404634, 500.76729712630004, + 271.25336596, 503.59056556, 270.19397818), + CubicToCommand(506.41559964, 269.1345904, 511.35940928, 279.37533894, + 511.35940928, 279.37533894), + CubicToCommand(512.0656678, 283.96601932, 502.17804852, 292.44112156, + 502.17804852, 292.44112156), + CubicToCommand(501.47179, 300.56309454, 498.6485215663, 297.73806046, + 498.6485215663, 297.73806046), + CubicToCommand(493.35158266630003, 296.67867268, 491.2328071063, + 303.38812862, 489.4671608063, 311.5101016), + CubicToCommand(487.7015145063, 319.63207458, 480.2858000463, 320.3383331, + 480.2858000463, 320.3383331), + CubicToCommand(477.4607659663, 333.40411572, 475.34022476, + 328.10717681999995, 475.34022476, 328.10717681999995), + CubicToCommand(474.9870955, 318.21955754, 464.39321770000004, + 328.46030608, 464.39321770000004, 328.46030608), + CubicToCommand(462.27444214, 331.99159868, 454.15246916, + 328.10717681999995, 454.15246916, 328.10717681999995), + CubicToCommand(442.14607432, 324.57588422, 446.38362544, 321.04459162, + 446.38362544, 321.04459162), + CubicToCommand(449.56178878, 317.16016976, 469.33702733999996, + 321.04459162, 469.33702733999996, 321.04459162), + CubicToCommand(473.2214492, 318.21955754, 459.0962788, 311.15697234, + 459.0962788, 311.15697234), + CubicToCommand(458.03689102, 307.97880899999996, 459.80253732, + 300.20996528, 459.80253732, 300.20996528), + CubicToCommand(461.92131288, 294.55989711999996, 473.92770772, + 284.67227784, 473.92770772, 284.67227784), + CubicToCommand(490.5265485863, 282.55350228, 485.58273894629997, + 279.72846819999995, 485.58273894629997, 279.72846819999995), + CubicToCommand(474.6357318863, 270.54710744, 464.39321770000004, + 283.96601932, 464.39321770000004, 283.96601932), + CubicToCommand(460.50879584, 294.91302637999996, 429.78655022, + 321.39772087999995, 429.78655022, 321.39772087999995), + CubicToCommand(421.31144798, 327.40091829999994, 425.90212836, + 315.39452345999996, 418.83954316, 321.39772087999995), + CubicToCommand(411.77695796, 327.40091829999994, 375.40464418, + 311.5101016, 375.40464418, 311.5101016), + CubicToCommand(354.9902416594, 309.4036855641, 350.16649596779996, + 337.19848961869997, 343.9355301751, 331.6896731627), + CubicToCommand(343.9355301751, 331.6896731627, 353.5018018285, + 346.7329796387, 350.67676774849997, 336.84536035869996), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(694.63349522, 43.13186400000001), + CubicToCommand(694.63349522, 43.13186400000001, 649.4329499400001, + 57.25703440000001, 644.4891402999999, 90.45118484), + CubicToCommand(644.4891402999999, 90.45118484, 640.2515891799999, + 130.70792047999998, 676.2707737, 161.78329536), + CubicToCommand(676.2707737, 161.78329536, 676.97703222, 173.08343168, + 680.50832482, 178.73349984), + CubicToCommand(680.50832482, 178.73349984, 677.6832907400001, + 187.20860208, 710.87744118, 173.7896902), + LineToCommand(758.9030205399999, 158.95826128), + CubicToCommand(758.9030205399999, 158.95826128, 770.20315686, + 154.72071016, 779.38451762, 139.18302272), + CubicToCommand(788.56587838, 123.64533528000001, 815.40370214, + 90.45118484000002, 809.04737546, 45.95689808000003), + CubicToCommand(809.04737546, 45.95689808000003, 811.1661510199999, + 26.18165952000001, 800.5722732199999, 25.475401000000005), + CubicToCommand(800.5722732199999, 25.475401000000005, 785.7408442999999, + 22.65036692000001, 773.02819094, 36.069278800000006), + CubicToCommand(773.02819094, 36.069278800000006, 761.0217960999998, + 41.719346960000024, 756.78424498, 41.01308843999999), + LineToCommand(694.63349522, 43.13186400000001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(791.0730961259999, 41.383874163), + CubicToCommand(791.0730961259999, 41.383874163, 794.780953356, + 25.616652704000018, 786.235225264, 34.16238079600001), + CubicToCommand(786.235225264, 34.16238079600001, 773.8050753119999, + 44.261877631999994, 760.5980409879999, 44.261877631999994), + CubicToCommand(760.5980409879999, 44.261877631999994, 734.9608567119999, + 48.146299492000026, 727.192012992, 71.45283065200002), + CubicToCommand(727.192012992, 71.45283065200002, 720.2000536439999, + 118.84277734400001, 734.18397234, 128.94227418000003), + CubicToCommand(734.18397234, 128.94227418000003, 742.729700432, + 142.14930850400003, 755.1598503839999, 130.496042924), + CubicToCommand(767.590000336, 118.84277734399998, 794.9575179859999, + 65.467289695, 791.0730961259999, 41.383874163), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(790.4198069949999, 42.019506831), + CubicToCommand(790.4198069949999, 42.019506831, 794.1100077619999, + 26.570101706000003, 785.7231878369998, 34.97457809400001), + CubicToCommand(785.7231878369998, 34.97457809400001, 773.5049154409999, + 44.87985383700001, 760.5450715989999, 44.87985383700001), + CubicToCommand(760.5450715989999, 44.87985383700001, 735.366955361, + 48.69364984500001, 727.7393633449999, 71.57642589300002), + CubicToCommand(727.7393633449999, 71.57642589300002, 720.870999238, + 118.10826848320002, 734.6077274519998, 128.02413810400003), + CubicToCommand(734.6077274519998, 128.02413810400003, 742.9945473769999, + 140.99104453120003, 755.1951633099999, 129.54965650720004), + CubicToCommand(767.4134357059999, 118.10826848320002, 794.2336030029999, + 65.66151078800004, 790.4198069949999, 42.01950683100003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(789.7488614009999, 42.655139499), + CubicToCommand(789.7488614009999, 42.655139499, 793.4214057049999, + 27.541207171000025, 785.193493947, 35.769118929), + CubicToCommand(785.193493947, 35.769118929, 773.222412033, + 45.497830042000004, 760.49210221, 45.497830042000004), + CubicToCommand(760.49210221, 45.497830042000004, 735.7730540099999, + 49.24100019800002, 728.2867136979999, 71.70002113400002), + CubicToCommand(728.2867136979999, 71.70002113400002, 721.559601295, + 117.3737596224, 735.0314825639999, 127.106002028), + CubicToCommand(735.0314825639999, 127.106002028, 743.2593943219999, + 139.8327805584, 755.2481326989998, 128.6032700904), + CubicToCommand(767.2192146129998, 117.3737596224, 793.492031557, + 65.85573188100003, 789.7488614009999, 42.655139499), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(789.0955722699999, 43.27311570399999), + CubicToCommand(789.0955722699999, 43.27311570399999, 792.7504601109999, + 28.49465617300001, 784.6638000569999, 36.581316227), + CubicToCommand(784.6638000569999, 36.581316227, 772.922252162, + 46.133462709999975, 760.421476358, 46.133462709999975), + CubicToCommand(760.421476358, 46.133462709999975, 736.196809122, + 49.80600701399999, 728.8517205139999, 71.84127283799998), + CubicToCommand(728.8517205139999, 71.84127283799998, 722.2305468889999, + 116.63925076159998, 735.455237676, 126.18786595199998), + CubicToCommand(735.455237676, 126.18786595199998, 743.5418977300001, + 138.6745165856, 755.283445625, 127.65688367359998), + CubicToCommand(767.042649983, 116.63925076159998, 792.768116574, + 66.04995297399998, 789.0955722699999, 43.27311570399999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(788.442283139, 43.90874837199999), + CubicToCommand(788.442283139, 43.90874837199999, 792.079514517, + 29.465761638000004, 784.1517626299999, 37.375857061999994), + CubicToCommand(784.1517626299999, 37.375857061999994, 772.622092291, + 46.751438914999994, 760.3685069689999, 46.751438914999994), + CubicToCommand(760.3685069689999, 46.751438914999994, 736.6029077709999, + 50.353357367, 729.3990708669999, 71.96486807900001), + CubicToCommand(729.3990708669999, 71.96486807900001, 722.919148946, + 115.90474190079999, 735.8789927879999, 125.26972987599999), + CubicToCommand(735.8789927879999, 125.26972987599999, 743.8067446749999, + 137.51625261279997, 755.336415014, 126.71049725680001), + CubicToCommand(766.8484288899999, 115.90474190079999, 792.044201591, + 66.24417406700002, 788.442283139, 43.90874837199999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(787.7713375449999, 44.54438103999999), + CubicToCommand(787.7713375449999, 44.54438103999999, 791.39091246, + 30.41921063999999, 783.6220687399999, 38.188054360000024), + CubicToCommand(783.6220687399999, 38.188054360000024, 772.3219324199999, + 47.36941512000001, 760.31553758, 47.36941512000001), + CubicToCommand(760.31553758, 47.36941512000001, 737.0090064199999, + 50.900707720000014, 729.9464212199999, 72.08846332000002), + CubicToCommand(729.9464212199999, 72.08846332000002, 723.5900945399999, + 115.17023304000003, 736.3027479, 124.35159380000002), + CubicToCommand(736.3027479, 124.35159380000002, 744.0715916199999, + 136.35798864, 755.37172794, 125.76411084), + CubicToCommand(766.6718642599999, 115.17023304, 791.302630145, + 66.43839516, 787.7713375449999, 44.54438103999999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(414.24886277999997, 403.3237092), + CubicToCommand(414.24886277999997, 403.3237092, 378.22967825999996, + 369.42330024, 364.10450786, 368.0107832), + CubicToCommand(364.10450786, 368.0107832, 303.36627513999997, 360.948198, + 277.2347099, 392.72983139999997), + CubicToCommand(277.2347099, 392.72983139999997, 308.31008477999995, + 356.71064688, 357.04192265999995, 366.59826616), + CubicToCommand(357.04192265999995, 366.59826616, 318.90396258, + 358.82942244000003, 297.00994846, 364.4794906), + LineToCommand(250.39688614, 389.1985388), + LineToCommand(245.4530765, 397.67364104), + CubicToCommand(245.4530765, 397.67364104, 252.5156617, 371.5420758, + 285.00355362, 360.948198), + CubicToCommand(285.00355362, 360.948198, 325.26028926, 352.47309576, + 344.32926929999996, 360.948198), + CubicToCommand(344.32926929999996, 360.948198, 306.19130922, 348.94180316, + 288.53484621999996, 352.47309576), + CubicToCommand(288.53484621999996, 352.47309576, 234.85919869999998, + 348.23554464, 212.25892605999996, 394.84860696), + CubicToCommand(212.25892605999996, 394.84860696, 219.32151125999997, + 369.42330024, 245.45307649999998, 356.71064688), + CubicToCommand(245.45307649999998, 356.71064688, 269.46586618, + 341.17295944, 305.4850507, 346.11676908000004), + CubicToCommand(305.4850507, 346.11676908000004, 330.91035741999997, + 351.76683724000003, 340.09171818, 356.00438836), + CubicToCommand(349.27307894, 360.24193948000004, 347.15430338, + 355.29812984, 332.32287446, 346.8230276), + CubicToCommand(332.32287446, 346.8230276, 322.43525517999996, 329.1665646, + 297.71620698, 329.87282312), + CubicToCommand(297.71620698, 329.87282312, 222.14654534, 336.2291498, + 203.78382381999995, 357.4169054), + CubicToCommand(203.78382381999995, 357.4169054, 227.79661349999998, + 337.64166683999997, 246.15933501999996, 332.6978572), + CubicToCommand(246.15933501999996, 332.6978572, 285.70981213999994, + 318.5726868, 300.54124105999995, 319.98520384), + CubicToCommand(300.54124105999995, 319.98520384, 344.32926929999996, + 321.75085014, 357.74818117999996, 314.68826494), + CubicToCommand(357.74818117999996, 314.68826494, 337.97294261999997, + 323.51649643999997, 343.62301077999996, 329.1665646), + CubicToCommand(349.27307893999995, 334.81663276, 361.27947378, + 348.23554464, 361.27947378, 350.3543202), + CubicToCommand(361.27947378, 352.47309576, 404.00811423999994, + 391.49387899, 410.36444092, 399.26272271000005), + LineToCommand(414.24886277999997, 403.3237092), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(658.6143107, 745.8590914), + CubicToCommand(658.6143107, 745.8590914, 631.24679305, 681.41300145, + 609.1762143, 664.6393616), + CubicToCommand(609.1762143, 664.6393616, 655.0830181, 692.8897024, + 661.26278015, 724.6713358), + CubicToCommand(661.26278015, 724.6713358, 661.26278015, 742.3277988, + 658.6143107, 745.8590914), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(741.5996868, 759.10143865), + CubicToCommand(741.5996868, 759.10143865, 694.81005985, 661.99089215, + 662.1456033, 619.61538095), + CubicToCommand(662.1456033, 619.61538095, 738.95121735, 685.8271172, + 747.77944885, 732.61674415), + LineToCommand(748.662272, 742.3277988), + LineToCommand(743.3653331, 737.91368305), + CubicToCommand(743.3653331, 737.91368305, 742.4825099499999, 753.80449975, + 741.5996868, 759.10143865), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(841.35870275, 673.4675931), + CubicToCommand(841.35870275, 673.4675931, 731.005809, 568.41163825, + 728.35733955, 563.9975225000001), + CubicToCommand(728.35733955, 563.9975225000001, 835.1789407, 680.5301783, + 840.4758796, 693.77252555), + CubicToCommand(840.4758796, 693.77252555, 836.944587, 677.88170885, + 841.35870275, 673.4675931), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(508.5343752, 750.27320715), + CubicToCommand(508.5343752, 750.27320715, 542.96447805, 658.45959955, + 576.51175775, 698.1866413), + CubicToCommand(576.51175775, 698.1866413, 602.99645225, 715.8431043, + 602.1136291, 721.1400432), + CubicToCommand(602.1136291, 721.1400432, 595.0510439, 709.66334225, + 563.2694104999999, 710.5461654), + CubicToCommand(563.2694104999999, 710.5461654, 529.7221308, 705.2492265, + 508.5343752, 750.27320715), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(844.8899953499999, 525.1533039), + CubicToCommand(844.8899953499999, 525.1533039, 765.4359118499999, + 474.83238435, 752.1935646, 472.1839149), + CubicToCommand(731.341281797, 468.016989632, 839.59305645, 523.3876576, + 848.42128795, 541.92694375), + CubicToCommand(848.42128795, 541.92694375, 851.95258055, 537.512828, + 844.8899953499999, 525.1533039), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(578.80709794, 713.3711994800001), + CubicToCommand(578.80709794, 713.3711994800001, 614.82628246, + 709.8399068800001, 626.8326773, 697.8335120400001), + LineToCommand(634.6015210200001, 704.18983872), + LineToCommand(665.6768959, 636.3890208), + LineToCommand(672.03322258, 645.57038156), + CubicToCommand(672.03322258, 645.57038156, 697.4585293, 619.43881632, + 696.04601226, 605.31364592), + CubicToCommand(694.63349522, 591.18847552, 718.6462849, 615.90752372, + 718.6462849, 615.90752372), + CubicToCommand(718.6462849, 615.90752372, 717.23376786, 595.42602664, + 729.94642122, 607.4324214799999), + CubicToCommand(729.94642122, 607.4324214799999, 725.7088701, + 579.8883391999999, 740.54029902, 594.0135095999999), + CubicToCommand(740.54029902, 594.0135095999999, 721.9303870179999, + 540.761617192, 761.72805462, 586.24466588), + CubicToCommand(771.6156739, 597.5448021999999, 763.84683018, 585.53840736, + 763.84683018, 585.53840736), + CubicToCommand(763.84683018, 585.53840736, 717.94002638, + 500.78738495999994, 756.0779864599999, 526.2126916799999), + CubicToCommand(756.0779864599999, 526.2126916799999, 759.60927906, + 485.95595603999993, 757.4905034999999, 478.18711232), + CubicToCommand(755.37172794, 470.4182685999999, 751.84043534, + 430.86779147999994, 743.3653331, 421.68643072), + CubicToCommand(734.89023086, 412.5050699599999, 744.0715916199999, + 409.68003587999993, 753.9592109, 418.86139663999995), + CubicToCommand(753.9592109, 418.86139663999995, 734.18397234, + 376.48588543999995, 757.4905034999999, 397.67364103999995), + CubicToCommand(757.4905034999999, 397.67364103999995, 751.13417682, + 370.83581727999996, 743.3653331, 365.89200764), + CubicToCommand(743.3653331, 365.89200764, 733.47771382, + 335.52289127999995, 760.31553758, 354.59187131999994), + CubicToCommand(760.31553758, 354.59187131999994, 752.54669386, + 332.69785719999993, 746.8966257, 327.04778903999994), + CubicToCommand(746.8966257, 327.04778903999994, 726.4151286199999, + 278.31595115999994, 739.12778198, 286.79105339999995), + LineToCommand(746.8966257, 293.14738007999995), + CubicToCommand(746.8966257, 293.14738007999995, 734.89023086, + 268.42833188, 746.19036718, 276.1971755999999), + CubicToCommand(757.4905034999999, 283.96601931999993, 757.4905034999999, + 283.2597608, 757.4905034999999, 283.2597608), + CubicToCommand(757.4905034999999, 283.2597608, 720.05880194, + 224.64030363999996, 756.0779864599999, 255.71567851999995), + CubicToCommand(756.0779864599999, 255.71567851999995, 741.6703126519999, + 231.14141331659997, 735.59648938, 218.99023547999997), + CubicToCommand(735.59648938, 218.99023547999997, 702.4023389399999, + 182.97105095999996, 727.8276456599999, 194.27118727999994), + LineToCommand(736.3027479, 197.09622135999996), + CubicToCommand(736.3027479, 197.09622135999996, 720.76506046, + 179.43975835999993, 706.63989006, 176.61472427999996), + CubicToCommand(692.51471966, 173.78969019999994, 710.87744118, + 162.48955387999996, 722.1775775, 166.02084647999993), + CubicToCommand(733.47771382, 169.55213907999996, 761.0217961, + 182.97105095999996, 761.0217961, 182.97105095999996), + CubicToCommand(761.0217961, 182.97105095999996, 783.62206874, + 216.16520139999994, 790.68465394, 216.87145991999995), + CubicToCommand(790.68465394, 216.87145991999995, 755.37172794, + 203.45254803999995, 765.96560574, 217.57771843999996), + CubicToCommand(765.96560574, 217.57771843999996, 791.39091246, + 242.29676663999993, 778.6782591, 241.59050811999995), + CubicToCommand(778.6782591, 241.59050811999995, 768.0843812999999, + 254.30316147999994, 776.55948354, 269.8408489199999), + CubicToCommand(776.55948354, 269.8408489199999, 743.965652842, + 237.36884781669994, 770.20315686, 282.55350228), + LineToCommand(782.2095517, 311.5101015999999), + CubicToCommand(782.2095517, 311.5101015999999, 739.12778198, + 267.72207335999997, 758.9030205399999, 306.56629195999994), + CubicToCommand(758.9030205399999, 306.56629195999994, 789.2721369, + 348.23554463999994, 792.8034295, 348.94180315999995), + CubicToCommand(796.3347220999999, 349.64806167999996, 804.1035658199999, + 365.18574911999997, 804.1035658199999, 365.18574911999997), + LineToCommand(796.3347220999999, 361.65445651999994), + LineToCommand(805.51608286, 377.19214395999995), + CubicToCommand(805.51608286, 377.19214395999995, 785.7408442999999, + 356.00438835999995, 796.3347220999999, 379.31091951999997), + LineToCommand(806.22234138, 404.73622623999995), + CubicToCommand(806.22234138, 404.73622623999995, 770.20315686, + 365.89200764, 794.21594654, 418.15513811999995), + CubicToCommand(794.21594654, 418.15513811999995, 765.25934722, + 408.97377735999993, 780.79703466, 439.34289371999995), + CubicToCommand(780.79703466, 439.34289371999995, 777.97200058, + 467.59323451999995, 778.6782591, 476.77459527999997), + CubicToCommand(779.38451762, 485.95595603999993, 781.50329318, + 536.1003109599999, 773.73444946, 550.2254813599999), + CubicToCommand(765.9656057399999, 564.3506517599999, 784.3283272599999, + 598.2510607199999, 787.85961986, 605.3136459199999), + CubicToCommand(791.39091246, 612.3762311199999, 797.7472391399999, + 631.44521116, 782.2095517, 615.2012651999999), + CubicToCommand(766.6718642599999, 598.9573192399998, 774.44070798, + 608.8449385199999, 777.97200058, 624.3826259599999), + CubicToCommand(781.50329318, 639.9203133999998, 792.0971709800001, + 667.4643956799999, 790.68465394, 677.3520149599999), + CubicToCommand(790.68465394, 677.3520149599999, 788.56587838, + 679.4707905199998, 782.91581022, 673.1144638399999), + CubicToCommand(782.91581022, 673.1144638399999, 756.78424498, + 632.8577281999999, 759.6092790600001, 658.2830349199999), + CubicToCommand(759.6092790600001, 658.2830349199999, 757.4905035000002, + 672.4082053199999, 751.8404353400001, 687.9458927599999), + CubicToCommand(751.8404353400001, 687.9458927599999, 746.1903671800001, + 707.0148727999999, 746.1903671800001, 691.4771853599999), + CubicToCommand(746.1903671800001, 691.4771853599999, 740.54029902, + 661.8143275199999, 735.5964893800001, 675.2332393999999), + CubicToCommand(730.65267974, 688.65215128, 724.29635306, + 699.2460290799999, 719.3525434200001, 703.4835801999999), + CubicToCommand(714.4087337800001, 707.7211313199999, 705.2273730200001, + 667.4643956799999, 703.10859746, 685.8271171999999), + CubicToCommand(703.10859746, 685.8271171999999, 681.9208418600001, + 663.9331030799999, 673.44573962, 692.8897023999999), + LineToCommand(652.9642425400001, 721.8463017199998), + CubicToCommand(652.9642425400001, 721.8463017199998, 652.2579840200001, + 699.9522875999999, 650.1392084600002, 710.5461654), + CubicToCommand(650.1392084600002, 710.5461654, 597.1698194600001, + 721.1400431999999, 578.8070979400001, 713.37119948), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(518.06886522, 83.38859964), + CubicToCommand(518.06886522, 83.38859964, 497.58913378629995, 69.26342924, + 490.5265485863, 69.96968776), + CubicToCommand(483.46396338629995, 70.67594628000003, 539.25662082, + 54.432000320000014, 612.00124838, 103.16383820000001), + CubicToCommand(612.00124838, 103.16383820000001, 620.4763506200001, + 108.10764784, 626.8326773, 107.40138932000002), + CubicToCommand(626.8326773, 107.40138932000002, 632.4827454599999, + 111.63894044000003, 627.53893582, 117.99526712000002), + CubicToCommand(627.53893582, 117.99526712000002, 612.00124838, + 134.94547160000002, 631.77648694, 154.72071016), + CubicToCommand(631.77648694, 154.72071016, 664.2643788600001, 166.727105, + 654.37675958, 151.18941756), + CubicToCommand(654.37675958, 151.18941756, 673.44573962, 158.25200276, + 677.6832907400001, 165.31458796), + CubicToCommand(681.92084186, 172.37717316, 679.8020663, 165.31458796, + 679.8020663, 165.31458796), + LineToCommand(657.90805218, 143.42057384000003), + CubicToCommand(657.90805218, 143.42057384000003, 648.72669142, + 139.88928124000003, 643.78288178, 125.05785232000002), + CubicToCommand(638.83907214, 110.22642340000002, 634.6015210200001, + 92.56996040000001, 642.37036474, 86.91989224000002), + CubicToCommand(642.37036474, 86.91989224000002, 635.30777954, + 94.68873596000003, 636.72029658, 87.62615076000003), + CubicToCommand(638.13281362, 80.56356556000003, 644.4891403, + 74.20723888000003, 647.3141743799999, 73.50098036000003), + CubicToCommand(650.13920846, 72.79472184000002, 679.0958077800001, + 44.89751030000002, 691.1022026200001, 44.191251780000044), + CubicToCommand(691.1022026200001, 44.191251780000044, 674.85825666, + 46.663156600000065, 669.5613177600001, 44.89751030000005), + CubicToCommand(664.2643788600001, 43.131864000000064, 617.2981872800001, + 23.00349618000004, 606.7043094800001, 20.884720620000053), + CubicToCommand(606.7043094800001, 20.884720620000053, 577.0414516400001, + 9.231455040000071, 598.22920724, 12.762747640000072), + CubicToCommand(598.22920724, 12.762747640000072, 661.43934478, + 19.472203580000098, 693.57410744, 42.77873474000009), + CubicToCommand(693.57410744, 42.77873474000009, 680.86145408, + 27.947305820000054, 648.3735621600001, 15.587781720000066), + CubicToCommand(648.3735621600001, 15.587781720000066, 609.1762143000001, + -6.659361659999917, 547.02546454, 2.1688698400000703), + CubicToCommand(547.02546454, 2.1688698400000703, 515.5969604000001, + 7.81893800000006, 501.82491926000006, 10.997101340000086), + CubicToCommand(501.82491926000006, 10.997101340000086, 497.2360045263, + 9.937713560000077, 496.17661674630006, 9.231455040000071), + CubicToCommand(495.1172289663001, 8.525196520000065, 474.28083698000006, + -7.365620179999922, 425.54899910000006, 4.993903920000065), + CubicToCommand(425.54899910000006, 4.993903920000065, 395.5330120000001, + 13.115876900000046, 380.34845382000003, 21.590979140000087), + CubicToCommand(380.34845382000003, 21.590979140000087, 353.51063006000004, + 23.709754700000047, 347.15430338000004, 29.359822860000094), + CubicToCommand(347.15430338000004, 29.359822860000094, 314.31328220000006, + 55.13825884000008, 310.78198960000003, 56.55077588000009), + CubicToCommand(307.25069700000006, 57.96329292000013, 287.12232918000007, + 71.3822048000001, 285.70981214000005, 72.0884633200001), + CubicToCommand(285.70981214000005, 72.0884633200001, 329.14471112000007, + 60.43519774000009, 333.38226224000005, 56.197646620000086), + CubicToCommand(337.6198133600001, 51.96009550000008, 368.34205898000005, + 47.3694151200001, 372.5796101000001, 49.84131994000009), + CubicToCommand(376.81716122000006, 52.31322476000011, 391.64859014000007, + 51.2538369800001, 374.69838566000004, 52.31322476000011), + CubicToCommand(374.69838566000004, 52.31322476000011, 508.18124594000005, + 78.4447900000001, 509.59376298000006, 81.9760826000001), + CubicToCommand(511.0062800200001, 85.5073752000001, 518.06886522, + 83.38859964000011, 518.06886522, 83.38859964000011), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(644.13601104, 67.14465368), + CubicToCommand(644.13601104, 67.14465368, 626.12641878, 54.07887105999998, + 622.5951261800001, 54.07887105999998), + CubicToCommand(619.0638335799999, 54.07887105999998, 597.16981946, + 36.06927879999998, 589.754105, 36.775537319999984), + CubicToCommand(582.3383905400001, 37.48179583999999, 560.7975056800001, + 19.825332839999987, 512.4187970600001, 34.30363249999999), + CubicToCommand(512.4187970600001, 34.30363249999999, 511.35940928, + 30.77233989999999, 517.7157359600001, 29.35982285999998), + CubicToCommand(517.7157359600001, 29.35982285999998, 529.01587228, + 25.475401000000005, 529.7221308000001, 24.416013219999968), + CubicToCommand(529.7221308000001, 24.416013219999968, 565.38818606, + 17.000298759999993, 578.10083942, 23.356625439999988), + CubicToCommand(578.10083942, 23.356625439999988, 594.3447853800001, + 27.94730581999997, 605.2917924400001, 38.89431287999997), + CubicToCommand(605.2917924400001, 38.89431287999997, 625.067031, + 44.54438103999996, 630.7170991600001, 42.778734739999976), + CubicToCommand(630.7170991600001, 42.778734739999976, 646.2547866000001, + 46.66315659999998, 646.9610451200001, 49.84131993999998), + CubicToCommand(646.9610451200001, 49.84131993999998, 657.20179366, + 55.13825883999996, 654.02363032, 59.72893921999997), + CubicToCommand(654.02363032, 59.72893921999997, 654.7298888400001, + 62.55397329999997, 644.1360110400001, 67.14465367999998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(622.118401679, 63.419139986999994), + CubicToCommand(623.5485751819999, 64.531497156, 625.349534408, + 64.708061786, 626.408922188, 66.138235289), + CubicToCommand(626.8326773, 66.685585642, 626.320639873, + 67.26824892100001, 625.7556330570001, 67.44481355100001), + CubicToCommand(623.9193609050001, 67.992163904, 622.047775827, + 66.98574551299998, 620.034939045, 68.02747682999998), + CubicToCommand(619.328680525, 68.398262553, 618.198666893, + 68.08044621900001, 617.2099049650001, 67.815599274), + CubicToCommand(614.331901496, 67.03871490199998, 611.100768767, + 66.98574551299998, 608.1168265199999, 68.20404145999998), + CubicToCommand(604.620846846, 66.208861141, 600.4539215780001, + 67.25059245799997, 596.7813772740001, 65.46728969499998), + CubicToCommand(596.675438496, 65.43197676899999, 596.28699631, + 66.03229651099997, 596.145744606, 65.99698358499998), + CubicToCommand(590.778179854, 63.96649033999998, 584.157006229, + 64.46087130399997, 579.86648572, 60.43519773999998), + CubicToCommand(575.575965211, 59.71128275699999, 571.426696406, + 58.89908545899996, 567.1361758969999, 57.69844597499997), + CubicToCommand(563.922699631, 56.79796636199998, 561.433138348, + 55.04997652499998, 558.572791342, 53.584490095999996), + CubicToCommand(556.136199448, 52.330881223000006, 553.576012313, + 51.41274514699998, 550.856917011, 50.74179955299999), + CubicToCommand(547.572814893, 49.94725871799997, 544.341682164, + 50.14147981099998, 541.004610657, 49.223343734999986), + CubicToCommand(540.828046027, 49.188030809, 540.49257323, + 49.78835055099998, 540.351321526, 49.75303762499999), + CubicToCommand(539.7863147099999, 49.55881653199998, 539.25662082, + 48.53474167799999, 538.956460949, 48.623023992999975), + CubicToCommand(535.990175165, 49.54116006899997, 533.359362178, + 47.82848315799998, 530.42838932, 48.428802899999994), + CubicToCommand(528.344926686, 46.27471441399999, 525.30801505, + 46.69846952599997, 522.571263285, 45.921585153999985), + CubicToCommand(517.3272937739999, 44.42078579899999, 511.7655079289999, + 46.66315659999998, 506.41559964, 44.89751029999999), + CubicToCommand(513.6724059329999, 41.64872110799996, 521.9532870799999, + 43.820466056999976, 529.1218110579999, 40.16557821599997), + CubicToCommand(533.2357669369999, 38.08211558199994, 537.932386095, + 40.02432651199996, 542.4700970859999, 38.682435323999954), + CubicToCommand(543.335263773, 38.417588378999966, 544.55355972, + 38.06445911899996, 545.25981824, 39.24744213999995), + CubicToCommand(545.5070087219999, 39.00025165799994, 545.8248250559999, + 38.59415300899994, 545.9307638339999, 38.629465934999956), + CubicToCommand(550.238940806, 40.67761564299997, 554.3352402219999, + 42.91998644399996, 558.7140430459999, 44.80922798499995), + CubicToCommand(559.3143627879999, 45.074074929999966, 560.26781179, + 44.65031981799996, 560.709223365, 45.02110554099997), + CubicToCommand(563.393005741, 47.157537563999966, 566.818359563, + 46.980972933999965, 569.2726079199999, 49.13506141999997), + CubicToCommand(572.27420663, 48.252238269999964, 575.434713507, + 48.92318386399995, 578.489281606, 47.810826694999975), + CubicToCommand(578.6305333099999, 47.775513768999986, 579.0366319589999, + 48.37583351099994, 579.089601348, 48.34052058499995), + CubicToCommand(581.1024381299999, 47.016285859999954, 583.132931375, + 47.49301036099996, 584.704356582, 48.02270425099994), + CubicToCommand(585.304676324, 48.234581806999955, 586.470002882, + 48.675993381999945, 587.017353235, 48.79958862299995), + CubicToCommand(588.994877091, 49.27631312399993, 590.5133329089999, + 50.123823347999945, 592.5967955429999, 50.45929614499994), + CubicToCommand(592.791016636, 50.49460907099993, 593.126489433, + 49.894289328999946, 593.2500846739999, 49.929602254999935), + CubicToCommand(595.22760853, 50.70648662699995, 597.063880682, + 50.61820431199996, 598.2292072399999, 52.66635401999994), + CubicToCommand(598.476397722, 52.419163537999935, 598.7589011299999, + 52.013064888999935, 598.900152834, 52.04837781499995), + CubicToCommand(600.7187685229999, 52.64869755699996, 601.866438618, + 53.99058874499994, 603.808649548, 54.41434385699995), + CubicToCommand(604.6561597719999, 54.59090848699995, 605.7508604779999, + 55.70326565599996, 606.757278869, 56.02108198999994), + CubicToCommand(610.9771735259999, 57.31000378899995, 614.243619181, + 60.01144262799997, 618.1103845779999, 61.58286783499997), + CubicToCommand(619.452275766, 62.13021818799996, 621.0060445099999, + 62.55397329999997, 622.1184016789999, 63.419139986999966), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(486.80986312479996, 38.29399313800002), + CubicToCommand(482.36396574139997, 35.257081502000005, 478.18291530299996, + 33.29721410900001, 473.8712070384, 30.13670723200002), + CubicToCommand(473.55162505809994, 29.90717321300002, 472.9159923901, + 30.207333084000027, 472.55933183749994, 29.995455528000008), + CubicToCommand(470.78132601339996, 28.91841128499999, 469.213432099, + 27.894336431000028, 467.4795674324, 26.658384021000046), + CubicToCommand(466.5278840767, 25.98743842700003, 465.0782884644, + 26.005094890000038, 464.181340144, 25.54602685200004), + CubicToCommand(459.6895359568, 23.268343125000058, 455.04765183409995, + 22.279581197000056, 450.62117656, 20.178462100000047), + CubicToCommand(451.8253473366, 19.04844846800009, 453.8064024852, + 19.48986004300008, 454.85872768, 18.059686540000087), + CubicToCommand(455.2030287085, 18.554067504000074, 455.62325252790004, + 19.04844846800009, 456.2465256718, 18.712975671000095), + CubicToCommand(459.2092801632, 17.12389400100008, 462.47572581820003, + 16.859047056000065, 465.434949017, 17.017955223000058), + CubicToCommand(468.4436103122, 17.17686339000008, 471.4805219482, + 17.706557280000055, 474.6145441307, 18.200938244000042), + CubicToCommand(475.1565975448, 18.27156409600002, 475.5079611585, + 19.189700172000045, 476.0782649134, 19.366264802000046), + CubicToCommand(480.0121248698, 20.53159136000002, 484.23025388049996, + 19.613455284000025, 487.9716583902, 21.096598176000015), + CubicToCommand(490.7808016535, 22.20895534500002, 493.5528663445, + 23.656785310999993, 495.7405021102, 25.899156112000014), + CubicToCommand(496.1854449778, 26.358224150000012, 495.6116099303, + 26.905574502999997, 495.1172289663, 27.24104730000002), + CubicToCommand(495.80229973070004, 27.04682620699998, 496.28432117060004, + 27.417611929999993, 496.48030790990003, 27.964962283000006), + CubicToCommand(496.62862219910005, 28.388717395000015, 496.62862219910005, + 28.91841128499999, 496.48030790990003, 29.342166397), + CubicToCommand(496.28255552430005, 29.889516750000013, 495.7899402066, + 30.066081379999986, 495.1295884904, 30.154363695), + CubicToCommand(492.6453241463, 30.489836491999995, 495.77404938990003, + 28.053244597999964, 494.53809697990005, 28.847785433000013), + CubicToCommand(492.29042924000004, 30.295615398999985, 493.60760137980003, + 32.767520219000005, 492.2921948863, 35.00989102), + CubicToCommand(491.79781392230007, 34.674418223, 491.3917152733, + 34.28597603699998, 491.5859363663, 33.597373979999986), + CubicToCommand(491.9990976005, 34.51551005599998, 490.93617852790004, + 35.027547483000006, 490.6395499495, 35.592554299), + CubicToCommand(489.959776124, 36.863819635, 488.37246010030003, + 39.37103738099998, 486.8098631248, 38.29399313799999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(429.42988966739995, 51.271493443), + CubicToCommand(423.86104123719997, 49.876632865999994, 418.47582002219997, + 50.070853959000004, 413.15063078139997, 47.810826695), + CubicToCommand(413.03233247929995, 47.775513769000014, 412.6385933544, + 48.375833510999996, 412.5255919912, 48.34052058500001), + CubicToCommand(410.11371914539995, 47.28113280500003, 408.492855842, + 45.58611235700002, 406.59302042319996, 43.73218374200002), + CubicToCommand(404.9809853513, 42.160758535000014, 402.0535437859, + 42.84936059200001, 399.8041103997, 41.98419390500001), + CubicToCommand(399.2320409985, 41.77231634900002, 398.8736147996, + 40.871836736000034, 398.34215526329996, 40.80121088400003), + CubicToCommand(396.18983242359997, 40.51870747600003, 394.5530783035, + 38.84134349100003, 392.70797791999996, 37.83492509999999), + CubicToCommand(396.83252767679994, 36.42240806000001, 401.08950090609994, + 36.49303391199999, 405.43652209669995, 35.80443185499999), + CubicToCommand(405.6360401286, 35.769118929, 405.89205884209997, + 36.351782208, 406.12688979999996, 36.351782208), + CubicToCommand(406.36701769679996, 36.351782208, 406.59655171579993, + 35.945683559, 406.83314831999996, 35.71614954), + CubicToCommand(407.1774493485, 36.21053050399999, 407.7106745310999, + 36.79319378299999, 408.16444563019996, 36.334125744999994), + CubicToCommand(409.1320198026, 35.38067674299998, 410.11371914539995, + 35.71614954, 411.06716814739997, 35.78677539199998), + CubicToCommand(411.3214212145999, 35.80443185499999, 411.54212700209996, + 36.351782207999975, 411.77695795999995, 36.351782207999975), + CubicToCommand(412.01708585679995, 36.351782207999975, 412.24838552209997, + 35.78677539199998, 412.48321647999995, 35.78677539199998), + CubicToCommand(412.72334437679996, 35.78677539199998, 412.95464404209997, + 36.351782207999975, 413.18947499999996, 36.351782207999975), + CubicToCommand(413.42960289679996, 36.351782207999975, 413.65913691579993, + 35.945683558999974, 413.89573351999996, 35.716149539999975), + CubicToCommand(415.11756075959994, 37.09335365399997, 416.68015773509995, + 36.122248188999976, 418.13151899369996, 36.44006452299996), + CubicToCommand(419.9642598531, 36.82850670899998, 420.43568741519994, + 38.858999953999984, 422.33199154139993, 39.38869384399996), + CubicToCommand(430.65701384589994, 41.68403403399998, 437.96149258899993, + 45.48017357899997, 445.6650073958999, 49.17037434599999), + CubicToCommand(446.20706081, 49.417564827999996, 446.57784653299996, + 49.858976402999986, 446.38362543999995, 50.54757845999998), + CubicToCommand(446.8550530020999, 50.54757845999998, 447.40770029399994, + 50.38867029299999, 447.74317309099996, 50.61820431199999), + CubicToCommand(449.61122687639994, 51.92478257399998, 451.4492646747, + 52.87823157599999, 452.67992014579994, 54.82044250599998), + CubicToCommand(453.06129974659996, 55.42076224799999, 452.48040211389997, + 56.144677230999974, 452.06900652599995, 56.05639491599999), + CubicToCommand(444.24895906329994, 54.290748616, 437.17577998549996, + 53.213704372999985, 429.42988966739995, 51.271493443), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(404.9580319494, 129.3324820123), + CubicToCommand(402.14712303979996, 127.18015917260001, 401.11598560059997, + 123.5941315373, 399.12433657419996, 120.43009336770001), + CubicToCommand(398.746488266, 119.8297736257, 399.2302753522, + 119.27536068750001, 399.7899852293, 119.1182181668), + CubicToCommand(400.7787471573, 118.8374804051, 401.7374930982, + 119.68145933650001, 402.4596424349, 120.03811988910002), + CubicToCommand(405.54069522839995, 121.5601069997, 408.2509622989, + 123.75303970430002, 411.77695796, 123.99846454000001), + CubicToCommand(415.290594097, 127.94291837419999, 422.812247335, + 128.6226921997, 422.82460685909996, 134.59234234000002), + CubicToCommand(422.82637250539995, 136.10903251169998, 420.30502958899996, + 134.4881692083, 419.54580167999995, 136.00485938), + CubicToCommand(415.2182025987, 134.2339161411, 411.00007358799996, + 134.41577771, 406.797835394, 131.8255745879), + CubicToCommand(405.7084316269, 131.15286334759998, 406.29109490589997, + 130.3530255737, 404.9580319494, 129.33248201229998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(356.33566413999995, 36.49303391199999), + CubicToCommand(356.57402639049997, 36.49303391199999, 368.9882855258, + 36.916789023999996, 368.9582695387, 37.11101011699998), + CubicToCommand(368.87881545519997, 37.65836046999999, 355.2303695562, + 39.51228908499999, 354.587674303, 39.21212921399999), + CubicToCommand(354.2981083098, 39.070877509999974, 341.03457330419997, + 43.36139801899998, 340.7979767, 43.13186399999998), + CubicToCommand(341.2711699084, 42.88467351799997, 355.8660022242, + 36.49303391199996, 356.33566414, 36.49303391199996), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(383.52661716, 53.72574180000001), + CubicToCommand(383.52661716, 53.72574180000001, 357.39505191999996, + 56.903905140000006, 349.6262082, 59.022680699999995), + CubicToCommand(341.85736448, 61.14145626000001, 309.01634329999996, + 74.56036814000001, 303.7194044, 78.09166074000001), + CubicToCommand(303.7194044, 78.09166074000001, 280.05974398, 87.62615076, + 250.04375688, 122.93907675999998), + CubicToCommand(250.04375688, 122.93907675999998, 263.46266876, + 116.93587933999999, 267.34709062, 111.99206969999997), + CubicToCommand(267.34709062, 111.99206969999997, 291.3598803, + 89.74492631999999, 291.00675104, 94.33560669999997), + CubicToCommand(291.00675104, 94.33560669999997, 312.5476359, + 79.15104851999999, 311.48824812, 83.03547037999996), + CubicToCommand(311.48824812, 83.03547037999996, 354.57001784, + 63.26023181999997, 351.03872523999996, 68.91029997999996), + CubicToCommand(351.03872523999996, 68.91029997999996, 389.17668532, + 60.78832699999998, 387.41103902, 64.31961959999998), + CubicToCommand(387.41103902, 64.31961959999998, 420.60518945999996, + 72.08846331999999, 415.66137982, 72.44159257999996), + CubicToCommand(415.66137982, 72.44159257999996, 405.42063128, + 74.56036813999998, 416.7207676, 80.91669481999998), + CubicToCommand(416.7207676, 80.91669481999998, 410.71757018, + 88.68553853999998, 401.18308016, 81.62295333999998), + CubicToCommand(391.64859013999995, 74.56036813999998, 396.94552904, + 78.44478999999998, 388.11729754, 80.21043629999997), + CubicToCommand(388.11729754, 80.21043629999997, 383.52661716, + 81.62295333999998, 375.40464418, 74.56036813999998), + CubicToCommand(375.40464418, 74.56036813999998, 365.51702489999997, + 66.43839516, 349.97933746, 72.79472183999997), + CubicToCommand(349.97933746, 72.79472183999997, 295.95056067999997, + 95.04186521999998, 292.41926808, 96.10125299999999), + CubicToCommand(292.41926808, 96.10125299999999, 286.0629414, + 101.04506263999997, 281.82539027999997, 107.40138931999999), + CubicToCommand(281.82539027999997, 107.40138931999999, 271.58464174, + 115.17023304, 266.28770283999995, 117.64213785999999), + CubicToCommand(266.28770283999995, 117.64213785999999, 243.6874302, + 138.12363494, 241.56865463999998, 140.59553975999998), + CubicToCommand(241.56865463999998, 140.59553975999998, 235.56545721999998, + 149.77690051999997, 234.15294017999997, 150.48315903999998), + CubicToCommand(234.15294017999997, 150.48315903999998, 245.45307649999995, + 143.77370309999998, 248.98436909999998, 140.24241049999998), + CubicToCommand(248.98436909999998, 140.24241049999998, 273.70341729999996, + 122.58594749999997, 283.23790732, 121.17343045999999), + CubicToCommand(283.23790732, 121.17343045999999, 291.00675104, + 115.87649155999998, 292.41926807999994, 113.40458673999998), + CubicToCommand(292.41926807999994, 113.40458673999998, 317.8445748, + 97.16064077999997, 325.26028926, 97.16064077999997), + CubicToCommand(325.26028926, 97.16064077999997, 341.50423521999994, + 106.34200153999998, 345.74178634, 93.98247743999997), + CubicToCommand(345.74178634, 93.98247743999997, 355.98253487999995, + 90.80431409999997, 365.87015415999997, 92.92308965999999), + CubicToCommand(365.87015415999997, 92.92308965999999, 371.52022231999996, + 88.33240928000001, 370.10770527999995, 84.44798742), + CubicToCommand(370.10770527999995, 84.44798742, 372.93273935999997, + 81.26982408, 374.69838566, 87.97928002), + CubicToCommand(374.69838566, 87.97928002, 380.70158308, 94.33560669999997, + 389.17668531999993, 90.80431409999997), + CubicToCommand(389.17668531999993, 90.80431409999997, 396.23927052, + 90.45118483999997, 392.70797791999996, 94.68873595999997), + CubicToCommand(392.70797791999996, 94.68873595999997, 384.93913419999996, + 101.39819189999997, 364.10450785999996, 101.75132115999997), + CubicToCommand(364.10450785999996, 101.75132115999997, 342.21049373999995, + 102.81070893999998, 313.25389441999994, 116.22962081999998), + CubicToCommand(313.25389441999994, 116.22962081999998, 260.63763467999996, + 134.59234234000002, 244.39368871999994, 152.95506386), + CubicToCommand(244.39368871999994, 152.95506386, 233.09355239999996, + 168.49275129999998, 223.55906237999997, 170.61152685999997), + CubicToCommand(223.55906237999997, 170.61152685999997, 213.31831383999997, + 172.02404389999998, 202.72443603999997, 185.08982651999997), + CubicToCommand(202.72443603999997, 185.08982651999997, 220.02776977999997, + 174.84907798, 235.91858647999996, 174.84907798), + CubicToCommand(235.91858647999996, 174.84907798, 242.98117167999996, + 170.61152686, 236.27171574, 176.96785354000002), + CubicToCommand(236.27171574, 176.96785354000002, 229.91538905999997, + 190.38676542000002, 232.74042313999996, 199.92125544), + CubicToCommand(232.74042313999996, 199.92125544, 231.68103535999995, + 209.1026162, 230.26851831999997, 211.92765028), + CubicToCommand(230.26851831999997, 211.92765028, 216.49647718, + 234.52792292, 216.49647718, 238.76547404000002), + CubicToCommand(216.49647718, 243.00302516, 218.61525274, 260.3063589, + 219.32151125999997, 261.36574668000003), + CubicToCommand(220.02776977999997, 262.42513446, 217.55586495999998, + 258.5407126, 224.26532089999998, 262.77826372), + CubicToCommand(230.97477683999998, 267.01581484, 235.91858648, + 269.84084892, 237.33110351999997, 274.78465856), + CubicToCommand(238.74362055999995, 279.7284682, 233.79981091999997, + 265.25016854, 233.44668165999997, 262.0720052), + CubicToCommand(233.09355239999996, 258.89384186, 225.67783793999996, + 246.1811885, 227.09035497999997, 241.94363738), + CubicToCommand(227.09035497999997, 241.94363738, 228.85600128, + 243.70928368, 230.26851831999994, 246.1811885), + CubicToCommand(230.26851831999994, 246.1811885, 229.20913053999996, + 245.12180072, 230.26851831999994, 238.76547404000002), + CubicToCommand(230.26851831999994, 238.76547404000002, 231.68103535999995, + 229.58411328, 234.15294017999997, 223.93404512), + CubicToCommand(236.624845, 218.28397696000002, 240.15613759999997, + 211.57452102000002, 240.86239611999997, 210.16200398), + CubicToCommand(241.56865463999998, 208.74948694, 241.56865463999998, + 198.5087384, 244.04055945999994, 203.09941878), + LineToCommand(250.04375687999993, 207.69009916), + CubicToCommand(250.04375687999993, 207.69009916, 245.09994723999995, + 203.09941878, 248.98436909999995, 199.21499692), + CubicToCommand(248.98436909999995, 199.21499692, 247.21872279999997, + 189.32737764, 250.39688613999994, 184.73669726000003), + CubicToCommand(250.39688613999994, 184.73669726000003, 262.7564102399999, + 169.90526834000002, 265.58144431999995, 168.13962204), + CubicToCommand(268.40647839999997, 166.37397574000002, 265.93457357999995, + 167.08023426, 265.93457357999995, 167.08023426), + CubicToCommand(265.93457357999995, 167.08023426, 276.52845138, + 159.6645198, 266.28770283999995, 162.48955388000002), + CubicToCommand(266.28770283999995, 162.48955388000002, 259.22511763999995, + 165.31458796, 253.92817873999996, 165.31458796), + CubicToCommand(253.92817873999996, 165.31458796, 240.50926685999997, + 168.84588056, 247.57185205999997, 161.4301661), + CubicToCommand(254.63443725999997, 154.01445164, 272.29090025999994, + 144.47996161999998, 279.00035619999994, 144.83309088000001), + LineToCommand(280.41287323999995, 147.65812496), + LineToCommand(300.18811179999994, 143.42057384000003), + LineToCommand(298.06933624, 144.83309088000001), + CubicToCommand(298.06933624, 144.83309088000001, 297.71620698, + 144.47996161999998, 305.13192144, 143.7737031), + CubicToCommand(312.54763589999993, 143.06744458000003, 322.78838443999996, + 145.5393494, 325.26028926, 142.36118606000002), + CubicToCommand(327.73219407999994, 139.18302272, 333.73539149999993, + 137.41737642, 333.02913298, 139.88928124), + CubicToCommand(332.32287446, 142.36118605999997, 331.9697452, + 145.89247866, 331.9697452, 145.89247866), + CubicToCommand(331.9697452, 145.89247866, 340.79797669999994, + 135.65173012, 339.73858892, 139.53615198), + CubicToCommand(338.67920114, 143.42057384, 324.20090147999997, + 152.6019346, 321.72899665999995, 163.54894166), + LineToCommand(340.09171818, 149.07064200000002), + LineToCommand(346.44804486, 143.7737031), + CubicToCommand(346.44804486, 143.7737031, 352.80437154, 147.65812496, + 353.1575008, 144.83309088000001), + CubicToCommand(353.51063006, 142.00805680000002, 361.63260303999994, + 131.76730826, 363.75137859999995, 132.12043752), + CubicToCommand(365.87015415999997, 132.47356678000003, 369.40144675999994, + 127.52975714000003, 369.04831749999994, 132.12043752), + CubicToCommand(368.69518824, 136.7111179, 382.11410012, 146.24560792, + 382.11410012, 146.24560792), + CubicToCommand(382.11410012, 146.24560792, 387.76416828, + 143.06744458000003, 390.2360731, 145.53934940000002), + CubicToCommand(392.70797791999996, 148.01125422, 400.12369237999997, + 110.57955266000002, 400.12369237999997, 110.57955266000002), + LineToCommand(444.26484988, 91.86370188000001), + LineToCommand(521.24702856, 85.86050446000002), + LineToCommand(491.23280710629996, 73.85410962), + LineToCommand(383.52661716, 53.72574180000001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(415.66137982, 405.0893555), + CubicToCommand(415.66137982, 405.0893555, 389.17668532, 375.42649766, + 374.3452564, 370.83581728), + CubicToCommand(374.3452564, 370.83581728, 350.68559597999996, + 358.82942244000003, 307.250697, 372.60146358) + ], + ), + Path( + commands: const [ + MoveToCommand(368.69518824, 368.36391246), + CubicToCommand(368.69518824, 368.36391246, 324.20090147999997, + 354.23874206, 297.00994846, 361.65445652), + CubicToCommand(297.00994846, 361.65445652, 264.52205654, 365.18574912, + 249.69062762, 389.55166806) + ], + ), + Path( + commands: const [ + MoveToCommand(362.33886156, 366.24513690000003), + CubicToCommand(362.33886156, 366.24513690000003, 332.32287446, + 353.53248354, 306.19130922, 349.64806168), + CubicToCommand(306.19130922, 349.64806168, 276.88158064, 345.0573813, + 247.57185205999997, 357.77003466), + CubicToCommand(247.57185205999997, 357.77003466, 226.03096719999996, + 368.36391246, 216.49647717999997, 386.37350472) + ], + ), + Path( + commands: const [ + MoveToCommand(364.10450785999996, 366.95139542), + CubicToCommand(364.10450785999996, 366.95139542, 336.91355483999996, + 347.52928612, 335.14790854, 345.0573813), + CubicToCommand(335.14790854, 345.0573813, 322.78838443999996, 325.635272, + 299.83498254, 324.92901348), + CubicToCommand(299.83498254, 324.92901348, 262.05015172, 326.34153052, + 231.68103535999998, 340.46670092) + ], + ), + Path( + commands: const [ + MoveToCommand(361.8003394385, 351.0729382441), + CubicToCommand(364.5229660331, 353.656078781, 412.13008721999995, + 404.73622624, 412.13008721999995, 404.73622624), + CubicToCommand(474.28083698, 469.35888082, 424.84274058, 408.97377736, + 424.84274058, 408.97377736), + CubicToCommand(411.42382869999994, 400.49867512000003, 395.17988274, + 367.30452468, 395.17988274, 367.30452468), + CubicToCommand(393.06110717999996, 362.36071504, 419.89893093999996, + 380.01717804, 419.89893093999996, 380.01717804), + CubicToCommand(426.96151613999996, 381.42969508, 450.97430582, + 415.33010404000004, 450.97430582, 415.33010404000004), + CubicToCommand(438.96791098, 411.09255292, 447.44301322, 423.80520628, + 447.44301322, 423.80520628), + CubicToCommand(452.38682286, 427.33649888, 488.4077730263, 454.88058116, + 488.4077730263, 454.88058116), + CubicToCommand(494.76409970629993, 461.94316635999996, 501.82491926, + 464.76820044, 501.82491926, 464.76820044), + CubicToCommand(526.54396746, 455.58683967999997, 515.24383114, + 478.89337084, 515.24383114, 478.89337084), + CubicToCommand(519.4813822599999, 490.89976568, 529.36900154, 470.4182686, + 529.36900154, 470.4182686), + CubicToCommand(549.1442400999999, 440.75541076, 520.1876407799999, + 444.99296187999994, 520.1876407799999, 444.99296187999994), + CubicToCommand(467.21825177999995, 449.93677152, 455.21185693999996, + 421.68643072, 455.21185693999996, 421.68643072), + CubicToCommand(450.9743058199999, 417.44887959999994, 466.51199325999994, + 421.68643072, 466.51199325999994, 421.68643072), + CubicToCommand(481.34518782629993, 425.21772332, 453.79933989999995, + 399.79241659999997, 453.79933989999995, 399.79241659999997), + CubicToCommand(458.03689102, 399.79241659999997, 474.28083698, + 411.79881143999995, 474.28083698, 411.79881143999995), + CubicToCommand(492.64532414629997, 428.0427573999999, 496.17661674629994, + 424.51146479999994, 496.17661674629994, 424.51146479999994), + CubicToCommand(527.9564845, 408.97377736, 546.3192060199999, 422.39268924, + 546.3192060199999, 422.39268924), + CubicToCommand(549.8504986199999, 425.21772331999995, 539.96287934, + 437.22411816, 542.78791342, 446.40547891999995), + CubicToCommand(545.6129475, 455.5868396799999, 554.08804974, 477.4808538, + 554.08804974, 477.4808538), + CubicToCommand(549.8504986199999, 480.30588787999994, 550.55675714, + 499.37486791999993, 550.55675714, 499.37486791999993), + CubicToCommand(580.21961498, 540.3378620799999, 563.2694104999999, + 536.8065694799999, 563.2694104999999, 536.8065694799999), + CubicToCommand(535.7253282199999, 536.1003109599999, 561.8568934599999, + 549.5192228399999, 561.8568934599999, 549.5192228399999), + CubicToCommand(567.5069616199999, 553.0505154399999, 583.04464906, + 565.7631687999999, 583.04464906, 565.7631687999999), + CubicToCommand(578.1008394199998, 563.6443932399999, 575.2758053399999, + 572.825754, 575.2758053399999, 572.825754), + CubicToCommand(583.7509075799999, 579.8883391999999, 578.80709794, + 588.36344144, 578.80709794, 588.36344144), + CubicToCommand(568.2132201399999, 590.4822169999999, 566.09444458, + 597.5448021999999, 566.09444458, 597.5448021999999), + CubicToCommand(578.1008394199999, 611.6699725999999, 560.4443764199999, + 612.3762311199999, 560.4443764199999, 612.3762311199999), + CubicToCommand(566.8007031, 620.1450748399999, 558.3256008599999, + 641.33283044, 558.3256008599999, 641.33283044), + CubicToCommand(549.8504986199999, 641.33283044, 538.5503623, + 651.2204497199999, 538.5503623, 651.2204497199999), + CubicToCommand(542.78791342, 659.69555196, 524.4251919, 669.58317124, + 524.4251919, 669.58317124), + CubicToCommand(509.59376297999995, 672.4082053199999, 514.53757262, + 684.41460016, 514.53757262, 684.41460016), + CubicToCommand(500.4141678663, 695.0084779599999, 496.17661674629994, + 723.2588187599999, 496.17661674629994, 723.2588187599999), + CubicToCommand(494.76409970629993, 741.6215402799999, 490.52654858629995, + 747.2716084399999, 499.70790934629997, 743.7403158399999), + CubicToCommand(508.88750445999995, 740.20902324, 507.47498741999993, + 718.31500912, 507.47498741999993, 718.31500912), + CubicToCommand(499.00165082629997, 690.77092684, 574.5695468199999, + 662.52058604, 574.5695468199999, 662.52058604), + CubicToCommand(581.63213202, 659.69555196, 583.04464906, + 650.5141911999999, 583.04464906, 650.5141911999999), + CubicToCommand(586.5759416599999, 651.2204497199999, 602.1136291, + 664.6393615999999, 602.1136291, 664.6393615999999), + CubicToCommand(615.53254098, 684.41460016, 616.2387994999999, 668.1706542, + 616.2387994999999, 668.1706542), + CubicToCommand(618.3575750599999, 661.81432752, 615.53254098, + 651.2204497199999, 615.53254098, 651.2204497199999), + CubicToCommand(626.12641878, 613.08248964, 601.4073705799999, + 601.78235332, 601.4073705799999, 601.78235332), + CubicToCommand(583.75090758, 542.4566376399999, 608.46995578, + 557.28806656, 608.46995578, 557.28806656), + CubicToCommand(613.41376542, 567.17568584, 632.4827454599999, 576.3570466, + 632.4827454599999, 576.3570466), + LineToCommand(638.8390721399999, 572.11949548), + CubicToCommand(636.01403806, 563.64439324, 650.84546698, 553.05051544, + 650.84546698, 553.05051544), + CubicToCommand(655.78927662, 564.35065176, 666.38315442, 550.22548136, + 666.38315442, 550.22548136), + CubicToCommand(672.7394810999999, 507.14371164, 694.63349522, + 532.56901836, 694.63349522, 532.56901836), + CubicToCommand(701.69608042, 534.68779392, 703.81485598, 522.68139908, + 703.81485598, 522.68139908), + CubicToCommand(710.1711826599999, 504.31867755999997, 703.81485598, + 480.30588788, 703.81485598, 480.30588788), + CubicToCommand(710.1711826599999, 479.59962936, 727.12138714, + 490.19350715999997, 727.12138714, 490.19350715999997), + CubicToCommand(732.06519678, 483.83718048000003, 715.8212508199999, + 454.17432264, 722.88383602, 458.41187376), + CubicToCommand(729.9464212199999, 462.64942487999997, 737.71526494, + 465.47445896, 737.71526494, 465.47445896), + CubicToCommand(739.12778198, 461.94316635999996, 721.47131898, + 440.04915224, 721.47131898, 440.04915224), + CubicToCommand(713.70247526, 435.1053426, 704.5211145, 399.08615807999996, + 704.5211145, 399.08615807999996), + CubicToCommand(717.23376786, 405.44248475999996, 699.5773048599999, + 378.60466099999996, 699.5773048599999, 378.60466099999996), + CubicToCommand(699.5773048599999, 372.95459284, 710.1711826599999, + 353.17935428, 710.1711826599999, 353.17935428), + CubicToCommand(708.7586656199999, 341.17295944, 710.1711826599999, + 341.87921796, 710.1711826599999, 341.87921796), + CubicToCommand(715.1149923, 343.99799352, 729.2401626999999, 346.8230276, + 717.23376786, 335.52289127999995), + CubicToCommand(705.22737302, 324.22275496, 718.6462849, 315.74765272, + 718.6462849, 315.74765272), + CubicToCommand(726.4151286199999, 310.80384308, 702.4023389399999, + 311.5101016, 702.4023389399999, 311.5101016), + CubicToCommand(693.22097818, 303.74125788, 693.9272367, 296.67867268, + 693.9272367, 296.67867268), + CubicToCommand(708.0524071, 300.20996528, 682.62710038, + 274.78465855999997, 678.38954926, 268.42833188), + CubicToCommand(674.1519981399999, 262.0720052, 691.10220262, 252.89064444, + 691.10220262, 252.89064444), + CubicToCommand(714.4087337799999, 246.53431776, 693.9272366999999, + 240.8842496, 693.9272366999999, 240.8842496), + CubicToCommand(659.3205692199999, 241.59050811999998, 678.38954926, + 222.52152808, 678.38954926, 222.52152808), + CubicToCommand(688.9834270599999, 223.2277866, 686.1583929799999, + 218.99023548, 686.1583929799999, 218.99023548), + CubicToCommand(676.97703222, 216.87145992, 660.0268277399998, 205.5713236, + 660.0268277399998, 205.5713236), + CubicToCommand(652.9642425399999, 199.21499691999998, 659.3205692199999, + 200.62751396, 659.3205692199999, 200.62751396), + CubicToCommand(688.9834270599999, 202.74628952, 638.1328136199999, + 182.97105095999999, 638.1328136199999, 182.97105095999999), + CubicToCommand(652.2579840199999, 182.97105095999999, 620.47635062, + 164.60832943999998, 620.47635062, 164.60832943999998), + CubicToCommand(616.9450580199998, 161.78329535999998, 611.29498986, + 148.36438348000001, 611.29498986, 148.36438348000001), + CubicToCommand(600.7011120599999, 139.18302272, 592.22600982, + 127.17662788, 592.22600982, 127.17662788), + CubicToCommand(591.5197512999998, 119.40778415999998, 583.04464906, + 110.93268192, 583.04464906, 110.93268192), + CubicToCommand(562.5631519799999, 86.91989224, 552.6755327, + 87.62615075999997, 552.6755327, 87.62615075999997), + CubicToCommand(526.54396746, 81.26982408, 517.3626066999999, + 82.68234111999999, 517.3626066999999, 82.68234111999999), + LineToCommand(424.13648205999993, 90.45118484), + CubicToCommand(377.52341973999995, 113.05145747999998, 391.29546087999995, + 150.13002978, 391.29546087999995, 150.13002978), + CubicToCommand(402.59559719999993, 164.96145869999998, 418.83954315999995, + 158.25200275999998, 418.83954315999995, 158.25200275999998), + CubicToCommand(426.96151613999996, 147.3049957, 447.44301321999995, + 151.18941755999998, 447.44301321999995, 151.18941755999998), + CubicToCommand(483.46396338629995, 156.83948572, 478.87328300629997, + 150.48315904, 478.87328300629997, 150.48315904), + CubicToCommand(474.63573188629994, 142.36118606, 446.03049617999994, + 131.414179, 445.67736691999994, 130.35479121999998), + CubicToCommand(445.32423765999994, 129.29540343999997, 429.7865502199999, + 123.29220601999998, 429.7865502199999, 123.29220601999998), + CubicToCommand(424.48961131999994, 121.17343045999999, 416.72076759999993, + 104.92948449999997, 416.72076759999993, 104.92948449999997), + CubicToCommand(411.07069943999994, 98.92628707999998, 438.96791097999994, + 109.16703561999998, 438.96791097999994, 109.16703561999998), + CubicToCommand(436.8491354199999, 110.93268192, 449.91491804, + 117.99526712, 449.91491804, 117.99526712), + CubicToCommand(480.63892930629993, 116.22962081999998, 499.35478008629997, + 135.29860085999996, 499.35478008629997, 135.29860085999996), + CubicToCommand(518.42199448, 164.60832943999998, 518.7751237399999, + 150.13002977999997, 518.7751237399999, 150.13002977999997), + CubicToCommand(523.71893338, 133.53295455999998, 502.88430703999995, + 96.10125299999999, 502.88430703999995, 96.10125299999999), + CubicToCommand(503.59056555999996, 92.56996039999999, 518.0688652199999, + 104.22322597999997, 518.0688652199999, 104.22322597999997), + CubicToCommand(520.54077004, 100.69193337999997, 521.9532870799999, + 110.93268191999996, 521.9532870799999, 110.93268191999996), + CubicToCommand(522.3064163399999, 115.17023303999997, 529.0158722799999, + 129.29540343999997, 529.0158722799999, 129.29540343999997), + CubicToCommand(533.9596819199999, 152.24880534, 540.3160085999999, + 139.18302271999997, 540.3160085999999, 139.18302271999997), + LineToCommand(548.4379815799999, 155.78009793999996), + CubicToCommand(550.9098864, 160.37077831999997, 540.3160085999999, + 173.78969019999994, 540.3160085999999, 173.78969019999994), + CubicToCommand(539.96287934, 178.73349983999995, 541.37539638, + 178.38037057999998, 531.4877770999999, 191.79928245999997), + CubicToCommand(521.6001578199999, 205.21819433999997, 527.6033552399999, + 212.98703805999997, 527.6033552399999, 212.98703805999997), + CubicToCommand(525.13145042, 224.64030363999996, 540.66913786, + 223.93404511999995, 540.66913786, 223.93404511999995), + CubicToCommand(545.25981824, 227.81846697999995, 551.26301566, + 227.81846697999995, 551.26301566, 227.81846697999995), + CubicToCommand(554.4411789999999, 231.34975957999995, 558.67873012, + 230.29037179999995, 558.67873012, 230.29037179999995), + CubicToCommand(561.5037642, 223.58091585999995, 572.45077126, + 227.11220845999995, 572.45077126, 227.11220845999995), + CubicToCommand(574.92267608, 222.87465733999994, 589.4009757399999, + 222.16839881999996, 589.4009757399999, 222.16839881999996), + CubicToCommand(591.16662204, 217.57771843999996, 591.8728805599999, + 214.75268435999996, 597.87607798, 213.69329657999995), + CubicToCommand(603.8792754, 212.63390879999997, 560.44437642, + 136.71111789999998, 560.44437642, 136.71111789999998), + CubicToCommand(571.74451274, 135.29860085999996, 557.2662130799999, + 113.40458673999996, 557.2662130799999, 113.40458673999996), + CubicToCommand(553.38179122, 101.75132115999995, 573.51015904, + 127.52975713999994, 577.3945808999999, 130.00166195999998), + CubicToCommand(581.27900276, 132.47356677999997, 583.04464906, + 136.35798863999995, 580.21961498, 136.00485937999997), + CubicToCommand(577.3945808999999, 135.65173012, 574.21641756, + 139.53615197999997, 576.6883223799999, 139.88928123999997), + CubicToCommand(579.1602272, 140.24241049999998, 602.1136291, 166.727105, + 608.1168265199999, 184.73669725999997), + CubicToCommand(614.12002394, 202.74628951999995, 624.71390174, + 209.80887471999998, 635.6609088, 220.40275251999998), + CubicToCommand(646.6079158599999, 230.99663031999998, 645.19539882, + 273.72527077999996, 645.19539882, 273.72527077999996), + CubicToCommand(644.4891402999999, 289.26295822, 655.0830181, + 307.97880899999996, 655.0830181, 307.97880899999996), + CubicToCommand(658.6143107, 314.68826493999995, 651.1985962399999, + 346.8230276, 651.1985962399999, 346.8230276), + CubicToCommand(647.66730364, 350.70744945999996, 650.13920846, + 352.1199665, 650.13920846, 352.1199665), + CubicToCommand(651.90485476, 354.23874206, 663.9112496, + 377.54527321999996, 663.9112496, 377.54527321999996), + CubicToCommand(660.7330862599999, 377.19214395999995, 667.0894129400001, + 383.54847064, 667.0894129400001, 383.54847064), + CubicToCommand(676.2707737000001, 394.14234844, 664.9706373800001, + 388.84540954, 664.9706373800001, 388.84540954), + CubicToCommand(654.37675958, 386.02037545999997, 666.73628368, + 403.32370919999994, 666.73628368, 403.32370919999994), + CubicToCommand(668.8550592400001, 406.50187253999997, 652.96424254, + 398.37989956, 652.96424254, 398.37989956), + CubicToCommand(636.7202965800001, 397.32051178, 657.20179366, + 410.03316513999994, 657.20179366, 410.03316513999994), + CubicToCommand(672.3863518400001, 422.7458185, 652.2579840200001, + 414.97697478, 652.2579840200001, 414.97697478), + CubicToCommand(644.1360110400001, 411.79881144, 649.7860792, 423.80520628, + 649.7860792, 423.80520628), + CubicToCommand(655.43614736, 426.63024035999996, 685.8052637200001, + 438.98976445999995, 685.8052637200001, 438.98976445999995), + CubicToCommand(686.51152224, 445.69922039999994, 681.21458334, + 454.52745189999996, 681.21458334, 454.52745189999996), + CubicToCommand(681.9208418600001, 461.59003709999996, 678.03642, + 467.59323451999995, 678.03642, 467.59323451999995), + CubicToCommand(675.91764444, 482.07153417999996, 674.85825666, + 483.48405121999997, 674.85825666, 483.48405121999997), + CubicToCommand(667.4425422, 483.8371804799999, 654.37675958, + 508.20309941999994, 654.37675958, 508.20309941999994), + CubicToCommand(651.1985962399999, 512.7937797999999, 633.18900398, + 533.9815354, 633.18900398, 533.9815354), + CubicToCommand(629.65771138, 546.3410594999999, 597.87607798, + 533.6284061399999, 597.87607798, 533.6284061399999), + CubicToCommand(586.2228124000001, 539.63160356, 589.754105, + 533.6284061399999, 589.754105, 533.6284061399999), + CubicToCommand(589.0478464800001, 529.74398428, 597.52294872, + 519.15010648, 597.52294872, 519.15010648), + CubicToCommand(609.88247282, 514.5594261, 605.2917924400001, + 495.49044605999995, 605.2917924400001, 495.49044605999995), + CubicToCommand(612.35437764, 493.01854124, 592.57913908, 488.0747316, + 592.9322683400001, 485.95595604), + CubicToCommand(593.2853976, 483.8371804799999, 603.52614614, + 481.36527565999995, 603.52614614, 481.36527565999995), + CubicToCommand(617.65131654, 477.83398306, 609.88247282, 473.59643194, + 609.88247282, 473.59643194), + CubicToCommand(608.82308504, 466.53384673999994, 614.12002394, + 456.64622746, 614.12002394, 456.64622746), + CubicToCommand(634.6015210200001, 455.23371041999997, 614.12002394, + 426.63024036, 614.12002394, 426.63024036), + CubicToCommand(595.0510439, 413.21132848, 593.2853976, 402.97057994, + 593.2853976, 402.97057994), + CubicToCommand(615.53254098, 388.49228027999993, 601.0542413200001, + 366.59826616, 601.40737058, 360.24193948), + CubicToCommand(601.76049984, 353.8856128, 603.8792754000001, + 315.74765271999996, 603.8792754000001, 315.74765271999996), + CubicToCommand(600.3479828, 304.80064566, 595.0510439, 280.78785597999996, + 595.0510439, 280.78785597999996), + CubicToCommand(598.9354657599999, 271.60649521999994, 612.00124838, + 249.35935183999996, 612.00124838, 249.35935183999996), + CubicToCommand(616.94505802, 241.94363737999996, 632.4827454599999, + 233.46853513999997, 628.5983236, 228.17159623999996), + CubicToCommand(624.71390174, 222.87465733999997, 610.9418606, + 226.05282067999997, 610.9418606, 226.05282067999997), + CubicToCommand(597.16981946, 223.58091585999998, 598.22920724, + 232.76227661999997, 598.22920724, 232.76227661999997), + CubicToCommand(595.40417316, 234.52792291999998, 593.99165612, + 243.35615441999997, 593.99165612, 243.35615441999997), + CubicToCommand(592.7203907840001, 257.3630265179, 577.0414516400001, + 268.42833188, 577.0414516400001, 268.42833188), + CubicToCommand(557.26621308, 279.37533893999995, 573.5101590400001, + 286.43792413999995, 573.5101590400001, 286.43792413999995), + CubicToCommand(584.10403684, 298.09118972, 566.8007031, + 298.44431897999993, 566.8007031, 298.44431897999993), + CubicToCommand(547.3785938000001, 295.26615563999997, 561.85689346, + 313.27574789999994, 561.85689346, 313.27574789999994), + CubicToCommand(580.9258735000001, 335.87602053999996, 575.6289346000001, + 340.81983017999994, 575.6289346000001, 340.81983017999994), + CubicToCommand(557.61934234, 342.58547647999995, 579.86648572, + 358.82942244, 579.86648572, 358.82942244), + CubicToCommand(579.86648572, 358.82942244, 578.45396868, 355.29812984, + 578.8070979400001, 358.47629317999997), + CubicToCommand(579.1602272, 361.65445651999994, 584.4571661, + 369.07017097999994, 585.86968314, 372.60146358), + CubicToCommand(587.28220018, 376.13275618, 580.2196149800001, + 376.48588543999995, 580.2196149800001, 376.48588543999995), + CubicToCommand(581.2790027600001, 393.43608992, 554.0880497400001, + 386.02037545999997, 554.0880497400001, 386.02037545999997), + LineToCommand(551.2630156600001, 386.37350472), + CubicToCommand(548.43798158, 386.72663398, 528.6627430200001, + 385.31411693999996, 518.4219944800001, 381.42969508), + CubicToCommand(508.18124594000005, 377.54527322, 496.17661674630006, + 377.54527322, 496.17661674630006, 377.54527322), + CubicToCommand(496.17661674630006, 377.54527322, 489.11403154630005, + 380.72343656, 475.6933540200001, 380.3703073), + CubicToCommand(462.2744421400001, 380.01717804, 448.1492717400001, + 384.96098767999996, 448.1492717400001, 384.96098767999996), + CubicToCommand(440.38042802000007, 384.25472915999995, 455.5649862000001, + 376.48588543999995, 455.9181154600001, 376.8390147), + CubicToCommand(456.2712447200001, 377.19214395999995, 466.1588640000001, + 367.30452468, 452.0336936000001, 368.36391246), + CubicToCommand(413.5479011989001, 371.2507441605, 394.4736242200001, + 353.17935428, 394.4736242200001, 353.17935428), + CubicToCommand(390.94233162000006, 350.70744946, 386.3516512400001, + 345.76363982, 386.3516512400001, 345.76363982), + CubicToCommand(368.69518824000005, 342.23234721999995, 388.8235560600001, + 367.65765394, 388.8235560600001, 367.65765394), + CubicToCommand(390.94233162000006, 370.12955876, 388.47042680000004, + 371.89520505999997, 388.47042680000004, 371.89520505999997), + CubicToCommand(387.0579097600001, 369.07017098, 373.2858686200001, + 359.53568096, 373.2858686200001, 359.53568096), + CubicToCommand(368.3226368707001, 357.8177071101, 365.9160609638001, + 355.4623349459, 361.80033943850003, 351.0729382441), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(319.6102211, 330.57908164), + CubicToCommand(319.6102211, 330.57908164, 340.09171818, 340.46670092, + 344.68239855999997, 345.41051056), + CubicToCommand(349.27307893999995, 350.35432019999996, 373.99212714, + 370.48268802, 373.99212714, 370.48268802), + CubicToCommand(373.99212714, 370.48268802, 364.45763711999996, + 366.95139542, 359.86695674, 363.77323208), + CubicToCommand(355.27627636, 360.59506874, 336.20729631999995, + 346.11676908, 336.20729631999995, 346.11676908), + CubicToCommand(336.20729631999995, 346.11676908, 329.49784037999996, + 335.52289128, 319.6102211, 330.57908164), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(217.18684488329995, 275.4962140189), + CubicToCommand(217.78186768639998, 275.23489836650003, 216.85666902519998, + 270.4464656009, 216.49647718, 269.48771966), + CubicToCommand(214.6919866614, 264.6745678462, 198.84001417999997, + 262.0720052, 198.84001417999997, 262.0720052), + CubicToCommand(198.43921246989996, 264.4821123995, 198.34210192339998, + 267.30008389430003, 198.52926043119996, 270.1922125337), + CubicToCommand(198.52926043119996, 270.1922125337, 207.12442661959994, + 279.9368144634, 217.18684488329995, 275.4962140189), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(217.18684488329995, 275.1430847589), + CubicToCommand(216.39406969459998, 275.4220568743, 217.16036018879998, + 270.31580777470003, 216.84960643999997, 269.48771966), + CubicToCommand(215.04511592139997, 264.67456784620003, 198.84001417999997, + 261.89544057, 198.84001417999997, 261.89544057), + CubicToCommand(198.43921246989996, 264.3055477695, 198.34210192339998, + 267.1235192643, 198.52926043119996, 270.0156479037), + CubicToCommand(198.52926043119996, 270.0156479037, 206.06503883959996, + 279.0539913134, 217.18684488329995, 275.1430847589), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(209.43389197999997, 275.3955721798), + CubicToCommand(208.33036304249998, 275.3955721798, 207.43694601469997, + 273.3827353978, 207.43694601469997, 270.9002367), + CubicToCommand(207.43694601469997, 268.4195036485, 208.33036304249998, + 266.4066668665, 209.43389197999997, 266.4066668665), + CubicToCommand(210.53742091749996, 266.4066668665, 211.43260359159996, + 268.4195036485, 211.43260359159996, 270.9002367), + CubicToCommand(211.43260359159996, 273.3827353978, 210.53742091749996, + 275.3955721798, 209.43389197999997, 275.3955721798), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(209.43389197999997, 270.9002367), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(128.92042069999997, 448.52425447999997), + CubicToCommand(128.92042069999997, 448.52425447999997, 119.03280142, + 466.18071748, 162.82082966, 455.58683968), + CubicToCommand(162.82082966, 455.58683968, 187.53987786, 453.46806412, + 191.77742897999997, 449.230513), + CubicToCommand(193.89620453999999, 450.64303004, 208.6676014858, + 455.816373699, 213.67144309999998, 456.99935672000004), + CubicToCommand(225.67783793999996, 459.8243908, 240.50926685999997, + 442.16792780000003, 240.50926685999997, 442.16792780000003), + CubicToCommand(240.50926685999997, 442.16792780000003, 248.63123983999998, + 423.62864165, 253.57504947999996, 423.62864165), + CubicToCommand(258.51885911999995, 423.62864165, 252.86879095999996, + 426.45367573, 252.86879095999996, 426.45367573), + CubicToCommand(252.86879095999996, 426.45367573, 241.21552537999997, + 444.28670336, 241.92178389999995, 447.11173744), + CubicToCommand(241.92178389999995, 447.11173744, 232.74042313999996, + 482.42466344, 204.49008233999996, 483.83718048000003), + CubicToCommand(204.49008233999996, 483.83718048000003, 175.97489459499994, + 485.514544465, 178.35851709999994, 495.84357532), + CubicToCommand(178.35851709999994, 495.84357532, 193.89620453999996, + 491.6060242, 198.13375565999996, 495.84357532), + CubicToCommand(198.13375565999996, 495.84357532, 217.20273569999995, + 495.1373168, 203.07756529999995, 506.43745312), + LineToCommand(191.07117045999996, 526.9189502), + CubicToCommand(191.07117045999996, 526.9189502, 191.31836094199997, + 533.840283696, 173.41470745999996, 527.62520872), + CubicToCommand(156.11137371999996, 521.6220113, 137.92521682999995, + 498.84517403, 137.92521682999995, 498.84517403), + CubicToCommand(137.92521682999995, 498.84517403, 109.76315834499997, + 473.15502036500004, 128.92042069999997, 448.52425447999997), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(126.80164513999998, 455.58683968), + CubicToCommand(126.80164513999998, 455.58683968, 123.27035253999998, + 472.53704416, 188.24613637999997, 454.17432264), + LineToCommand(200.25253121999998, 455.58683968), + CubicToCommand(204.49008233999996, 456.99935672, 225.67783793999996, + 461.94316635999996, 229.20913053999996, 459.8243908), + CubicToCommand(229.20913053999996, 459.8243908, 216.49647717999994, + 483.83718048000003, 196.01498009999995, 481.0121464), + CubicToCommand(196.01498009999995, 481.0121464, 172.70844893999995, + 483.83718048000003, 173.41470745999996, 492.31228272), + CubicToCommand(173.41470745999996, 492.31228272, 180.47729265999996, + 505.02493608, 188.95239489999994, 509.2624872), + CubicToCommand(188.95239489999994, 509.2624872, 193.89620453999996, + 513.50003832, 193.18994601999995, 519.15010648), + CubicToCommand(192.48368749999997, 524.80017464, 187.53987785999996, + 527.62520872, 184.00858525999996, 529.03772576), + CubicToCommand(180.47729265999996, 530.4502428, 174.82722449999994, + 524.80017464, 172.00219041999995, 524.80017464), + CubicToCommand(169.17715633999998, 524.80017464, 154.34572741999997, + 513.5000383199999, 146.57688369999994, 505.02493608), + CubicToCommand(138.80803997999993, 496.54983384, 123.97661105999995, + 475.36207823999996, 124.68286957999993, 470.41826860000003), + CubicToCommand(125.38912809999994, 465.47445896, 126.80164513999995, + 455.58683968, 126.80164513999995, 455.58683968), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(132.45171329999997, 486.397367615), + CubicToCommand(137.04239367999998, 493.3716705, 142.69246183999996, + 500.78738496000005, 146.57688369999997, 505.0249360800001), + CubicToCommand(154.34572741999997, 513.50003832, 169.17715633999998, + 524.80017464, 172.00219041999998, 524.80017464), + CubicToCommand(174.8272245, 524.80017464, 180.47729266, 530.4502428, + 184.00858526, 529.0377257600001), + CubicToCommand(187.53987786, 527.62520872, 192.48368749999997, + 524.80017464, 193.18994601999998, 519.1501064800001), + CubicToCommand(193.89620453999999, 513.50003832, 188.95239489999997, + 509.26248720000007, 188.95239489999997, 509.26248720000007), + CubicToCommand(183.53892334419996, 506.5610483610001, 178.70105248219997, + 500.39894277400003, 175.91309697449998, 496.2849868950001), + CubicToCommand(175.91309697449998, 496.2849868950001, 176.23974153999998, + 500.78738496000005, 167.05838077999996, 499.37486792000004), + CubicToCommand(157.87702001999997, 497.96235088000003, 148.69565925999996, + 493.0185412400001, 145.87062517999996, 487.36847308000006), + CubicToCommand(143.04559109999997, 481.71840492, 138.80803997999996, + 477.4808538000001, 141.63307405999996, 483.83718048000003), + CubicToCommand(144.45810813999995, 490.19350716, 148.69565925999996, + 496.54983384, 151.52069333999995, 497.25609236), + CubicToCommand(154.34572741999995, 497.96235088000003, 153.63946889999994, + 500.08112644000005, 149.40191777999993, 499.37486792000004), + CubicToCommand(145.16436665999993, 498.66860940000004, 140.22055701999994, + 497.96235088000003, 132.45171329999994, 488.78099012), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(127.86103291999999, 449.230513), + CubicToCommand(127.86103291999999, 449.230513, 131.03919625999998, + 425.21772332, 133.15797182, 418.15513812), + CubicToCommand(133.15797182, 418.15513812, 131.74545478, 406.14874328, + 135.98300589999997, 398.73302882), + CubicToCommand(140.22055701999997, 391.31731436, 143.75184961999997, + 380.37030730000004, 149.04878852, 370.83581728), + CubicToCommand(154.34572741999997, 361.30132726000005, 154.69885667999998, + 354.23874206000005, 161.76144187999998, 351.41370798), + CubicToCommand(168.82402707999998, 348.5886739, 179.41790487999998, + 333.40411572000005, 184.36171452, 331.63846942000004), + CubicToCommand(189.30552415999998, 329.87282312, 188.95239489999997, + 331.28534016000003, 188.95239489999997, 331.28534016000003), + CubicToCommand(188.95239489999997, 331.28534016000003, 200.95878974, + 305.15377492, 224.97157941999996, 312.21636012), + CubicToCommand(224.97157941999996, 312.21636012, 196.36810935999998, + 307.27255048, 224.26532089999998, 290.67547526000004), + CubicToCommand(224.26532089999998, 290.67547526000004, 215.79021866, + 292.61768619000003, 221.61685144999996, 280.25816209000004), + CubicToCommand(225.50303895629997, 272.01612516160003, 224.61845015999998, + 283.96601932, 205.19634085999996, 304.80064566), + CubicToCommand(205.19634085999996, 304.80064566, 196.36810935999998, + 319.98520384, 187.1867486, 325.28214274000004), + CubicToCommand(178.00538783999997, 330.57908164, 156.81763223999997, + 342.93860574, 154.69885667999998, 349.64806168), + CubicToCommand(152.58008111999996, 356.35751762, 146.93001295999994, + 366.59826616000004, 143.39872035999994, 369.42330024), + CubicToCommand(139.86742775999997, 372.24833432, 134.92361811999996, + 379.66404878000003, 134.21735959999995, 385.6672462), + CubicToCommand(134.21735959999995, 385.6672462, 132.09858403999993, + 392.7298314, 129.62667921999997, 394.84860696000004), + CubicToCommand(127.15477439999998, 396.96738252, 126.80164513999998, + 402.61745068000005, 126.80164513999998, 406.1487432800001), + CubicToCommand(126.80164513999998, 409.68003588000005, 123.27035253999998, + 414.62384552000003, 123.62348179999998, 418.86139664000007), + CubicToCommand(123.62348179999998, 418.86139664000007, 125.03599883999996, + 452.40867634000006, 124.32974031999998, 455.9399689400001), + LineToCommand(127.86103291999999, 449.2305130000001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(112.67647473999997, 457.35248598), + CubicToCommand(112.67647473999997, 457.35248598, 109.14518213999997, + 454.88058116, 101.37633841999997, 465.47445896), + CubicToCommand(101.37633841999997, 465.47445896, 114.26555640999996, + 523.74078686, 114.26555640999996, 526.21269168), + CubicToCommand(114.26555640999996, 526.21269168, 116.20776733999995, + 522.50483445, 113.91242714999996, 509.79218109), + CubicToCommand(111.61708695999997, 497.07952772999994, 110.02800528999995, + 474.65581971999995, 110.02800528999995, 474.65581971999995), + LineToCommand(112.67647473999995, 457.35248598), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(150.81443481999997, 350.3543202), + CubicToCommand(150.81443481999997, 350.3543202, 119.73905994, + 356.00438836, 120.44531845999998, 407.56126032), + LineToCommand(119.03280142, 451.34928856), + CubicToCommand(119.03280142, 451.34928856, 116.91402585999998, + 406.14874327999996, 114.79525029999996, 403.3237092), + CubicToCommand(112.67647473999997, 400.49867512000003, 119.73905993999998, + 380.72343656, 114.08899177999996, 391.31731436), + CubicToCommand(114.08899177999996, 391.31731436, 89.36994357999995, + 416.03636256000004, 103.49511397999996, 453.46806412), + CubicToCommand(103.49511397999996, 453.46806412, 106.14358342999998, + 459.29469690999997, 100.84664452999996, 451.17272393), + CubicToCommand(100.84664452999996, 451.17272393, 92.72467154999998, + 428.92558055000006, 94.66688247999997, 417.62544423), + CubicToCommand(94.66688247999997, 417.62544423, 95.02001173999994, + 413.74102237, 98.37473970999994, 408.79721273), + CubicToCommand(98.37473970999994, 408.79721273, 113.55929788999995, + 388.13915102, 118.32654289999996, 384.07816453), + CubicToCommand(118.32654289999996, 384.07816453, 121.50470623999993, + 358.65285781, 148.69565925999996, 349.47149705000004), + CubicToCommand(148.69565925999996, 349.47149705000004, 158.75984316999995, + 345.41051056000003, 150.81443481999997, 350.3543202), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(396.94552904, 233.46853514), + CubicToCommand(398.11085559799994, 232.8434963498, 398.09496478129995, + 231.13081943880002, 399.11903963529994, 230.8253626289), + CubicToCommand(401.14776723399996, 230.2179803017, 401.4373332272, + 228.3340356996, 402.35193801059995, 226.9497690004), + CubicToCommand(403.89334723049996, 224.62088153070002, 404.2341169664, + 221.9141457528, 405.261723113, 219.2815671195), + CubicToCommand(405.74197890659997, 218.0456147095, 405.7896513567, + 216.359422493, 405.2370040648, 215.194095935), + CubicToCommand(403.16413530859995, 210.8135274647, 401.924651606, + 206.489459676, 399.37858964139997, 202.2060017522), + CubicToCommand(398.90539643299996, 201.4114609172, 398.4427971024, + 200.009537755, 398.0879021961, 198.95368126760002), + CubicToCommand(397.2704079592, 196.5100267884, 395.0509905601, + 194.76027130510002, 393.5325347421, 192.4296181891), + CubicToCommand(393.02402860769996, 191.6509681708, 393.9509929152, + 190.01774534330002, 392.6867901644, 189.8694310541), + CubicToCommand(391.1030054333, 189.6840381926, 388.5445839446, + 188.6599633386, 388.1331883567, 190.4715164424), + CubicToCommand(387.0949883323, 195.03924342050001, 388.8800567416, + 199.4939690354, 390.58920236, 203.8056773), + CubicToCommand(389.2084669534, 205.02750453960002, 389.7981928176, + 206.6536647819, 390.0665710552, 208.007915494), + CubicToCommand(391.3201799282, 214.3748360518, 389.2049356608, + 220.2932824494, 387.8612788265, 226.4271376956), + CubicToCommand(387.82066896159995, 226.61076491080001, 388.4245199962, + 226.9603628782, 388.37508189979997, 227.0698329488), + CubicToCommand(386.21746212119996, 231.8123589106, 383.65374369359995, + 236.1293641141, 380.44203307389995, 240.3333679544), + CubicToCommand(379.10367317849995, 242.084889084, 377.5640296049, + 243.7022210948, 376.68650339379997, 245.5791031117), + CubicToCommand(376.0367455554, 246.9686667498, 375.316361865, + 248.6742810756, 375.75777344, 250.41873962), + CubicToCommand(369.69277839949996, 255.327236334, 365.72007422449997, + 262.1161463575, 361.10997173519996, 268.9068220273), + CubicToCommand(360.2942431446, 270.1074615113, 360.80804621789997, + 272.242127888, 361.78091732919995, 272.7170867427), + CubicToCommand(363.21638777109996, 273.4198139701, 364.9043456339, + 271.6117921589, 365.7341993949, 270.1180553891), + CubicToCommand(366.42103580559996, 268.8856342717, 367.04430894949996, + 267.7379641767, 367.91124128279995, 266.6026536058), + CubicToCommand(368.1460722407, 266.2936655033, 367.830021553, + 265.5538597036, 368.06838380349996, 265.3402165013), + CubicToCommand(372.71733051139995, 261.1962446352, 375.66419418609996, + 256.0123070984, 379.99532456, 251.83125666), + CubicToCommand(383.438334845, 251.2503590273, 386.1521332081, + 249.4882440199, 389.2384829405, 247.699644318), + CubicToCommand(389.78230200089996, 247.3835936303, 390.70750066209996, + 247.823239559, 391.21953808909996, 247.47893853050002), + CubicToCommand(394.314716053, 245.4007728354, 394.31118476039995, + 241.8059169686, 394.49834326819996, 238.4335325356), + CubicToCommand(394.5866255832, 236.8727012064, 394.932692258, + 234.5473450293, 396.94552904, 233.46853514), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(381.33545010169996, 225.5831587642), + CubicToCommand(381.55439024289996, 225.4472039991, 381.2807150664, + 224.70033561420001, 381.47317051309994, 224.3189560134), + CubicToCommand(381.75920521369994, 223.7468866122, 382.47076067259997, + 223.4149451078, 382.75679537319996, 222.8428757066), + CubicToCommand(382.9492508199, 222.4614961058, 382.66145047299995, + 221.7905505118, 382.90157836979995, 221.5398287372), + CubicToCommand(387.02083118769997, 217.2192922411, 387.47460228679995, + 211.8799778299, 385.64539271999996, 206.63071138), + CubicToCommand(387.4534145312, 205.536010674, 387.5611189555, + 203.3077650434, 386.81071927799997, 201.81226262730002), + CubicToCommand(385.3046229841, 198.81066391730002, 384.9638532482, + 195.41002914350003, 383.2423481057, 192.6856369026), + CubicToCommand(381.8262997731, 190.4450317479, 379.0401099117, + 188.2485677507, 376.61940883439996, 190.54920487959998), + CubicToCommand(375.87607174209995, 191.2554633996, 375.3092992798, + 192.6450270377, 375.80014895119996, 193.9021672033), + CubicToCommand(375.91315031439996, 194.18996755019998, 376.41635950989996, + 194.44775191, 376.3686870598, 194.5925349066), + CubicToCommand(376.1797629057, 195.1646043078, 375.2157200259, + 195.560109079, 375.20512614809996, 196.0403648726), + CubicToCommand(375.15215675909997, 198.6817717374, 373.46773018889996, + 201.3496632967, 374.58008735789997, 203.6962072294), + CubicToCommand(375.94316630149996, 206.5724450521, 377.3892306212, + 209.8512502312, 378.58280751999996, 212.98703806), + CubicToCommand(376.4039999858, 216.7231456308, 378.22791261369997, + 221.0472134195, 375.1327346498, 224.3613315246), + CubicToCommand(374.892606753, 224.6191158844, 374.9084975697, + 225.3006553562, 375.12390641829995, 225.6573159088), + CubicToCommand(375.6394751379, 226.5154200106, 376.35632753569996, + 227.2322724084, 377.21443163749996, 227.747841128), + CubicToCommand(377.5710921901, 227.9614843303, 378.1855371025, + 227.9650156229, 378.53866636249995, 227.7460754817), + CubicToCommand(379.55920992389997, 227.10691152110002, 380.2619371513, + 226.247041773, 381.33545010169996, 225.5831587642), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(492.23922549729997, 207.37757976490002), + CubicToCommand(494.69170820799997, 210.5416179345, 495.203745635, + 215.476599343, 491.58593636629996, 217.93084770000002), + CubicToCommand(492.55704183129995, 223.7733713067, 498.4737225826, + 220.279157279, 502.17804851999995, 219.34336474), + CubicToCommand(501.98382742699994, 218.6582939756, 502.38992607599994, + 218.05091164840002, 502.88430703999995, 218.04208341690003), + CubicToCommand(504.75589211799996, 218.0155987224, 505.9565316019999, + 216.19345174080001, 507.82811668, 216.51833066), + CubicToCommand(508.6050010519999, 213.7692193709, 511.48300452099994, + 212.52973566830002, 512.630674616, 210.09314377430002), + CubicToCommand(515.667586252, 203.5690806958, 514.625854935, + 196.02447405590001, 510.070487481, 190.2931861661), + CubicToCommand(509.7173582209999, 189.84471200590002, 510.08814394399997, + 188.9318728688, 509.876266388, 188.29270890819998), + CubicToCommand(508.53437519999994, 184.38886493889999, 504.897143822, + 183.8326863544, 501.47178999999994, 182.6179217), + CubicToCommand(499.39538995119995, 175.7760422875, 498.22829774689995, + 168.6587220522, 495.11722896629993, 162.13642462), + CubicToCommand(492.2674758380999, 161.695013045, 491.0350547206999, + 158.6245541293, 488.6743856175999, 157.3532887933), + CubicToCommand(486.32077909969996, 156.0837891036, 485.3178920012999, + 158.87174461130002, 485.39028349959995, 160.71861064109999), + CubicToCommand(485.40264302369997, 161.08056813259998, 486.20071515129996, + 161.4760729038, 485.90055528029995, 162.1205338033), + CubicToCommand(485.76636616149995, 162.4100997965, 485.30729812349995, + 162.6060865358, 485.30729812349995, 162.84268314), + CubicToCommand(485.3090637698, 163.08104539049998, 485.69927160209994, + 163.3123450558, 485.9358682062999, 163.54894165999997), + CubicToCommand(484.3150049028999, 164.996771626, 481.80249021799995, + 165.8389849111, 481.28162455949996, 167.86064992459998), + CubicToCommand(479.59719798929996, 174.4111976976, 484.14020591919996, + 179.92884238509998, 487.0853039476, 185.5806761914), + CubicToCommand(488.12880091089994, 187.5846847419, 486.8292852341, + 189.82882118919997, 485.48739404609995, 191.9617219196), + CubicToCommand(484.71404096669994, 193.1888460981, 484.9047307671, + 195.17872947819998, 485.39204914589993, 196.690122711), + CubicToCommand(486.7198151634999, 200.80584423629998, 489.52895842679993, + 203.876303152, 492.2392254972999, 207.37757976489996), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(426.6278089893, 239.84075263670002), + CubicToCommand(424.0022929412, 243.10896393800002, 417.9779077656, + 247.6449092827, 423.12829802269994, 251.0490753491), + CubicToCommand(423.47083340489996, 251.27684372180002, 424.12412253589997, + 251.283906307, 424.4295793458, 251.04730970280002), + CubicToCommand(427.99795051809997, 248.2840732433, 431.53454005699996, + 246.6896946344, 435.83565444379997, 245.66208848780002), + CubicToCommand(436.05459458499996, 245.6108847451, 436.5383816712, + 246.4354415672, 437.18990515589996, 246.15117251290002), + CubicToCommand(440.0378926378, 244.90992316400002, 443.5868417008, + 245.0123306494, 445.67736692, 242.64989590000002), + CubicToCommand(452.3285565321, 243.0454006712, 458.6230855916, + 241.0749394004, 464.6103921949, 238.76547404000002), + CubicToCommand(466.66030754919996, 237.9744644976, 468.90267835019995, + 236.9874682159, 471.02321955649995, 236.1011137733), + CubicToCommand(473.4527488653, 235.0841015045, 475.5785870105, + 233.4455817381, 477.54198569609997, 231.43097930980002), + CubicToCommand(477.77681665399996, 231.19085141300002, 478.4018554442, + 231.34975958, 478.87328300629997, 231.34975958), + CubicToCommand(478.80265715429994, 229.82600682310002, 480.50120889489995, + 229.601769743, 480.93732353099995, 228.5035377444), + CubicToCommand(481.09976299059997, 228.0939078028, 480.8349160456, + 227.394711868, 481.06974700349997, 227.2146159454), + CubicToCommand(484.8446987929, 224.3366124764, 486.4690933889, + 220.77353824300002, 484.4050528642, 216.57659698790002), + CubicToCommand(483.9018436687, 215.5560534265, 483.46219773999997, + 214.4648840131, 482.4628419342, 213.6244363743), + CubicToCommand(480.5471156987, 212.015932595, 478.6013734761, + 213.5237945352, 476.75274179999997, 212.98703806), + CubicToCommand(476.4720040383, 214.0958639364, 475.1848478856, + 213.7921727728, 474.5033084138, 214.1417707402), + CubicToCommand(472.9866182421, 214.9168894659, 470.6330117242, + 213.88398638040002, 469.1163215525, 214.6573394598), + CubicToCommand(466.7115112919, 215.88446363830002, 464.5203442336, + 216.182857863, 462.009595195, 216.8485065181), + CubicToCommand(461.4587135494, 216.9932895147, 460.08504072799997, + 216.8237874699, 459.80253732, 217.93084770000002), + CubicToCommand(459.5659407158, 217.6942510958, 459.30462506339995, + 217.2810898616, 459.1174665556, 217.3199340802), + CubicToCommand(455.7486134152, 218.019130015, 453.5238990772, + 218.39697832320002, 451.2267932409, 221.3844518628), + CubicToCommand(451.044931672, 221.6192828207, 450.2645160074, + 221.303232133, 449.9837782457, 221.5415943835), + CubicToCommand(448.2940547366, 222.9682365939, 447.5789679851, + 225.1470441281, 445.63499140880003, 226.3335584417), + CubicToCommand(445.2800965025, 226.5507329366, 444.6585890049, + 226.25410435819998, 444.3160536227, 226.4818727309), + CubicToCommand(443.1754461129, 227.2428662862, 442.5274539208, + 228.37464556449999, 441.3939089962, 229.1638894606), + CubicToCommand(440.81301136350004, 229.5682224633, 440.1067528435, + 228.9943874158, 440.1632535251, 228.542381963), + CubicToCommand(440.59583686860003, 225.1046686169, 441.7488039025, + 221.9494586788, 440.73355728, 218.63710622), + CubicToCommand(444.4008046451, 214.187677544, 448.8449363822, + 210.7464329053, 452.0336936, 205.92445286), + CubicToCommand(452.0601782945, 202.1000629742, 453.2820055341, + 198.3109860144, 453.0824875022, 194.6437386493), + CubicToCommand(453.0648310392, 194.30296891339998, 452.5563249048, + 193.00875017549998, 452.333853471, 192.39607090939998), + CubicToCommand(451.78297182539995, 190.88820896919998, 453.3826473732, + 189.0060300134, 451.892441896, 187.7277020922), + CubicToCommand(449.4134744908, 185.60362959329998, 447.1322594712, + 187.15033575209998, 445.67736692, 189.6805069), + CubicToCommand(442.4321090206, 190.36910895699998, 438.78428376479997, + 191.5927018429, 435.9980939034, 189.41389430869998), + CubicToCommand(434.2200880793, 188.0243306706, 433.1995445179, + 186.4246551228, 431.78879312419997, 184.56013263), + CubicToCommand(430.0496315187, 182.26126114739998, 430.6411230292, + 179.72579306059998, 430.7647182702, 176.9643222474), + CubicToCommand(430.775312148, 176.738319521, 430.2191335635, + 176.4981916242, 430.2191335635, 176.26159501999996), + CubicToCommand(430.2208992098, 176.0232327695, 430.6093413958, + 175.79193310419998, 430.845938, 175.55533649999998), + CubicToCommand(429.5993917122, 174.45004191619998, 429.1138389797, + 172.58198813079997, 427.3146454, 172.02404389999998), + CubicToCommand(427.8531675215, 170.0959581404, 426.65959062269997, + 168.53512681119997, 425.1217126954, 168.00190162859997), + CubicToCommand(421.5992483269, 166.78184003529998, 418.6382594818, + 170.1577557609, 415.2976566822, 170.28664794079998), + CubicToCommand(414.390114484, 170.32019522049995, 413.5655576619, + 168.4874543611, 412.4196532132, 167.91361931359998), + CubicToCommand(411.6568940116, 167.5322397128, 410.41387901639996, + 167.47397338489998, 409.7747150558, 167.94363530069998), + CubicToCommand(408.5599504014, 168.8335210359, 407.5199847307, + 169.0348047141, 406.1816248353, 169.3967622056), + CubicToCommand(403.3265747682, 170.16658399239998, 401.0700787968, + 172.0982010446, 398.4569222728, 173.60782863109998), + CubicToCommand(395.8349375173, 175.1209875102, 394.1487453008, + 177.65822124329998, 392.24184729679996, 179.98887435929998), + CubicToCommand(390.5803741285, 182.02289889689996, 390.3402462317, + 186.28516906509998, 392.8474639777, 187.16799221509996), + CubicToCommand(396.1015501086, 188.31566231009998, 398.4039528838, + 183.54312036119998, 401.8752135096, 184.11872105499998), + CubicToCommand(402.42609515519996, 184.20876901629998, 402.789818293, + 184.75788501559998, 402.5955972, 185.44295577999998), + CubicToCommand(403.28243361069997, 185.6354112267, 403.6673445041, + 185.231078224, 404.00811424, 184.73669725999997), + CubicToCommand(405.5230387654, 186.5358908397, 407.57825105859996, + 187.13091364279998, 409.234427288, 188.67232286269999), + CubicToCommand(410.94710419899997, 190.2684671179, 414.0299226388, + 189.53042696449998, 415.8220536333, 191.29077632559998), + CubicToCommand(418.52172682599996, 193.94277706819997, 417.5347305443, + 198.67647479849998, 420.95831871999997, 200.98064322), + CubicToCommand(419.9236499882, 203.29893681189998, 418.85190268409997, + 205.553667137, 418.2604111736, 208.0732444071), + CubicToCommand(417.762498917, 210.2026138449, 419.5175513392, + 212.316092466, 421.6557490085, 212.15541865269998), + CubicToCommand(423.87693205389996, 211.98944790049998, 424.3395313845, + 210.64932235879996, 425.19586984, 208.74948694), + CubicToCommand(425.6672974021, 209.2209145021, 426.49538551679996, + 209.74531145319997, 426.4229940185, 210.126691054), + CubicToCommand(425.6160936594, 214.34128877209997, 423.8027749093, + 217.86022184799998, 422.9941089039, 222.1525080033), + CubicToCommand(422.88993577220003, 222.71221788039998, 422.3514136507, + 223.06887843299998, 421.66457723999997, 222.87465734), + CubicToCommand(420.83825477159996, 230.20032383869997, 413.68385596400003, + 234.42728108089997, 409.1902861305, 240.01555162039998), + CubicToCommand(408.47873067160003, 240.90190606299998, 408.4716680864, + 243.10719829169997, 409.1938174231, 243.81875375059997), + CubicToCommand(411.671019182, 246.2677051687, 415.13168593, + 243.53801598889999, 418.13328464, 242.6498959), + CubicToCommand(418.5093673019, 240.5205264622, 420.0260574736, + 238.85728764759997, 422.37789834520004, 238.9561638404), + CubicToCommand(422.82990379800003, 238.97382030339998, 423.2448306785, + 238.0168400088, 423.8169000797, 237.7873059898), + CubicToCommand(424.4313449921, 237.54364680039998, 425.3176994347, + 237.93915157159998, 425.82267427650004, 237.58955360419998), + CubicToCommand(428.87547672920005, 235.47960627569998, 431.3615067196, + 233.5268014679, 434.430199989, 231.4274480172), + CubicToCommand(434.7692040786, 231.19614835189998, 435.3765864058, + 231.4768861136, 435.7579660066, 231.28619631319998), + CubicToCommand(436.3318010541, 231.00016161259998, 436.66197691220003, + 230.31685649449997, 437.23228066710004, 229.99021192899997), + CubicToCommand(437.8467255795, 229.637082669, 438.2740119841, + 230.149120096, 438.61478172, 230.64350105999998), + CubicToCommand(437.4741742102, 231.25971161869998, 437.46711162500003, + 232.94943512779997, 436.4395054784, 233.30609568039998), + CubicToCommand(435.0711295959, 233.77928888879998, 434.08060202160004, + 234.6850654407, 432.8905564154, 235.4743093368), + CubicToCommand(432.37498769580003, 235.81507907269997, 431.2114267841, + 235.37719879029999, 431.02779956890004, 235.69324947799998), + CubicToCommand(429.9719430815, 237.50833387439997, 427.88141786230005, + 238.28168695379998, 426.62780898930004, 239.8407526367), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(328.79158185999995, 152.6019346), + CubicToCommand(328.79158185999995, 152.6019346, 312.4805413406, + 147.5309984264, 292.77239734, 192.85867024), + CubicToCommand(292.77239734, 192.85867024, 288.53484621999996, 202.040031, + 284.2972951, 205.5713236), + CubicToCommand(280.05974397999995, 209.1026162, 260.28450541999996, + 215.45894288, 256.75321282, 222.52152808), + LineToCommand(238.39049129999998, 250.77186888), + CubicToCommand(238.39049129999998, 250.77186888, 264.52205654, + 222.52152808, 270.1721247, 218.28397696), + CubicToCommand(270.1721247, 218.28397696, 284.2972951, 203.45254804, + 278.64722694, 215.45894288), + CubicToCommand(278.64722694, 215.45894288, 253.92817873999996, + 234.52792292, 256.0469543, 250.77186888), + CubicToCommand(256.0469543, 250.77186888, 246.15933501999996, 276.1971756, + 244.74681797999997, 279.7284682), + CubicToCommand(244.74681797999997, 279.7284682, 272.99715877999995, + 223.2277866, 277.2347099, 221.10901103999998), + CubicToCommand(281.47226101999996, 218.99023547999997, 283.59103658, + 218.99023548, 281.47226101999996, 225.34656216), + CubicToCommand(279.35348545999994, 231.70288883999999, 278.64722694, + 260.65948815999997, 273.70341729999996, 264.19078076), + CubicToCommand(273.70341729999996, 264.19078076, 287.82858769999996, + 228.17159623999999, 286.41607065999995, 222.52152808), + CubicToCommand(286.41607065999995, 222.52152808, 292.06613882, + 216.16520139999997, 296.30368993999997, 225.34656216), + LineToCommand(294.18491437999995, 253.59690296), + LineToCommand(301.95375809999996, 274.78465855999997), + CubicToCommand(301.95375809999996, 274.78465855999997, 297.71620698, + 255.00941999999998, 300.54124105999995, 227.46533771999998), + CubicToCommand(300.54124105999995, 227.46533771999998, 297.00994846, + 209.10261619999997, 304.07253366, 218.99023547999997), + CubicToCommand(311.13511886, 228.87785476, 328.08532333999995, + 239.47173256, 328.08532333999995, 247.94683479999998), + CubicToCommand(328.08532333999995, 247.94683479999998, 318.90396258, + 216.87145991999998, 302.66001661999996, 208.39635768), + LineToCommand(295.59743141999996, 218.99023547999997), + LineToCommand(293.47865586, 215.45894288), + CubicToCommand(293.47865586, 215.45894288, 287.12232917999995, + 214.04642583999998, 294.89117289999996, 202.04003099999997), + CubicToCommand(302.66001661999996, 190.03363616, 301.95375809999996, + 188.62111911999997, 301.95375809999996, 188.62111911999997), + CubicToCommand(301.95375809999996, 188.62111911999997, 313.25389442, + 201.33377248, 316.07892849999996, 201.33377248), + CubicToCommand(316.07892849999996, 201.33377248, 339.38545966, + 187.9148606, 341.50423522, 230.99663031999998), + CubicToCommand(341.50423522, 230.99663031999998, 353.51063006, + 205.57132359999997, 337.26668409999996, 193.56492876), + CubicToCommand(337.26668409999996, 193.56492876, 311.13511886, + 190.03363616, 313.25389442, 180.8522754), + LineToCommand(325.96654778, 158.95826128), + CubicToCommand(332.32287446, 149.77690051999997, 329.49784037999996, + 154.72071015999998, 329.49784037999996, 154.72071015999998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(293.47865586, 181.55853392), + LineToCommand(265.22831506, 190.73989468000002), + LineToCommand(252.51566169999998, 207.69009916000002), + CubicToCommand(252.51566169999998, 207.69009916000002, 282.88477806, + 190.03363616000001, 289.94736326, 187.9148606), + CubicToCommand(297.00994846, 185.79608503999998, 293.47865586, + 181.55853392, 293.47865586, 181.55853392), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(222.85280386, 192.85867024), + CubicToCommand(222.85280386, 192.85867024, 219.32151125999997, + 194.9774458, 218.61525274, 199.92125544), + CubicToCommand(217.90899421999998, 204.86506508, 213.67144309999998, + 205.5713236, 215.08396014, 210.51513324), + CubicToCommand(216.49647718, 215.45894288, 220.02776977999997, 219.696494, + 220.02776977999997, 212.6339088), + CubicToCommand(220.02776977999997, 205.5713236, 222.85280386, 202.040031, + 224.26532089999998, 199.92125544), + CubicToCommand(225.67783793999996, 197.80247988, 228.50287201999998, + 190.03363616000001, 222.85280386, 192.85867024), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(207.31511641999998, 300.9162238), + CubicToCommand(207.31511641999998, 300.9162238, 192.48368749999997, + 293.8536386, 186.83361933999998, 287.49731192), + CubicToCommand(181.18355118000002, 281.14098524, 181.9816233076, + 290.2623140258, 173.41470746000002, 289.61608748000003), + CubicToCommand(163.09097354390002, 288.8374374617, 164.93960522, + 260.65948816, 164.93960522, 260.65948816), + LineToCommand(157.87702002, 274.07840004), + CubicToCommand(157.87702002, 274.07840004, 155.75824446000001, + 299.50370676, 169.88341486000002, 295.26615564), + CubicToCommand(176.7817949541, 293.19681817640003, 179.06477562, + 295.97241415999997, 176.23974153999998, 297.3849312), + CubicToCommand(173.41470746, 298.79744824, 186.12736081999998, + 299.50370676, 181.18355118, 302.32874084), + CubicToCommand(176.23974153999998, 305.15377492, 201.66504826, + 295.97241415999997, 197.42749714, 314.33513568), + LineToCommand(207.31511641999998, 300.9162238), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(185.06797303999997, 326.34153052), + CubicToCommand(185.06797303999997, 326.34153052, 157.87702001999997, + 334.11037424, 151.52069333999998, 317.16016976), + CubicToCommand(151.52069333999998, 317.16016976, 143.04559109999997, + 321.39772088, 146.93001295999997, 326.69465978), + CubicToCommand(150.81443481999997, 331.99159868, 152.93321038, + 332.6978572, 152.93321038, 332.6978572), + CubicToCommand(152.93321038, 332.6978572, 162.4677004, 334.81663276, + 161.40831261999998, 336.22914979999996), + CubicToCommand(160.34892483999997, 337.64166683999997, 156.11137372, + 343.64486425999996, 156.11137372, 343.64486425999996), + CubicToCommand(156.11137372, 343.64486425999996, 174.12096598, + 333.05098646, 185.06797304, 326.34153052), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(588.34158796, 464.41507118), + CubicToCommand(587.582360051, 468.193554262, 584.63373073, + 469.60607130200003, 581.2790027599999, 470.77139786), + CubicToCommand(577.888961864, 469.076377412, 573.315937947, 463.602873882, + 569.9788664399999, 467.24010525999995), + CubicToCommand(569.149012679, 466.392595036, 567.7718085649999, + 466.286656258, 567.15383236, 465.1213297), + CubicToCommand(566.3416350619999, 463.53224803, 566.818359563, + 461.695975878, 566.2180398209999, 460.283458838), + CubicToCommand(565.2469343559999, 458.023431574, 564.0109819459999, + 455.622152606, 564.32879828, 453.11493485999995), + CubicToCommand(567.524618083, 451.861325987, 568.5663494, 448.488941554, + 567.7364956389999, 445.38140406599996), + CubicToCommand(567.612900398, 444.922336028, 566.853672489, 444.586863231, + 567.206801749, 443.968887026), + CubicToCommand(567.5422745459999, 443.38622374700003, 568.089624899, + 442.997781561, 568.5663494, 442.52105706), + CubicToCommand(568.336815381, 442.76824754200004, 568.0719684359999, + 443.17434619100004, 567.877747343, 443.13903326499997), + CubicToCommand(566.8007031, 442.944812172, 567.012580656, + 441.81479853999997, 567.242114675, 441.14385294600004), + CubicToCommand(568.283845992, 438.05397192099997, 571.8327950549999, + 437.59490388300003, 574.21641756, 439.69602298), + CubicToCommand(574.6754855979999, 438.70726105200004, 575.575965211, + 439.042733849, 576.33519312, 438.98976446), + CubicToCommand(576.246910805, 437.965689606, 576.970825788, 437.029897067, + 577.3239550479999, 436.288325621), + CubicToCommand(578.2420911239999, 434.363771154, 581.1024381299999, + 436.305982084, 582.5149551699999, 435.22893784100006), + CubicToCommand(584.421853174, 433.763451412, 586.293438252, 432.545155465, + 588.2003362559999, 433.604543245), + CubicToCommand(591.396156059, 435.387846008, 594.415411232, 437.524278031, + 596.5341867919999, 440.614159056), + CubicToCommand(597.5406051829999, 442.079645485, 597.964360295, + 444.339672749, 597.8584215169999, 446.034693197), + CubicToCommand(597.787795665, 447.182363292, 595.351203771, 446.546730624, + 594.750884029, 448.188781683), + CubicToCommand(593.603213934, 451.278662708, 596.852003126, + 452.19679878399995, 598.176237851, 454.56276482600003), + CubicToCommand(598.5293671109999, 455.180741031, 598.0702990729999, + 455.710434921, 597.505292257, 455.88699955100003), + CubicToCommand(596.781377274, 456.11653357, 595.40417316, + 455.78106077300004, 595.6337071789999, 456.575601608), + CubicToCommand(597.364040553, 462.208013305, 592.490856765, + 463.40865278900003, 588.34158796, 464.41507118), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(571.39138348, 499.02173866), + CubicToCommand(571.373727017, 495.949514098, 568.5486929369999, + 492.85963307299994, 570.6851249599999, 489.8403779), + CubicToCommand(570.9323154419999, 490.087568382, 571.161849461, + 490.476010568, 571.39138348, 490.476010568), + CubicToCommand(571.638573962, 490.476010568, 571.868107981, + 490.08756838200003, 572.097642, 489.8403779), + CubicToCommand(574.74611145, 493.760112686, 581.190720445, + 495.38450728199996, 580.943529963, 500.416599237), + CubicToCommand(580.8905605739999, 501.21114007200003, 578.98366257, + 502.83553466800004, 580.5727442399999, 503.9655483), + CubicToCommand(577.376924437, 506.34917080499997, 577.270985659, + 510.533752536, 575.6289345999999, 513.85316758), + CubicToCommand(573.4395331879999, 513.358786616, 571.3031011649999, + 512.705497485, 569.2726079199999, 511.73439202), + CubicToCommand(569.8905841249999, 509.12123549600005, 569.6963630319999, + 506.13729324900004, 571.1441929979999, 503.806640133), + CubicToCommand(571.9034209069999, 502.570687723, 571.39138348, + 500.66378971899996, 571.39138348, 499.02173866), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(277.94096842, 483.13092196), + CubicToCommand(277.94096842, 483.13092196, 248.17570309459998, + 501.03457544199995, 272.99715877999995, 473.94956119999995), + CubicToCommand(288.53484621999996, 456.99935672, 306.19130922, + 447.11173743999996, 306.19130922, 447.11173743999996), + CubicToCommand(306.19130922, 447.11173743999996, 324.55403074, + 439.34289372, 330.91035741999997, 437.22411816), + CubicToCommand(337.26668409999996, 435.1053426, 364.10450785999996, + 425.92398184, 369.75457601999994, 425.21772332), + CubicToCommand(375.40464418, 424.5114648, 392.35484866, + 417.44887959999994, 404.3612435, 424.5114648), + CubicToCommand(416.36763834, 431.57404999999994, 430.49280874, + 439.34289372, 430.49280874, 439.34289372), + CubicToCommand(430.49280874, 439.34289372, 401.53620942, 424.5114648, + 395.17988274, 428.74901592000003), + CubicToCommand(388.82355606, 432.98656704, 376.1109027, + 432.28030851999995, 365.51702489999997, 437.93037668), + CubicToCommand(365.51702489999997, 437.93037668, 339.38545966, + 445.6992204, 333.7353915, 449.230513), + CubicToCommand(328.08532333999995, 452.7618056, 309.72260181999997, + 473.24330268, 306.89756774, 471.83078564), + CubicToCommand(304.07253366, 470.41826860000003, 307.60382625999995, + 469.71201008, 309.72260181999997, 464.76820044), + CubicToCommand(311.84137738, 459.8243908, 308.31008477999995, + 456.99935672, 294.18491437999995, 468.29949304), + CubicToCommand(280.05974397999995, 479.59962936, 277.94096842, + 483.13092196, 277.94096842, 483.13092196), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(291.0155792715, 472.590013549), + CubicToCommand(291.0155792715, 472.590013549, 293.5051405545, + 449.565985797, 308.47428988589996, 452.514615118), + CubicToCommand(308.47428988589996, 452.514615118, 322.99849634969996, + 445.151870047, 327.80458557829996, 441.673546836), + CubicToCommand(327.80458557829996, 441.673546836, 342.175180814, + 438.67194812599996, 344.48817746699996, 437.57724742000005), + CubicToCommand(377.11555544469996, 422.21965590260004, 403.10410333439995, + 430.1986115323, 404.0699118605, 428.22108767630004), + CubicToCommand(405.0339547403, 426.24532946659997, 439.6847633778, + 438.81319983000003, 446.05698087449997, 446.01703673400004), + CubicToCommand(446.7473485778, 446.81157756900006, 427.99265357919995, + 436.147073917, 410.8694157618, 432.81000241000004), + CubicToCommand(396.2622239219, 429.9549523429, 358.1207325493, + 433.233757522, 338.86459400149994, 443.015438024), + CubicToCommand(333.6153275516, 445.681563937, 317.8216213981, + 455.88699955100003, 313.3527706128, 455.692778458), + CubicToCommand(308.8839198275, 455.498557365, 291.0155792715, + 472.590013549, 291.0155792715, 472.590013549), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(284.2972951, 517.7375894400001), + CubicToCommand(284.2972951, 517.7375894400001, 257.45947133999994, + 513.50003832, 287.12232917999995, 510.67500424), + CubicToCommand(287.12232917999995, 510.67500424, 318.90396258, + 507.14371164, 325.96654778, 497.96235088000003), + CubicToCommand(325.96654778, 497.96235088000003, 349.97933745999995, + 481.71840492000007, 354.9231471, 481.01214640000006), + CubicToCommand(359.86695674, 480.30588788000006, 412.83634573999996, + 467.59323452000007, 413.54260425999996, 463.3556834000001), + CubicToCommand(414.24886277999997, 459.11813228000005, 424.13648206, + 459.11813228000005, 426.96151613999996, 460.53064932000007), + CubicToCommand(429.78655022, 461.9431663600001, 428.37403317999997, + 464.0619419200001, 423.43022354, 465.47445896000005), + CubicToCommand(418.4864139, 466.88697600000006, 363.39824934, + 495.84357532000007, 352.09811301999997, 497.96235088000003), + CubicToCommand(340.7979767, 500.08112644000005, 320.31647962, + 513.50003832, 311.84137738, 515.6188138800001), + CubicToCommand(303.36627513999997, 517.7375894400001, 284.2972951, + 517.7375894400001, 284.2972951, 517.7375894400001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(318.76271087599997, 504.67180682000003), + CubicToCommand(318.76271087599997, 504.67180682000003, 303.60993432939995, + 503.20632039099996, 318.7962581557, 501.776146888), + CubicToCommand(318.7962581557, 501.776146888, 334.3621959365, + 495.71998007900004, 337.9782395589, 491.02336092100006), + CubicToCommand(337.9782395589, 491.02336092100006, 350.2742003921, + 482.70716684800004, 352.80437154, 482.33638112500006), + CubicToCommand(355.3363083342, 481.98325186500006, 379.9847306822, + 475.46801701800007, 380.3466881737, 473.29627206900005), + CubicToCommand(380.7086456652, 471.12452712000004, 440.857152521, + 448.91269666600004, 448.661309167, 454.54510836300005), + CubicToCommand(453.8011055463, 458.252965593, 436.31944152999995, + 455.30433627200006, 419.3092050758, 463.09083645500004), + CubicToCommand(416.9167543393, 464.18553716100007, 357.1443301454, + 489.92866021500004, 351.3583072203, 491.02336092100006), + CubicToCommand(345.5722842952, 492.10040516400005, 335.0861109195, + 498.96876927100004, 330.7461523141, 500.06346997700007), + CubicToCommand(326.407959355, 501.14051422000006, 318.76271087599997, + 504.67180682000003, 318.76271087599997, 504.67180682000003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(304.77879218, 508.55622868), + CubicToCommand(304.77879218, 508.55622868, 313.96015294, 507.84997016, + 311.84137738, 510.67500423999996), + CubicToCommand(309.72260181999997, 513.5000383199999, 305.4850507, + 512.08752128, 305.4850507, 512.08752128), + LineToCommand(304.77879218, 508.55622868), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(292.06613882, 511.38126276), + CubicToCommand(292.06613882, 511.38126276, 301.24749957999995, + 510.67500423999996, 299.12872402, 513.5000383199999), + CubicToCommand(297.00994846, 516.3250724, 292.77239734, 514.9125553599999, + 292.77239734, 514.9125553599999), + LineToCommand(292.06613882, 511.38126276), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(273.70341729999996, 514.20629684), + CubicToCommand(273.70341729999996, 514.20629684, 282.88477806, + 513.50003832, 280.76600249999996, 516.3250724), + CubicToCommand(278.64722694, 519.15010648, 274.40967581999996, + 517.7375894400001, 274.40967581999996, 517.7375894400001), + LineToCommand(273.70341729999996, 514.20629684), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(260.28450541999996, 515.61881388), + CubicToCommand(260.28450541999996, 515.61881388, 269.46586618, + 514.9125553599999, 267.34709061999996, 517.73758944), + CubicToCommand(265.22831506, 520.56262352, 260.99076393999997, + 519.15010648, 260.99076393999997, 519.15010648), + LineToCommand(260.28450541999996, 515.61881388), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(328.08532334, 445.6992204), + LineToCommand(333.7353915, 448.52425447999997), + CubicToCommand(331.61661594, 451.34928856, 325.96654778, 450.64303004, + 325.96654778, 450.64303004), + LineToCommand(328.08532334, 445.6992204), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(310.42886033999997, 455.58683968), + CubicToCommand(310.42886033999997, 455.58683968, 321.91615516779996, + 451.808356598, 317.49144554, 457.70561524000004), + CubicToCommand(315.37266997999996, 460.53064931999995, 311.13511886, + 459.11813228, 311.13511886, 459.11813228), + LineToCommand(310.42886033999997, 455.58683968), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(290.65362178, 464.06194192), + CubicToCommand(290.65362178, 464.06194192, 299.83498254, 463.3556834, + 297.71620698, 466.18071748), + CubicToCommand(295.59743141999996, 469.00575156, 291.3598803, + 467.59323452, 291.3598803, 467.59323452), + LineToCommand(290.65362178, 464.06194192), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(277.2347099, 474.65581972), + CubicToCommand(277.2347099, 474.65581972, 286.41607065999995, 473.9495612, + 284.2972951, 476.77459528), + CubicToCommand(282.17851953999997, 479.59962936, 277.94096842, + 478.18711232, 277.94096842, 478.18711232), + LineToCommand(277.2347099, 474.65581972), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(265.22831506, 483.13092196), + CubicToCommand(265.22831506, 483.13092196, 274.40967581999996, + 482.42466344, 272.29090026, 485.2496975199999), + CubicToCommand(270.1721247, 488.07473159999995, 265.93457358, + 486.66221456, 265.93457358, 486.66221456), + LineToCommand(265.22831506, 483.13092196), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(334.2333037566, 494.43105828), + CubicToCommand(334.2333037566, 494.43105828, 346.4533417989, + 493.495265741, 343.6336046578, 497.25609236), + CubicToCommand(340.8138675167, 500.999262516, 335.1726275882, + 499.127677438, 335.1726275882, 499.127677438), + LineToCommand(334.2333037566, 494.43105828), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(352.59602527659996, 485.95595604000005), + CubicToCommand(352.59602527659996, 485.95595604000005, 364.8160633189, + 485.020163501, 361.9963261778, 488.78099012), + CubicToCommand(359.17658903669997, 492.52416027600003, 353.53534910819997, + 490.652575198, 353.53534910819997, 490.652575198), + LineToCommand(352.59602527659996, 485.95595604000005), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(371.66500531659995, 478.18711232), + CubicToCommand(371.66500531659995, 478.18711232, 383.88504335889996, + 477.251319781, 381.06530621779996, 481.0121464), + CubicToCommand(378.24556907669995, 484.755316556, 372.60432914819995, + 482.883731478, 372.60432914819995, 482.883731478), + LineToCommand(371.66500531659995, 478.18711232), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(390.0277268366, 469.71201008), + CubicToCommand(390.0277268366, 469.71201008, 402.24776487889994, + 468.776217541, 399.4280277378, 472.53704416), + CubicToCommand(396.6082905967, 476.28021431599996, 390.96705066819993, + 474.408629238, 390.96705066819993, 474.408629238), + LineToCommand(390.0277268366, 469.71201008), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(341.2958889566, 437.93037668), + CubicToCommand(341.2958889566, 437.93037668, 353.5159269989, + 436.994584141, 350.6961898578, 440.75541076), + CubicToCommand(347.8764527167, 444.498580916, 340.8226957482, + 444.039512878, 340.8226957482, 444.039512878), + LineToCommand(341.2958889566, 437.93037668), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(358.95235195659995, 432.28030852), + CubicToCommand(358.95235195659995, 432.28030852, 371.1723899989, + 431.3374533958, 368.35265285779997, 435.1053426), + CubicToCommand(365.53291571669996, 438.848512756, 357.7729002282, + 438.389444718, 357.7729002282, 438.389444718), + LineToCommand(358.95235195659995, 432.28030852), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(318.90396258, 502.90616052), + CubicToCommand(318.90396258, 502.90616052, 328.08532334, 502.199902, + 325.96654778, 505.02493608), + CubicToCommand(323.84777221999997, 507.84997016, 319.6102211, + 506.43745312, 319.6102211, 506.43745312), + LineToCommand(318.90396258, 502.90616052), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(189.65865341999998, 327.75404756), + CubicToCommand(189.65865341999998, 327.75404756, 181.88980969999997, + 343.291735, 181.18355118, 348.94180316), + CubicToCommand(181.18355118, 348.94180316, 182.59606821999998, + 333.40411572000005, 184.71484378, 329.87282312), + CubicToCommand(186.83361933999998, 326.34153052, 189.65865341999998, + 327.75404756, 189.65865341999998, 327.75404756), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(157.17076149999997, 352.47309576), + CubicToCommand(157.17076149999997, 352.47309576, 151.52069333999998, + 377.89840248, 152.22695185999999, 382.84221212), + CubicToCommand(152.22695185999999, 382.84221212, 150.10817629999997, + 362.36071504, 150.81443481999997, 360.24193948000004), + CubicToCommand(151.52069334, 358.12316392, 157.17076149999997, + 352.47309576000004, 157.17076149999997, 352.47309576000004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(193.89620453999999, 220.75588178), + LineToCommand(193.54307527999995, 226.40594994), + LineToCommand(189.65865341999995, 226.7590792), + CubicToCommand(189.65865341999995, 226.7590792, 214.73083087999998, + 249.00622258, 215.79021865999994, 262.42513446), + CubicToCommand(215.79021865999994, 262.42513446, 217.20273569999995, + 247.9468348, 193.89620453999996, 220.75588178), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(200.93053939919997, 222.9894243495), + CubicToCommand(200.16954584389998, 222.2549154887, 200.5562223836, + 220.93950899520001, 199.83583869319997, 220.48044095720002), + CubicToCommand(198.4074308365, 219.5693674664, 202.12764759059996, + 219.4687256273, 201.72508023419996, 218.2045228765), + CubicToCommand(201.0488377013, 216.0751534387, 201.37901355939997, + 216.0380748664, 201.16360471079997, 213.7851101876), + CubicToCommand(201.06296287169997, 212.7292537002, 202.1011628961, + 210.0101583982, 202.78093672159997, 209.2332740262), + CubicToCommand(205.33406127139995, 206.3164263386, 202.99634557019996, + 201.139551387, 205.89200550219996, 198.4010339757), + CubicToCommand(206.42876197739997, 197.890762195, 207.09264498619996, + 196.9161254374, 207.59585418169996, 196.17808528400002), + CubicToCommand(208.75941509339998, 194.4777678971, 210.84994031259998, + 193.6161325027, 212.52730429759998, 192.1541773663), + CubicToCommand(213.08877982099995, 191.6668589875, 212.73388491469996, + 190.20666949740001, 213.73853765939998, 190.36910895699998), + CubicToCommand(214.99920911759997, 190.57215828149998, 217.19037617589998, + 190.3426242625, 217.11092209239996, 191.8275328008), + CubicToCommand(216.91140406049996, 195.5707029568, 214.56486012779996, + 198.6076145928, 212.30483286379996, 201.5527126212), + CubicToCommand(213.10113934509997, 202.7957276164, 212.29247333969997, + 203.9169130169, 211.79102979049995, 204.90037800599998), + CubicToCommand(209.43389197999997, 209.52637131199998, 209.76759913069995, + 214.54257245029999, 209.46037667449997, 219.5570079423), + CubicToCommand(209.45154844299998, 219.7070878778, 208.90949502889998, + 219.8448082892, 208.92362019929996, 219.9383875431), + CubicToCommand(209.54689334319994, 224.06117165359998, 210.57803078239996, + 227.9438278673, 212.13003388009997, 231.87945347), + CubicToCommand(212.77626042589998, 233.521504529, 213.60787983319997, + 235.1017579675, 213.8727267782, 236.7385120876), + CubicToCommand(214.0687135175, 237.94974544939998, 214.25057508639998, + 239.4205288173, 213.53019139599996, 240.66530945879998), + CubicToCommand(217.12328161649998, 245.7680272658, 214.81205060979997, + 250.4487556071, 216.87609113449997, 256.6832526924), + CubicToCommand(217.24157991859997, 257.7867816299, 220.22905345819999, + 261.1662286481, 219.41155922129997, 260.8819595938), + CubicToCommand(214.97449006939996, 259.3440816665, 214.77673768379998, + 258.6413544391, 214.46421828869995, 257.31182277519997), + CubicToCommand(214.20643392889997, 256.2118251303, 213.6237706499, + 253.7805301752, 213.20531247679997, 252.7176111026), + CubicToCommand(213.09231111359998, 252.4280451094, 212.79038559629998, + 249.1015674802, 212.65266518489997, 248.88615863159998), + CubicToCommand(209.98124233299995, 244.680389145, 212.37899000839997, + 244.9911428938, 209.96005457739997, 240.8277489184), + CubicToCommand(207.43871166099996, 239.62710943439998, 205.73309733519994, + 237.6778359192, 203.75027654029998, 235.62615491859998), + CubicToCommand(203.4006785729, 235.2659630734, 205.41881229379996, + 233.9876351522, 205.09923031349996, 233.6115524903), + CubicToCommand(203.17114455389998, 231.3356344096, 201.13535436999996, + 229.9813836975, 201.72861152679997, 227.3452737716), + CubicToCommand(202.00228670329994, 226.1269778246, 202.24418024639996, + 224.26068968549998, 200.93053939919997, 222.98942434949998), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(194.60246306, 226.05282068), + CubicToCommand(194.60246306, 226.05282068, 195.30872158, 238.05921552, + 199.54627269999997, 240.8842496), + CubicToCommand(203.78382381999995, 243.70928368, 201.66504826, + 242.29676664000002, 196.01498009999997, 240.17799108), + CubicToCommand(190.36491193999998, 238.05921552, 192.48368749999997, + 236.64669848, 192.48368749999997, 236.64669848), + CubicToCommand(192.48368749999997, 236.64669848, 187.53987786, 237.352957, + 191.77742897999997, 240.8842496), + CubicToCommand(196.01498009999995, 244.4155422, 202.37130677999997, + 248.65309332, 199.54627269999997, 248.65309332), + CubicToCommand(196.72123861999998, 248.65309332, 183.30232673999998, + 241.59050812, 183.30232673999998, 236.64669848), + CubicToCommand(183.30232673999998, 231.70288884, 181.53668043999997, + 224.46373901, 181.53668043999997, 224.46373901), + CubicToCommand(181.53668043999997, 224.46373901, 183.47889136999996, + 223.05122197, 191.95399360999997, 223.2277866), + CubicToCommand(191.95399360999997, 223.2277866, 194.42589843, + 224.46373901, 194.60246306, 226.05282068000002), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(193.18994601999998, 258.89384186), + CubicToCommand(193.18994601999998, 258.89384186, 178.14663954399998, + 253.96592303670002, 145.51749592, 259.95322964), + CubicToCommand(145.51749592, 259.95322964, 161.4630476553, 256.2842166286, + 194.60246306, 260.3063589), + CubicToCommand(212.78861994999997, 262.513416775, 193.18994601999998, + 258.89384186, 193.18994601999998, 258.89384186), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(196.89427195739998, 258.7684809727), + CubicToCommand(196.89427195739998, 258.7684809727, 182.3347525676, + 252.5569372893, 149.30833852609996, 255.7015533496), + CubicToCommand(149.30833852609996, 255.7015533496, 165.51344026749996, + 253.4256352689, 198.17966246379996, 260.29753066850003), + CubicToCommand(216.10803499399998, 264.0689511653, 196.89427195739998, + 258.7684809727, 196.89427195739998, 258.7684809727), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(200.05124754179997, 258.9326860786), + CubicToCommand(200.05124754179997, 258.9326860786, 185.99317170119997, + 251.6564576763, 152.82550595569998, 252.3362315018), + CubicToCommand(152.82550595569998, 252.3362315018, 169.15420293809998, + 251.26978113660002, 201.2201053924, 260.553549382), + CubicToCommand(218.81653641819997, 265.6474389575, 200.05124754179997, + 258.9326860786, 200.05124754179997, 258.9326860786), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(202.29361834279996, 259.3264252035), + CubicToCommand(202.29361834279996, 259.3264252035, 190.24484799159998, + 251.7217865894, 160.4513323254, 249.783106952), + CubicToCommand(160.4513323254, 249.783106952, 175.1750568211, + 250.083266823, 203.21528571139999, 260.8696000697), + CubicToCommand(218.60465886219998, 266.7898121136, 202.29361834279996, + 259.3264252035, 202.29361834279996, 259.3264252035), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(405.844386392, 277.8939616943), + CubicToCommand(405.844386392, 277.8939616943, 404.20233533299995, + 279.0310379115, 404.5801836412, 276.9458096312), + CubicToCommand(404.95979759569997, 274.8605813509, 454.7651484261, + 251.6070195799, 461.1461941543, 252.04843115490002), + CubicToCommand(461.1461941543, 252.04843115490002, 407.73892487189994, + 275.3655561927, 405.844386392, 277.8939616943), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(399.8517828498, 279.02220968), + CubicToCommand(399.8517828498, 279.02220968, 398.30507669099995, + 280.2846467845, 398.5187198933, 278.1764651023), + CubicToCommand(398.73236309559996, 276.0682834201, 446.5584244237, + 248.9744409466, 452.9535953223, 248.9144089724), + CubicToCommand(452.9535953223, 248.9144089724, 401.5432720051999, + 276.3543181207, 399.8517828498, 279.02220968), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(394.04986910799994, 281.4499733425), + CubicToCommand(394.04986910799994, 281.4499733425, 392.58791397159996, + 282.8130522861, 392.66207111619997, 280.6942767261), + CubicToCommand(392.7362282608, 278.5772668124, 427.7260409879, + 251.2150461013, 445.05056248349996, 247.9062249351), + CubicToCommand(445.05056248349996, 247.9062249351, 413.21595969449993, + 262.2556324152, 394.04986910799994, 281.4499733425), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(388.9718703492, 284.7393723994), + CubicToCommand(388.9718703492, 284.7393723994, 387.6564638557, + 285.9647309316, 387.7235584151, 284.0595985739), + CubicToCommand(387.79065297449995, 282.1527005699, 419.2791890887, + 257.5272316238, 434.871611564, 254.550351962), + CubicToCommand(434.871611564, 254.550351962, 406.2222347002, + 267.4642890002, 388.9718703492, 284.7393723994), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(333.02913298, 545.9879302400001), + CubicToCommand(333.02913298, 545.9879302400001, 306.19130922, + 541.75037912, 335.85416705999995, 538.92534504), + CubicToCommand(335.85416705999995, 538.92534504, 367.63580046, + 535.39405244, 374.69838566, 526.21269168), + CubicToCommand(374.69838566, 526.21269168, 398.71117533999995, + 509.9687457200001, 403.65498498, 509.26248720000007), + CubicToCommand(408.59879462, 508.55622868, 437.55539394, + 502.19990200000007, 438.26165245999994, 497.96235088000003), + CubicToCommand(438.96791097999994, 493.72479976, 449.56178878, + 489.48724864, 452.38682285999994, 490.8997656800001), + CubicToCommand(455.21185693999996, 492.3122827200001, 455.21185693999996, + 508.55622868, 450.2680472999999, 509.9687457200001), + CubicToCommand(445.32423765999994, 511.38126276000014, 412.13008721999995, + 524.09391612, 400.8299509, 526.21269168), + CubicToCommand(389.52981457999994, 528.33146724, 369.04831749999994, + 541.75037912, 360.5732152599999, 543.8691546800001), + CubicToCommand(352.09811301999997, 545.9879302400001, 333.02913297999993, + 545.9879302400001, 333.02913297999993, 545.9879302400001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(461.92131287999996, 479.95275862000005), + CubicToCommand(461.92131287999996, 479.95275862000005, 456.62437398, + 482.77779269999996, 454.50559841999996, 487.36847308), + CubicToCommand(454.50559841999996, 487.36847308, 443.2054621, + 506.08432386, 418.13328463999994, 511.73439202), + CubicToCommand(418.13328463999994, 511.73439202, 377.52341973999995, + 527.62520872, 363.75137859999995, 531.15650132), + CubicToCommand(363.75137859999995, 531.15650132, 340.09171817999993, + 539.98473282, 327.02593555999994, 538.57221578), + CubicToCommand(327.02593555999994, 538.57221578, 314.66641145999995, + 538.92534504, 325.61341852, 541.75037912), + CubicToCommand(325.61341852, 541.75037912, 361.27947377999993, + 538.21908652, 367.2826712, 535.04092318), + CubicToCommand(367.2826712, 535.04092318, 394.82675348, 525.85956242, + 400.12369237999997, 521.26888204), + CubicToCommand(405.42063127999995, 516.6782016599999, 437.55539394, + 507.84997016, 441.43981579999996, 504.31867755999997), + CubicToCommand(445.32423766, 500.78738496, 462.62757139999997, + 485.95595604, 461.92131287999996, 479.95275862), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(358.24609343659995, 535.588273533), + CubicToCommand(358.24609343659995, 535.588273533, 367.4786579393, + 535.182174884, 365.44286775539996, 537.918926649), + CubicToCommand(363.4070775715, 540.6556784139999, 359.0847754291, + 539.119566133, 359.0847754291, 539.119566133), + LineToCommand(358.24609343659995, 535.588273533), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(345.529908784, 537.971896038), + CubicToCommand(345.529908784, 537.971896038, 354.76070764039997, + 537.583453852, 352.7266831028, 540.320205617), + CubicToCommand(350.6908929189, 543.0569573820001, 346.36859077649996, + 541.503188638, 346.36859077649996, 541.503188638), + LineToCommand(345.529908784, 537.971896038), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(327.1159835213, 540.178953913), + CubicToCommand(327.1159835213, 540.178953913, 336.34854802399997, + 539.772855264, 334.31275784009995, 542.509607029), + CubicToCommand(332.2769676562, 545.246358794, 327.95466551379997, + 543.710246513, 327.95466551379997, 543.710246513), + LineToCommand(327.1159835213, 540.178953913), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(313.6370396671, 541.150059378), + CubicToCommand(313.6370396671, 541.150059378, 322.86960416979997, + 540.743960729, 320.83381398589995, 543.480712494), + CubicToCommand(318.798023802, 546.2174642589999, 314.47572165959997, + 544.663695515, 314.47572165959997, 544.663695515), + LineToCommand(313.6370396671, 541.1500593779999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(387.4375237145, 522.522490913), + CubicToCommand(387.4375237145, 522.522490913, 399.72642196249996, + 521.992797023, 397.016154892, 525.630028401), + CubicToCommand(394.3076534678, 529.284916242, 388.5534121761, + 527.2191100709999, 388.5534121761, 527.2191100709999), + LineToCommand(387.4375237145, 522.522490913), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(405.650165299, 514.718334267), + CubicToCommand(405.650165299, 514.718334267, 416.1716516007, + 508.89170147699997, 415.22879647649995, 517.825871755), + CubicToCommand(414.7520719755, 522.3282698200001, 406.76605376059996, + 519.414953425, 406.76605376059996, 519.414953425), + LineToCommand(405.650165299, 514.718334267), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(421.7740473106, 509.73921170100004), + CubicToCommand(421.7740473106, 509.73921170100004, 434.41430917229997, + 503.2063203910001, 431.35267848809997, 512.8644056520001), + CubicToCommand(429.9790056667, 517.172582624, 422.88817012589993, + 514.453487322, 422.88817012589993, 514.453487322), + LineToCommand(421.77404731059994, 509.73921170100004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(438.5724062088, 501.228796535), + CubicToCommand(438.5724062088, 501.228796535, 446.26885843049996, + 492.577129665, 448.1510373863, 504.33633402299995), + CubicToCommand(448.8678897841, 508.82107562499993, 439.68829467039996, + 505.925415693, 439.68829467039996, 505.925415693), + LineToCommand(438.5724062088, 501.228796535), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(372.28651281419997, 530.4502428), + CubicToCommand(372.28651281419997, 530.4502428, 381.51731167059995, + 530.0441441510001, 379.483287133, 532.780895916), + CubicToCommand(377.44749694909996, 535.517647681, 373.12342916039995, + 533.9815354, 373.12342916039995, 533.9815354), + LineToCommand(372.28651281419997, 530.4502428), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(435.14352109419997, 316.10607891890004), + CubicToCommand(435.14352109419997, 316.10607891890004, 433.8616618804, + 317.28023370840003, 433.8598962341, 315.3892265211), + CubicToCommand(433.8598962341, 313.4999849801, 464.7304561433, + 290.0804524569, 480.3440663742, 287.6367979777), + CubicToCommand(480.3440663742, 287.6367979777, 451.91892659049995, + 299.5178319304, 435.14352109419997, 316.10607891890004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(440.38042801999995, 428.74901592000003), + CubicToCommand(440.38042801999995, 428.74901592000003, 479.22641226630003, + 468.29949304, 495.47035822629994, 474.65581972), + CubicToCommand(495.47035822629994, 474.65581972, 511.71253853999997, + 494.43105828, 504.64995333999997, 540.33786208), + CubicToCommand(504.64995333999997, 540.33786208, 499.00165082629997, + 553.75677396, 493.3515826663, 517.0313309200001), + CubicToCommand(493.3515826663, 517.0313309200001, 499.00165082629997, + 472.53704416000005, 479.2264122663, 500.78738496000005), + CubicToCommand(479.2264122663, 500.78738496000005, 464.3932177, + 483.30748659000005, 475.69335401999996, 483.8371804800001), + CubicToCommand(475.69335401999996, 483.8371804800001, 481.34518782629993, + 487.36847308000006, 482.05144634629994, 484.54343900000015), + CubicToCommand(482.75770486629995, 481.7184049200001, 468.63076881999996, + 457.7056152400001, 438.26165245999994, 432.2803085200001), + CubicToCommand(407.8925360999999, 406.85500180000014, 440.38042801999995, + 428.74901592000015, 440.38042801999995, 428.74901592000015), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(337.26668409999996, 497.25609236), + CubicToCommand(337.26668409999996, 497.25609236, 336.91355483999996, + 494.07792901999994, 340.09171818, 495.49044605999995), + CubicToCommand(343.26988151999996, 496.90296309999997, 509.59376297999995, + 507.84997016, 565.38818606, 550.22548136), + CubicToCommand(565.38818606, 550.22548136, 485.58273894629997, + 509.2624872, 337.26668409999996, 497.25609236), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(355.62940562, 489.48724864), + CubicToCommand(355.62940562, 489.48724864, 355.27627636, 486.3090853, + 358.45443969999997, 487.72160234), + CubicToCommand(361.63260304, 489.13411938, 602.8198876199999, + 487.36847308000006, 644.4891402999999, 544.5754132), + CubicToCommand(644.4891402999999, 544.5754132, 605.6449216999999, + 500.08112644000005, 355.62940562, 489.48724864), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(376.1109027, 482.42466344), + CubicToCommand(376.1109027, 482.42466344, 375.75777344, + 479.24650010000005, 378.93593677999996, 480.65901714000006), + CubicToCommand(382.11410012, 482.07153418000007, 688.2771685399999, + 459.11813228000005, 729.94642122, 516.3250724), + CubicToCommand(729.94642122, 516.3250724, 712.99621674, + 471.12452712000004, 376.1109027, 482.42466344), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(393.76736569999997, 473.9495612), + CubicToCommand(393.76736569999997, 473.9495612, 393.41423643999997, + 470.77139786, 396.59239978, 472.1839149), + CubicToCommand(399.77056312, 473.59643194, 615.53254098, 405.44248476, + 657.20179366, 462.64942487999997), + CubicToCommand(657.20179366, 462.64942487999997, 633.5421332399999, + 419.2145259, 393.76736569999997, 473.9495612), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(291.3598803, 514.20629684), + CubicToCommand(291.3598803, 514.20629684, 291.00675104, + 511.02813349999997, 294.18491437999995, 512.44065054), + CubicToCommand(297.36307772, 513.85316758, 328.79158186, + 517.7375894400001, 332.32287446, 586.2446658800001), + CubicToCommand(332.32287446, 586.2446658800001, 319.6102211, 512.08752128, + 291.3598803, 514.20629684), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(275.82219286, 517.03133092), + CubicToCommand(275.82219286, 517.03133092, 275.46906359999997, + 513.85316758, 278.64722694, 515.26568462), + CubicToCommand(281.82539027999997, 516.67820166, 306.89756774, + 508.55622868, 301.95375809999996, 577.06330512), + CubicToCommand(301.95375809999996, 577.06330512, 304.07253366, + 514.9125553599999, 275.82219286, 517.03133092), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(261.69702245999997, 517.7375894400001), + CubicToCommand(261.69702245999997, 517.7375894400001, 261.34389319999997, + 514.5594261, 264.52205654, 515.97194314), + CubicToCommand(267.70021987999996, 517.38446018, 294.89117289999996, + 518.4438479600001, 272.99715878, 557.9943250800001), + CubicToCommand(272.99715878, 557.9943250800001, 289.94736326, + 515.6188138800001, 261.69702245999997, 517.7375894400001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(345.2579992538, 439.448832498), + CubicToCommand(345.2579992538, 439.448832498, 344.47405229659995, + 442.98012509800003, 347.2090384153, 440.84369307500003), + CubicToCommand(375.5794431637, 418.5471115986, 432.86054042829994, + 314.547013236, 531.1893828753, 304.59936198180003), + CubicToCommand(531.1893828753, 304.59936198180003, 463.5439418297, + 283.2173852888, 345.2668274853, 439.448832498), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(365.0332378138, 436.27066915800003), + CubicToCommand(365.0332378138, 436.27066915800003, 362.6266619069, + 434.169550061, 365.92488919529995, 433.07484935499997), + CubicToCommand(369.2231164837, 431.96249218599996, 567.7559177482999, + 303.953135436, 637.1281608753, 318.01827386179997), + CubicToCommand(637.1281608753, 318.01827386179997, 589.2614896823, + 304.75827014879997, 365.0420660453, 436.27066915800003), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(328.23540327549995, 447.058768051), + CubicToCommand(328.23540327549995, 447.058768051, 327.3366893088, + 449.883802131, 330.3577101281, 448.15346875700004), + CubicToCommand(346.2043856706, 439.11335970100004, 352.58719704509997, + 338.0989692317, 429.5287658602, 335.8830831252), + CubicToCommand(429.5287658602, 335.8830831252, 372.30240363089996, + 309.903363467, 328.2354032755, 447.058768051), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(293.0584320406, 466.516190277), + CubicToCommand(293.0584320406, 466.516190277, 291.1568309755, + 468.79387400400003, 294.60513819939996, 468.33480596600003), + CubicToCommand(312.6959501892, 465.986496387, 350.48960924069996, + 393.1218048786, 428.95316516639997, 402.08952243630006), + CubicToCommand(428.95316516639997, 402.08952243630006, 372.51251554059996, + 376.75956061650004, 293.0584320406, 466.5161902770001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(312.90076516, 455.710434921), + CubicToCommand(312.90076516, 455.710434921, 311.46882601069996, + 458.305934982, 314.76881894539997, 457.19357781300005), + CubicToCommand(332.07038703909996, 451.419914412, 355.1721032283, + 372.6456047375, 433.8987404527, 366.3828573114), + CubicToCommand(433.8987404527, 366.3828573114, 373.6442948189, + 352.35832875050005, 312.90076516, 455.710434921), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(280.6282820886, 475.55629933299997), + CubicToCommand(280.6282820886, 475.55629933299997, 279.08687286869997, + 477.392571485, 281.8801253153, 477.039442225), + CubicToCommand(296.533223959, 475.132544221, 327.1459995084, + 416.106988412, 390.7022037232, 423.3708572902), + CubicToCommand(390.7022037232, 423.3708572902, 344.98432407729996, + 402.85404728419996, 280.6282820886, 475.55629933299997), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(267.21113585489996, 485.991268966), + CubicToCommand(267.21113585489996, 485.991268966, 265.43666132339996, + 487.845197581, 268.2458045867, 487.633320025), + CubicToCommand(275.60501836509997, 487.08596967200003, 329.70088970449996, + 428.678390068, 362.3088455729, 456.04590771799997), + CubicToCommand(362.3088455729, 456.04590771799997, 341.09460527839997, + 422.9912433357, 267.21113585489996, 485.991268966), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(389.9800543865, 429.6283077774), + CubicToCommand(389.9800543865, 429.6283077774, 387.8595131802, + 427.2358570409, 391.27074183179997, 426.5560832154), + CubicToCommand(394.6819704834, 425.8780750362, 607.7919476008, + 323.9773301243, 674.8158811487999, 346.6941354201), + CubicToCommand(674.8158811487999, 346.6941354201, 629.0150161268, + 327.49449755390003, 389.9818200328, 429.6283077774), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(330.91035741999997, 543.16289616), + CubicToCommand(330.91035741999997, 543.16289616, 330.55722815999997, + 539.98473282, 333.7353915, 541.39724986), + CubicToCommand(336.91355483999996, 542.8097669, 364.10450785999996, + 543.86915468, 342.21049374, 583.4196318), + CubicToCommand(342.21049374, 583.4196318, 359.16069822, 541.0441206, + 330.91035741999997, 543.16289616), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(349.27307894, 540.33786208), + CubicToCommand(349.27307894, 540.33786208, 348.91994968, 537.15969874, + 352.09811301999997, 538.57221578), + CubicToCommand(355.27627636, 539.98473282, 386.70478049999997, + 543.86915468, 390.2360731, 612.3762311199999), + CubicToCommand(390.2360731, 612.3762311199999, 377.52341974, 538.21908652, + 349.27307894, 540.3378620799999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(361.27947378, 537.512828), + CubicToCommand(361.27947378, 537.512828, 360.92634452, 534.3346646599999, + 364.10450786, 535.7471817), + CubicToCommand(367.2826712, 537.1596987400001, 410.71757018, 543.16289616, + 452.38682286, 600.36983628), + CubicToCommand(452.38682286, 600.36983628, 389.52981458, 535.39405244, + 361.27947378, 537.512828), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(374.7425268175, 533.928566011), + CubicToCommand(374.7425268175, 533.928566011, 373.97093938439997, + 530.821028523, 377.3080108914, 531.809790451), + CubicToCommand(380.64508239839995, 532.7808959160001, 416.72429889259996, + 529.4261679460001, 483.99012598369995, 589.7759584800001), + CubicToCommand(483.99012598369995, 589.7759584800001, 402.4614080812, + 528.0842767580001, 374.7425268175, 533.928566011), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(393.1052483375, 526.159722291), + CubicToCommand(393.1052483375, 526.159722291, 392.33366090439995, + 523.052184803, 395.67073241139997, 524.040946731), + CubicToCommand(399.0078039184, 525.012052196, 460.5123271325999, + 532.957460546, 551.7927095499999, 594.71976812), + CubicToCommand(551.7927095499999, 594.71976812, 420.82412960119996, + 520.315433038, 393.1052483375, 526.159722291), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(321.72899665999995, 505.7311946), + CubicToCommand(321.72899665999995, 505.7311946, 321.3758674, 502.55303126, + 324.55403074, 503.9655483), + CubicToCommand(327.73219408, 505.37806534000003, 422.0177065, + 509.96874572, 475.69335401999996, 557.28806656), + CubicToCommand(475.69335401999996, 557.28806656, 414.07053250369995, + 513.553007709, 321.72899665999995, 505.7311946), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(304.07253366, 512.7937798), + CubicToCommand(304.07253366, 512.7937798, 303.7194044, 509.61561645999996, + 306.89756774, 511.02813349999997), + CubicToCommand(310.07573107999997, 512.44065054, 353.51063006, + 518.44384796, 395.17988274, 575.65078808), + CubicToCommand(395.17988274, 575.65078808, 332.32287446, 510.67500424, + 304.07253366, 512.7937798), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(412.3119487889, 518.020092848), + CubicToCommand(412.3119487889, 518.020092848, 411.3143586294, + 514.983181212, 414.71499340319997, 515.724752658), + CubicToCommand(418.11562817699996, 516.448667641, 480.036843918, + 519.856365, 575.5936216739999, 574.76796493), + CubicToCommand(575.5936216739999, 574.76796493, 438.46823307709997, + 514.02973221, 412.31194878889994, 518.020092848), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(427.14337770889995, 513.782541728), + CubicToCommand(427.14337770889995, 513.782541728, 426.1457875494, + 510.745630092, 429.5464223232, 511.487201538), + CubicToCommand(432.94705709699997, 512.211116521, 494.86827283799994, + 515.61881388, 590.4250505939999, 570.53041381), + CubicToCommand(590.4250505939999, 570.53041381, 454.3590497770999, + 509.08592257, 427.14337770889995, 513.782541728), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(444.0935821889, 504.954310228), + CubicToCommand(444.0935821889, 504.954310228, 443.0959920294, + 501.91739859200004, 446.4966268032, 502.658970038), + CubicToCommand(449.897261577, 503.382885021, 525.943647718, + 511.02813349999997, 684.3644963392001, 571.58980159), + CubicToCommand(684.3644963392001, 571.58980159, 471.32161378119997, + 500.25769106999996, 444.09534783519996, 504.95431022799994), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(247.57185205999997, 517.03133092), + CubicToCommand(247.57185205999997, 517.03133092, 256.75321282, + 516.3250724, 254.63443725999997, 519.15010648), + CubicToCommand(252.51566169999998, 521.97514056, 248.27811057999998, + 520.56262352, 248.27811057999998, 520.56262352), + LineToCommand(247.57185205999997, 517.03133092), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(301.95375809999996, 541.75037912), + CubicToCommand(301.95375809999996, 541.75037912, 311.13511886, + 541.0441206, 309.01634329999996, 543.86915468), + CubicToCommand(306.89756774, 546.69418876, 302.66001661999996, + 545.28167172, 302.66001661999996, 545.28167172), + LineToCommand(301.95375809999996, 541.75037912), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(286.41607065999995, 541.0441206), + CubicToCommand(286.41607065999995, 541.0441206, 295.59743141999996, + 540.33786208, 293.47865586, 543.16289616), + CubicToCommand(291.3598803, 545.98793024, 287.12232917999995, 544.5754132, + 287.12232917999995, 544.5754132), + LineToCommand(286.41607065999995, 541.0441206), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(118.02638302899996, 520.174181334), + CubicToCommand(118.02638302899996, 520.174181334, 126.94289684399996, + 522.50483445, 124.01192398599997, 524.482358306), + CubicToCommand(121.08095112799998, 526.459882162, 117.53200206499997, + 523.7407868600001, 117.53200206499997, 523.7407868600001), + LineToCommand(118.02638302899996, 520.174181334), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(121.55767562899996, 503.22397685400006), + CubicToCommand(121.55767562899996, 503.22397685400006, 130.47418944399996, + 505.55462997, 127.54321658599997, 507.532153826), + CubicToCommand(124.61224372799998, 509.5096776820001, 121.06329466499997, + 506.79058238000005, 121.06329466499997, 506.79058238000005), + LineToCommand(121.55767562899996, 503.22397685400006), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(108.84502226899997, 495.455133134), + CubicToCommand(108.84502226899997, 495.455133134, 117.76153608399997, + 497.78578625, 114.83056322599998, 499.763310106), + CubicToCommand(111.89959036799996, 501.740833962, 108.35064130499995, + 499.02173866000004, 108.35064130499995, 499.02173866000004), + LineToCommand(108.84502226899997, 495.455133134), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(249.69062762, 627.91391856), + LineToCommand(239.80300833999996, 631.44521116), + CubicToCommand(236.27171574, 631.44521116, 216.49647717999997, + 637.8015378399999, 206.60885789999998, 655.45800084), + CubicToCommand(206.60885789999998, 655.45800084, 228.50287201999998, + 638.5077963599999, 249.69062762, 627.91391856), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(404.5660584708, 791.501048255), + CubicToCommand(404.8150145991, 791.94245983, 404.9121251456, + 792.684031276, 405.42769386519996, 792.719344202), + CubicToCommand(406.5894891306, 792.789970054, 408.76653101849996, + 793.319663944, 408.545825231, 792.207306775), + CubicToCommand(407.0485571686, 784.597371222, 405.5053823024, 775.8750785, + 398.2079661445, 772.69691516), + CubicToCommand(397.0797181588, 772.202534196, 394.5336561942, + 772.926449179, 394.402998368, 774.4978743859999), + CubicToCommand(394.1787612879, 777.1816567619999, 393.97218067079996, + 779.5652792669999, 394.5177653775, 782.1431228649999), + CubicToCommand(395.04745926749996, 784.6679970739999, 398.8577239829, + 784.6679970739999, 400.47682163999997, 782.2314051799999), + CubicToCommand(402.1294665768, 785.1800345009999, 402.89222577839996, + 788.4464801559999, 404.5660584708, 791.5010482549999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(385.00622875939996, 799.852555254), + CubicToCommand(386.3269321918, 802.342116537, 386.1026951117, + 805.6262186549999, 388.60108462619996, 806.6326370459999), + CubicToCommand(389.9076628882, 807.1446744729999, 393.17410854319996, + 805.4319975619999, 392.43606838979997, 803.525099558), + CubicToCommand(391.0200200572, 799.8878681799999, 390.3349492928, + 795.9328204679999, 387.88246658209994, 792.7370006649999), + CubicToCommand(387.5293373221, 792.2779326269999, 387.9530924341, + 791.3421400879998, 387.67058902609995, 790.7594768089999), + CubicToCommand(386.6217951239, 788.6053883229998, 384.60013011039996, + 787.2988100609999, 382.11410012, 787.8814733399998), + CubicToCommand(380.1454044955, 791.7658951999999, 382.1723664479, + 795.5267218189998, 384.8596801165, 798.545976992), + CubicToCommand(385.0998080133, 798.8108239369999, 384.8067107275, + 799.4817695309999, 385.00622875939996, 799.852555254), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(315.0831039868, 790.688850957), + CubicToCommand(314.8924141864, 790.017905363, 314.825319627, + 789.205708065, 315.11665126649996, 788.6407012489999), + CubicToCommand(316.0542094518, 786.82208556, 317.45436696769997, + 784.844561704, 316.8593441646, 783.0259460149999), + CubicToCommand(316.24489925219996, 781.1720173999998, 314.27443798139996, + 781.489833734, 313.1267678864, 782.4962521249998), + CubicToCommand(311.12275933589996, 784.2442419619999, 311.0362426672, + 787.4753746909998, 309.8161810739, 789.8766536589999), + CubicToCommand(309.4701143991, 790.5475992529999, 309.5601623604, + 791.5716741069998, 308.7815123421, 792.260276164), + CubicToCommand(307.9445959959, 793.0018476099999, 307.1677116239, + 795.685629986, 307.3372136687, 796.6920483769999), + CubicToCommand(307.43079292259995, 797.2570551929999, 307.1076796497, + 814.7369535629999, 307.4996531283, 814.2778855249999), + CubicToCommand(308.592588188, 812.9889637259998, 313.977809403, + 795.7915687639999, 314.09963899769997, 794.2201435569998), + CubicToCommand(314.20028083679995, 792.9312217579998, 315.49273392839996, + 792.1190244599999, 315.0831039868, 790.6888509569999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(269.81546414739995, 778.70011258), + CubicToCommand(274.4661765016, 774.28599683, 279.3923296786, + 769.0596837820001, 278.611914014, 762.509136009), + CubicToCommand(278.4070990432, 760.778802635, 275.2730768607, + 761.714595174, 274.94290100259997, 763.1977380659999), + CubicToCommand(273.52685267, 769.6070341349999, 269.92669986429996, + 774.303653293, 265.39252016589995, 778.52354795), + CubicToCommand(261.5151608911, 782.143122865, 258.2275274805, + 793.337320407, 257.8126006, 794.2378000199999), + CubicToCommand(264.3401949711, 784.9505004819999, 268.32525867019996, + 780.1126296199999, 269.8154641474, 778.7001125799999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(245.84858127119998, 768.176860632), + CubicToCommand(246.7720142861, 767.505915038, 246.23878910349998, + 766.640748351, 246.6219343506, 766.058085072), + CubicToCommand(248.30459527449997, 763.4802414740001, 250.60170111079998, + 761.290840062, 250.63348274419997, 758.2186155), + CubicToCommand(250.63877968309998, 757.724234536, 249.97136538169997, + 757.1768841830001, 249.37987387119998, 757.582982832), + CubicToCommand(248.89078984609995, 757.900799166, 248.28693881149997, + 758.130333185, 248.09624901109999, 758.359867204), + CubicToCommand(244.51198702209996, 762.685700639, 242.0400822021, + 767.3999762599999, 239.49225459119998, 772.3614423629999), + CubicToCommand(239.16914131829998, 772.997075031, 237.14924195109998, + 780.924826918, 237.70188924299998, 781.1190480109999), + CubicToCommand(238.12387870869998, 781.2779561779998, 241.1607903447, + 773.897554644, 241.53157606769997, 773.6856770879999), + CubicToCommand(243.76158734459995, 772.4850376039999, 243.78277510019996, + 769.6246905979999, 245.84858127119998, 768.176860632), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(275.3931408091, 802.677589334), + CubicToCommand(276.17708776629996, 801.141477053, 278.95974633509996, + 799.022701493, 278.7602283032, 797.451276286), + CubicToCommand(278.5518820398, 795.8092252270001, 279.3782045082, + 793.2666945550001, 277.7679350826, 794.484990502), + CubicToCommand(275.5485176835, 796.144698024, 269.4588035948, + 798.5283205290001, 268.9820790938, 808.7867255320001), + CubicToCommand(268.93617229, 809.793143923, 274.1872043862, + 805.0435553760001, 275.3931408091, 802.6775893340001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(300.89437031999995, 772.3437859), + CubicToCommand(301.60062883999996, 771.1608028789999, 302.8507064204, + 772.008313103, 303.66113807209996, 771.531588602), + CubicToCommand(304.80527687449995, 770.878299471, 305.8840867638, + 769.889537543, 306.39965548339995, 768.741867448), + CubicToCommand(308.11233239439997, 764.9633843660001, 311.23576069909996, + 761.7499081000001, 311.48824812, 757.51235698), + CubicToCommand(308.8556694867, 755.0404521600001, 307.656795649, + 758.642370612, 306.54443848, 760.3373910600001), + CubicToCommand(304.2084884251, 757.4240746650001, 302.4446077714, + 760.7434897090001, 300.14926758139995, 761.6616257850001), + CubicToCommand(300.0256723404, 761.714595174, 299.6495896785, + 761.0966189690001, 299.51716620599996, 761.1495883580001), + CubicToCommand(297.4390005109, 761.92647273, 296.23836102689995, + 763.833370734, 294.47624601949997, 765.2105748480001), + CubicToCommand(294.1743205022, 765.440108867, 293.4592337507, + 765.1222925330001, 293.19968374459995, 765.369483015), + CubicToCommand(292.04848235699995, 766.4288707950001, 290.3305085071, + 767.011534074, 289.83612754309996, 768.212173558), + CubicToCommand(287.8762601501, 772.979418568, 282.3321307681, + 776.6872757980001, 279.0003562, 790.0002489), + CubicToCommand(279.6730674403, 791.606987033, 286.9669523056, + 778.276357468, 287.82682205369997, 777.040405058), + CubicToCommand(289.30290236049996, 774.921629498, 289.5112486239, + 779.971377916, 291.7642133027, 778.806051358), + CubicToCommand(291.854261264, 778.753081969, 292.18267147579996, + 779.176837081, 292.41926808, 779.4063711), + CubicToCommand(292.76180346219996, 778.911990136, 293.14671435559995, + 778.505891487, 293.83178511999995, 778.70011258), + CubicToCommand(293.83178511999995, 777.9938540600001, 293.5951885158, + 777.0050921320001, 293.95008342209996, 776.740245187), + CubicToCommand(296.1341878952, 775.0099118129999, 295.985873606, + 773.120670272, 297.36307772, 770.9312688599999), + CubicToCommand(298.17174372539995, 772.326129437, 300.04509444969995, + 771.0548641009999, 300.89437031999995, 772.3437859), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(406.48001905999996, 868.3949446199999), + CubicToCommand(406.48001905999996, 868.3949446199999, 419.54580167999995, + 832.3757601, 411.77695796, 812.6005215399999), + CubicToCommand(411.77695796, 812.6005215399999, 431.90532578, + 850.7384816199999, 423.7833528, 870.51372018), + CubicToCommand(423.7833528, 870.51372018, 423.07709428, 852.1509986599999, + 416.01450908, 843.32276716), + CubicToCommand(416.01450908, 843.32276716, 408.95192388, 865.9230398, + 406.48001905999996, 868.3949446199999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(380.34845382, 863.80426424), + CubicToCommand(380.34845382, 863.80426424, 389.88294383999994, + 848.2665767999999, 375.75777344, 815.77868488), + CubicToCommand(375.75777344, 815.77868488, 374.3452564, 851.7978694, + 362.33886155999994, 871.2199787), + CubicToCommand(362.33886155999994, 871.2199787, 387.41103902, + 835.55392344, 380.34845382, 863.80426424), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(362.69199082, 860.27297164), + CubicToCommand(362.69199082, 860.27297164, 362.33886156, 824.96004564, + 363.04512007999995, 819.66310674), + CubicToCommand(363.04512007999995, 819.66310674, 356.33566413999995, + 848.9728353200001, 338.32607188, 865.9230398), + CubicToCommand(338.32607188, 865.9230398, 363.75137859999995, 844.7352842, + 362.69199082, 860.27297164), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(345.74178634, 803.77229004), + CubicToCommand(345.74178634, 803.77229004, 356.33566413999995, + 827.78507972, 338.67920114, 860.27297164), + CubicToCommand(338.67920114, 860.27297164, 349.97933745999995, + 838.73208678, 341.50423522, 826.37256268), + CubicToCommand(341.50423522, 826.37256268, 346.0949156, 820.3693652600001, + 345.74178634, 803.77229004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(311.84137738, 859.5667131199999), + CubicToCommand(311.84137738, 859.5667131199999, 310.07573107999997, + 832.0226308399999, 313.25389442, 828.13820898), + CubicToCommand(313.25389442, 828.13820898, 313.60702368, 816.83807266, + 312.90076516, 815.07242636), + CubicToCommand(312.90076516, 815.07242636, 319.96335036, 804.1254193, + 320.31647962, 817.1912019199999), + CubicToCommand(320.31647962, 817.1912019199999, 322.78838443999996, + 830.96324306, 327.73219408, 839.0852160399999), + CubicToCommand(327.73219408, 839.0852160399999, 334.08852076, + 848.6197060599999, 333.7353915, 859.9198423799999), + CubicToCommand(333.7353915, 859.9198423799999, 316.07892849999996, + 806.5973241199999, 311.84137738, 859.5667131199999), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(305.4850507, 810.83487524), + CubicToCommand(305.4850507, 810.83487524, 293.83178511999995, + 829.90385528, 290.65362178, 863.45113498), + CubicToCommand(290.65362178, 863.45113498, 288.18171695999996, + 852.5041279200001, 294.89117289999996, 827.0788212), + CubicToCommand(294.89117289999996, 827.0788212, 302.30688735999996, + 799.88786818, 305.4850507, 810.83487524), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(266.99396135999996, 845.79467198), + CubicToCommand(266.99396135999996, 845.79467198, 275.82219286, + 836.2601819600001, 278.29409768, 827.43195046), + CubicToCommand(278.29409768, 827.43195046, 284.65042436, 799.53473892, + 273.35028803999995, 814.7192971000001), + CubicToCommand(273.35028803999995, 814.7192971000001, 273.70341729999996, + 828.8444675000001, 259.22511763999995, 841.91025012), + CubicToCommand(259.22511763999995, 841.91025012, 267.70021987999996, + 837.6726990000001, 266.99396135999996, 845.79467198), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(256.75321282, 836.9664404800001), + CubicToCommand(256.75321282, 836.9664404800001, 262.75641024, + 806.2441948600001, 264.16892728, 804.83167782), + CubicToCommand(264.16892728, 804.83167782, 267.34709061999996, + 798.8284804000001, 262.40328098, 804.47854856), + CubicToCommand(262.40328098, 804.47854856, 246.86559353999996, + 838.3789575200001, 239.80300833999996, 850.0322231000001), + CubicToCommand(239.80300833999996, 850.0322231000001, 253.92817873999996, + 833.7882771400001, 256.75321282, 836.9664404800001), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(246.51246427999996, 807.6567119), + CubicToCommand(246.51246427999996, 807.6567119, 266.99396135999996, + 768.10623478, 228.50287201999998, 813.6599093199999), + CubicToCommand(228.50287201999998, 813.6599093199999, 247.92498131999997, + 796.3565755799999, 246.51246428, 807.6567119), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(219.32151125999997, 781.87827592), + CubicToCommand(219.32151125999997, 781.87827592, 227.79661349999998, + 748.6841254799999, 232.38729387999996, 749.03725474), + LineToCommand(235.21232795999998, 751.86228882), + CubicToCommand(235.21232795999998, 751.86228882, 224.61845015999998, + 768.8124933, 225.67783794, 786.1158270399999), + CubicToCommand(225.67783794, 786.1158270399999, 224.61845015999998, + 769.1656225599999, 219.32151125999997, 781.87827592), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(802.51448415, 761.7499081), + CubicToCommand(802.51448415, 761.7499081, 781.32672855, 744.0934451, + 776.9126128, 737.91368305), + CubicToCommand(776.9126128, 737.91368305, 800.74883785, 770.5781396, + 800.74883785, 782.9376637), + CubicToCommand(800.74883785, 782.9376637, 805.1629536, 769.69531645, + 802.51448415, 761.7499081), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(812.2255388, 722.9056895), + CubicToCommand(812.2255388, 722.9056895, 775.1469665, 696.4209950000001, + 768.96720445, 683.17864775), + CubicToCommand(768.96720445, 683.17864775, 815.7568314, 735.2652136, + 815.7568314, 743.21062195), + CubicToCommand(815.7568314, 743.21062195, 816.6396545499999, + 727.3198052500001, 812.2255388, 722.9056895), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(842.2415258999999, 450.99615930000004), + CubicToCommand(842.2415258999999, 450.99615930000004, 821.0537703, + 436.87098890000004, 818.40530085, 440.4022815), + CubicToCommand(818.40530085, 440.4022815, 836.944587, 451.87898244999997, + 841.35870275, 466.886976), + CubicToCommand(841.35870275, 466.886976, 838.7102333, 450.99615930000004, + 842.2415258999999, 450.99615930000004), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(857.24951945, 593.13068645), + LineToCommand(826.3507092, 571.94293085), + CubicToCommand(826.3507092, 571.94293085, 859.8979889, 601.95891795, + 860.78081205, 609.0215031500001), + LineToCommand(857.24951945, 593.13068645), + CloseCommand() + ], + ), + Path( + commands: const [ + MoveToCommand(167.32322772499998, 553.4036447), + LineToCommand(206.16744632499996, 561.790464625) + ], + ), + Path( + commands: const [ + MoveToCommand(256.0469543, 839.4383452999999), + CubicToCommand(256.0469543, 839.4383452999999, 255.16413114999997, + 833.25858325, 239.27331445, 851.7978694) + ], + ), + Path( + commands: const [ + MoveToCommand(265.75800895, 848.2665767999999), + CubicToCommand(265.75800895, 848.2665767999999, 269.28930155, + 836.7898758499999, 257.8126006, 844.7352842) + ], + ), + Path( + commands: const [ + MoveToCommand(361.10290914999996, 863.27457035), + CubicToCommand(361.10290914999996, 863.27457035, 363.75137859999995, + 843.85246105, 343.44644615, 866.80586295) + ], + ), +]; diff --git a/packages/vector_graphics_compiler/test/parsers_test.dart b/packages/vector_graphics_compiler/test/parsers_test.dart new file mode 100644 index 00000000000..56c03891fe3 --- /dev/null +++ b/packages/vector_graphics_compiler/test/parsers_test.dart @@ -0,0 +1,226 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/numbers.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/src/svg/parsers.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('Colors', () { + final SvgParser parser = SvgParser( + '', + const SvgTheme(), + 'test_key', + true, + null, + ); + expect(parser.parseColor('null', attributeName: 'foo', id: null), null); + expect(parser.parseColor('red', attributeName: 'foo', id: null), + const Color.fromARGB(255, 255, 0, 0)); + expect(parser.parseColor('#ABCDEF', attributeName: 'foo', id: null), + const Color.fromARGB(255, 0xAB, 0xCD, 0xEF)); + // RGBA in svg/css, ARGB in this library. + expect(parser.parseColor('#ABCDEF88', attributeName: 'foo', id: null), + const Color.fromARGB(0x88, 0xAB, 0xCD, 0xEF)); + }); + + test('Colors - mapped', () async { + final TestColorMapper mapper = TestColorMapper(); + final SvgParser parser = SvgParser( + '', + const SvgTheme(), + 'test_key', + true, + mapper, + ) + ..enableMaskingOptimizer = false + ..enableClippingOptimizer = false + ..enableOverdrawOptimizer = false; + final VectorInstructions instructions = parser.parse(); + + // TestMapper just always returns this color. + expect(instructions.paints.single.fill!.color, + const Color.fromARGB(255, 255, 0, 255)); + + // TestMapper should have gotten the ID/element name/attribute name from the rect. + expect(mapper.lastId, 'rect1'); + expect(mapper.lastElementName, 'rect'); + expect(mapper.lastAttributeName, 'fill'); + expect(mapper.lastColor, const Color.fromARGB(255, 255, 0, 0)); + }); + + test('Multiple matrix translates', () { + final AffineMatrix expected = AffineMatrix.identity + .translated(0.338957, 0.010104) + .translated(-0.5214, 0.125) + .translated(0.987, 0.789); + expect( + parseTransform( + 'translate(0.338957,0.010104), translate(-0.5214,0.125),translate(0.987,0.789)', + ), + expected, + ); + }); + + test('Transform has whitespace in params', () { + expect( + parseTransform('translate( 50 , 1160 )'), + AffineMatrix.identity.translated(50, 1160), + ); + }); + + test('Translate and scale matrix', () { + final AffineMatrix expected = AffineMatrix.identity + .translated(0.338957, 0.010104) + .scaled(0.869768, 1.000000); + expect( + parseTransform( + 'translate(0.338957,0.010104),scale(0.869768,1.000000)', + ), + expected, + ); + }); + + test('SVG Transform parser tests', () { + expect(() => parseTransform('invalid'), throwsStateError); + expect(() => parseTransform('transformunsupported(0,0)'), throwsStateError); + + expect( + parseTransform('skewX(60)'), + AffineMatrix.identity.xSkewed(60.0), + ); + expect( + parseTransform('skewY(60)'), + AffineMatrix.identity.ySkewed(60.0), + ); + expect( + parseTransform('translate(10,0.0)'), + AffineMatrix.identity.translated(10.0, 0.0), + ); + + expect( + parseTransform('scale(10)'), + AffineMatrix.identity.scaled(10.0, 10.0), + ); + expect( + parseTransform('scale(10, 15)'), + AffineMatrix.identity.scaled(10.0, 15.0), + ); + + expect( + parseTransform('rotate(20)'), + AffineMatrix.identity.rotated(radians(20.0)), + ); + expect( + parseTransform('rotate(20, 30)'), + AffineMatrix.identity + .translated(30.0, 30.0) + .rotated(radians(20.0)) + .translated(-30.0, -30.0), + ); + expect( + parseTransform('rotate(20, 30, 40)'), + AffineMatrix.identity + .translated(30.0, 40.0) + .rotated(radians(20.0)) + .translated(-30.0, -40.0), + ); + + expect( + parseTransform('matrix(1.5, 2.0, 3.0, 4.0, 5.0, 6.0)'), + const AffineMatrix(1.5, 2.0, 3.0, 4.0, 5.0, 6.0), + ); + + expect( + parseTransform('matrix(1.5, 2.0, 3.0, 4.0, 5.0, 6.0 )'), + const AffineMatrix(1.5, 2.0, 3.0, 4.0, 5.0, 6.0), + ); + + expect( + parseTransform('rotate(20)\n\tscale(10)'), + AffineMatrix.identity.rotated(radians(20.0)).scaled(10.0, 10.0), + ); + }); + + test('FillRule tests', () { + expect(parseRawFillRule(''), PathFillType.nonZero); + expect(parseRawFillRule(null), isNull); + expect(parseRawFillRule('inherit'), isNull); + expect(parseRawFillRule('nonzero'), PathFillType.nonZero); + expect(parseRawFillRule('evenodd'), PathFillType.evenOdd); + expect(parseRawFillRule('invalid'), PathFillType.nonZero); + }); + + test('Parses pattern units to double correctly', () { + final ViewportNode viewportNode = ViewportNode(SvgAttributes.empty, + width: 100, height: 1000, transform: AffineMatrix.identity); + expect(parsePatternUnitToDouble('25.0', 'width'), 25.0); + expect( + parsePatternUnitToDouble('0.25', 'width', viewBox: viewportNode), 25.0); + expect( + parsePatternUnitToDouble('25%', 'width', viewBox: viewportNode), 25.0); + expect(parsePatternUnitToDouble('25', 'width'), 25.0); + expect( + parsePatternUnitToDouble('0.1%', 'height', viewBox: viewportNode), 1.0); + }); + + test('Point conversion', () { + expect(parseDoubleWithUnits('1pt', theme: const SvgTheme()), 1 + 1 / 3); + }); + + test('Parse a transform with scientific notation', () { + expect( + parseTransform('translate(9e-6,6.5e-4)'), + AffineMatrix.identity.translated(9e-6, 6.5e-4), + ); + + expect( + parseTransform('translate(9E-6,6.5E-4)'), + AffineMatrix.identity.translated(9e-6, 6.5e-4), + ); + }); + + test('Parse a transform with a missing space', () { + expect( + parseTransform('translate(0-70)'), + AffineMatrix.identity.translated(0, -70), + ); + }); + + test('Parse a transform with doubled periods', () { + expect( + parseTransform('matrix(.70711-.70711.70711.70711-640.89 452.68)'), + const AffineMatrix( + 0.70711, -0.70711, // + 0.70711, 0.70711, // + -640.89, 452.68, // + 0.70711, // + ), + ); + }); +} + +class TestColorMapper extends ColorMapper { + String? lastId; + late String lastElementName; + late String lastAttributeName; + late Color lastColor; + + @override + Color substitute( + String? id, + String elementName, + String attributeName, + Color color, + ) { + lastId = id; + lastElementName = elementName; + lastAttributeName = attributeName; + lastColor = color; + return const Color.fromARGB(255, 255, 0, 255); + } +} diff --git a/packages/vector_graphics_compiler/test/path_ops_test.dart b/packages/vector_graphics_compiler/test/path_ops_test.dart new file mode 100644 index 00000000000..e2ae564360a --- /dev/null +++ b/packages/vector_graphics_compiler/test/path_ops_test.dart @@ -0,0 +1,160 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/_initialize_path_ops_io.dart' + as vector_graphics; +import 'package:vector_graphics_compiler/src/svg/path_ops.dart'; + +void main() { + setUpAll(() { + if (!vector_graphics.initializePathOpsFromFlutterCache()) { + fail('error in setup'); + } + }); + test('Path tests', () { + final Path path = Path() + ..lineTo(10, 0) + ..lineTo(10, 10) + ..lineTo(0, 10) + ..close() + ..cubicTo(30, 30, 40, 40, 50, 50); + + expect(path.fillType, FillType.nonZero); + expect(path.verbs.toList(), [ + PathVerb.moveTo, // Skia inserts a moveTo here. + PathVerb.lineTo, + PathVerb.lineTo, + PathVerb.lineTo, + PathVerb.close, + PathVerb.moveTo, // Skia inserts a moveTo here. + PathVerb.cubicTo, + ]); + expect(path.points, + [0, 0, 10, 0, 10, 10, 0, 10, 0, 0, 30, 30, 40, 40, 50, 50]); + + final SvgPathProxy proxy = SvgPathProxy(); + path.replay(proxy); + expect(proxy.toString(), + 'M0.0,0.0L10.0,0.0L10.0,10.0L0.0,10.0ZM0.0,0.0C30.0,30.0 40.0,40.0 50.0,50.0'); + path.dispose(); + }); + + test('Ops test', () { + final Path cubics = Path() + ..moveTo(16, 128) + ..cubicTo(16, 66, 66, 16, 128, 16) + ..cubicTo(240, 66, 16, 66, 240, 128) + ..close(); + + final Path quad = Path() + ..moveTo(55, 16) + ..lineTo(200, 80) + ..lineTo(198, 230) + ..lineTo(15, 230) + ..close(); + + final Path intersection = cubics.applyOp(quad, PathOp.intersect); + + expect(intersection.verbs, [ + PathVerb.moveTo, + PathVerb.lineTo, + PathVerb.cubicTo, + PathVerb.lineTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.lineTo, + PathVerb.lineTo, + PathVerb.close + ]); + expect(intersection.points, [ + 34.06542205810547, 128.0, // move + 48.90797424316406, 48.59233856201172, // line + 57.80497360229492, 39.73065185546875, 68.189697265625, 32.3614387512207, + 79.66168212890625, 26.885154724121094, // cubic + 151.7936248779297, 58.72270584106445, // line + 150.66123962402344, 59.74142837524414, 149.49365234375, + 60.752471923828125, 148.32867431640625, 61.76123809814453, // cubic + 132.3506317138672, 75.59684753417969, 116.86703491210938, + 89.0042953491211, 199.52090454101562, 115.93260192871094, // cubic + 199.36000061035156, 128.0, // line + 34.06542205810547, 128.0, // line + // close + ]); + cubics.dispose(); + quad.dispose(); + intersection.dispose(); + }); + + test('Quad', () { + final Path top = Path() + ..moveTo(87.998, 103.591) + ..lineTo(82.72, 103.591) + ..lineTo(82.72, 106.64999999999999) + ..lineTo(87.998, 106.64999999999999) + ..lineTo(87.998, 103.591) + ..close(); + + final Path bottom = Path() + ..moveTo(116.232, 154.452) + ..lineTo(19.031999999999996, 154.452) + ..cubicTo(18.671999999999997, 142.112, 21.361999999999995, + 132.59199999999998, 26.101999999999997, 125.372) + ..cubicTo(32.552, 115.55199999999999, 42.782, 110.012, 54.30199999999999, + 107.502) + ..cubicTo(56.931999185062395, 106.9278703703336, 59.593157782987156, + 106.50716022812718, 62.27200212186002, 106.24200362009655) + ..lineTo(62.291999999999994, 106.24199999999999) + ..cubicTo(67.10118331429277, 105.77278829340533, 71.940772522921, + 105.69920780785604, 76.76199850891219, 106.021997940542) + ..cubicTo(78.762, 106.142, 80.749, 106.32199999999999, 82.722, 106.562) + ..lineTo(83.362, 106.652) + ..cubicTo(84.112, 106.742, 84.85199999999999, 106.852, 85.592, 106.972) + ..cubicTo(86.852, 107.152, 88.102, 107.372, 89.342, 107.60199999999999) + ..cubicTo(89.542, 107.642, 89.732, 107.67199999999998, 89.922, + 107.71199999999999) + ..cubicTo(91.54899999999999, 108.02599999999998, 93.14, 108.502, 94.672, + 109.13199999999999) + ..cubicTo(98.35184786478965, 110.61003782601773, 101.5939983878398, + 113.00207032444644, 104.09199525642647, 116.08199471003054) + ..cubicTo(104.181, 116.17999999999999, 104.264, 116.28399999999999, + 104.342, 116.392) + ..cubicTo(104.512, 116.612, 104.682, 116.832, 104.842, 117.062) + ..cubicTo(105.102, 117.41199999999999, 105.352, 117.77199999999999, + 105.592, 118.142) + ..cubicTo(107.63018430068513, 121.33505319707416, 109.25008660688327, + 124.77650539945358, 110.41200699229772, 128.38200813032248) + ..cubicTo(112.762, 135.252, 114.50200000000001, 143.862, 116.232, 154.452) + ..close(); + + final Path intersect = bottom.applyOp(top, PathOp.intersect); + // current revision of Skia makes this result in a quad verb getting used. + final Path difference = bottom.applyOp(intersect, PathOp.difference); + + expect(difference.verbs.toList(), [ + PathVerb.moveTo, + PathVerb.lineTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.quadTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.lineTo, + PathVerb.lineTo, + PathVerb.lineTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.lineTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.cubicTo, + PathVerb.close, + ]); + }); +} diff --git a/packages/vector_graphics_compiler/test/path_test.dart b/packages/vector_graphics_compiler/test/path_test.dart new file mode 100644 index 00000000000..127a309aa24 --- /dev/null +++ b/packages/vector_graphics_compiler/test/path_test.dart @@ -0,0 +1,645 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('SVG Path tests', () { + Path path = parseSvgPathData( + 'M22.1595 3.80852C19.6789 1.35254 16.3807 -4.80966e-07 12.8727 ' + '-4.80966e-07C9.36452 -4.80966e-07 6.06642 1.35254 3.58579 3.80852C1.77297 5.60333 ' + '0.53896 7.8599 0.0171889 10.3343C-0.0738999 10.7666 0.206109 11.1901 0.64265 ' + '11.2803C1.07908 11.3706 1.50711 11.0934 1.5982 10.661C2.05552 8.49195 3.13775 6.51338 4.72783 ' + '4.9391C9.21893 0.492838 16.5262 0.492728 21.0173 4.9391C25.5082 9.38548 25.5082 16.6202 ' + '21.0173 21.0667C16.5265 25.5132 9.21893 25.5133 4.72805 21.0669C3.17644 19.5307 2.10538 ' + '17.6035 1.63081 15.4937C1.53386 15.0627 1.10252 14.7908 0.66697 14.887C0.231645 14.983 ' + '-0.0427272 15.4103 0.0542205 15.8413C0.595668 18.2481 1.81686 20.4461 3.5859 ' + '22.1976C6.14623 24.7325 9.50955 26 12.8727 26C16.236 26 19.5991 24.7326 22.1595 22.1976C27.2802 ' + '17.1277 27.2802 8.87841 22.1595 3.80852Z'); + expect( + path.toFlutterString(), + 'Path()\n' + ' ..moveTo(22.1595, 3.80852)\n' + ' ..cubicTo(19.6789, 1.35254, 16.3807, -4.809659999999999e-7, 12.8727, -4.809659999999999e-7)\n' + ' ..cubicTo(9.36452, -4.809659999999999e-7, 6.06642, 1.35254, 3.5857900000000003, 3.80852)\n' + ' ..cubicTo(1.77297, 5.60333, 0.53896, 7.8599, 0.017188900000000007, 10.3343)\n' + ' ..cubicTo(-0.0738999, 10.7666, 0.20610900000000001, 11.1901, 0.6426500000000002, 11.2803)\n' + ' ..cubicTo(1.07908, 11.3706, 1.50711, 11.0934, 1.5982, 10.661)\n' + ' ..cubicTo(2.05552, 8.49195, 3.13775, 6.51338, 4.72783, 4.9391)\n' + ' ..cubicTo(9.21893, 0.49283800000000005, 16.5262, 0.49272800000000005, 21.0173, 4.9391)\n' + ' ..cubicTo(25.5082, 9.38548, 25.5082, 16.6202, 21.0173, 21.0667)\n' + ' ..cubicTo(16.5265, 25.5132, 9.21893, 25.5133, 4.72805, 21.0669)\n' + ' ..cubicTo(3.17644, 19.5307, 2.10538, 17.6035, 1.63081, 15.4937)\n' + ' ..cubicTo(1.53386, 15.0627, 1.10252, 14.7908, 0.6669700000000002, 14.887)\n' + ' ..cubicTo(0.23164500000000002, 14.983, -0.04272720000000001, 15.4103, 0.05422050000000001, 15.8413)\n' + ' ..cubicTo(0.5956680000000001, 18.2481, 1.8168600000000001, 20.4461, 3.5859, 22.1976)\n' + ' ..cubicTo(6.14623, 24.7325, 9.50955, 26.0, 12.8727, 26.0)\n' + ' ..cubicTo(16.236, 26.0, 19.5991, 24.7326, 22.1595, 22.1976)\n' + ' ..cubicTo(27.2802, 17.1277, 27.2802, 8.87841, 22.1595, 3.80852)\n' + ' ..close();', + ); + + path = parseSvgPathData('M10 10L20 20'); + + expect( + path.toFlutterString(), + 'Path()\n' + ' ..moveTo(10.0, 10.0)\n' + ' ..lineTo(20.0, 20.0);', + ); + }); + + test('addRect', () { + final PathBuilder builder = PathBuilder() + ..addRect(const Rect.fromLTRB(10, 10, 20, 20)); + + expect( + builder.toPath().toFlutterString(), + 'Path()\n' + ' ..moveTo(10.0, 10.0)\n' + ' ..lineTo(20.0, 10.0)\n' + ' ..lineTo(20.0, 20.0)\n' + ' ..lineTo(10.0, 20.0)\n' + ' ..close();', + ); + }); + + test('addOval', () { + final PathBuilder builder = PathBuilder() + ..addOval(const Rect.fromLTRB(10, 10, 20, 20)) + ..addOval(const Rect.fromLTRB(50, 50, 80, 70)); + expect( + builder.toPath().toFlutterString(), + 'Path()\n' + ' ..moveTo(15.0, 10.0)\n' + ' ..cubicTo(17.75957512247, 10.0, 20.0, 12.24042487753, 20.0, 15.0)\n' + ' ..cubicTo(20.0, 17.75957512247, 17.75957512247, 20.0, 15.0, 20.0)\n' + ' ..cubicTo(12.24042487753, 20.0, 10.0, 17.75957512247, 10.0, 15.0)\n' + ' ..cubicTo(10.0, 12.24042487753, 12.24042487753, 10.0, 15.0, 10.0)\n' + ' ..close()\n' + ' ..moveTo(65.0, 50.0)\n' + ' ..cubicTo(73.27872536741, 50.0, 80.0, 54.48084975506, 80.0, 60.0)\n' + ' ..cubicTo(80.0, 65.51915024494, 73.27872536741, 70.0, 65.0, 70.0)\n' + ' ..cubicTo(56.72127463259, 70.0, 50.0, 65.51915024494, 50.0, 60.0)\n' + ' ..cubicTo(50.0, 54.48084975506, 56.72127463259, 50.0, 65.0, 50.0)\n' + ' ..close();', + ); + }); + + test('addRRect', () { + final PathBuilder builder = PathBuilder() + ..addRRect(const Rect.fromLTRB(20, 20, 60, 60), 5, 5); + expect( + builder.toPath().toFlutterString(), + 'Path()\n' + ' ..moveTo(25.0, 20.0)\n' + ' ..lineTo(55.0, 20.0)\n' + ' ..cubicTo(57.75957512247, 20.0, 60.0, 22.24042487753, 60.0, 25.0)\n' + ' ..lineTo(60.0, 55.0)\n' + ' ..cubicTo(60.0, 57.75957512247, 57.75957512247, 60.0, 55.0, 60.0)\n' + ' ..lineTo(25.0, 60.0)\n' + ' ..cubicTo(22.24042487753, 60.0, 20.0, 57.75957512247, 20.0, 55.0)\n' + ' ..lineTo(20.0, 25.0)\n' + ' ..cubicTo(20.0, 22.24042487753, 22.24042487753, 20.0, 25.0, 20.0)\n' + ' ..close();', + ); + }); + + test('reset/no reset', () { + final PathBuilder builder = PathBuilder()..lineTo(10, 10); + + final Path a = builder.toPath(reset: false); + final Path b = builder.toPath(); + final Path c = builder.toPath(); + + expect(a, b); + expect(identical(a, b), false); + expect(a != c, true); + expect(c.isEmpty, true); + }); + + test('PathBuilder.fromPath', () { + final PathBuilder builder = PathBuilder()..lineTo(10, 10); + + final Path a = builder.toPath(); + + final PathBuilder builderA = PathBuilder.fromPath(a); + final Path b = builderA.toPath(); + + expect(a, b); + expect(identical(a, b), false); + }); + + test('transforms', () { + Path path = parseSvgPathData( + 'M22.1595 3.80852C19.6789 1.35254 16.3807 -4.80966e-07 12.8727 ' + '-4.80966e-07C9.36452 -4.80966e-07 6.06642 1.35254 3.58579 3.80852C1.77297 5.60333 ' + '0.53896 7.8599 0.0171889 10.3343C-0.0738999 10.7666 0.206109 11.1901 0.64265 ' + '11.2803C1.07908 11.3706 1.50711 11.0934 1.5982 10.661C2.05552 8.49195 3.13775 6.51338 4.72783 ' + '4.9391C9.21893 0.492838 16.5262 0.492728 21.0173 4.9391C25.5082 9.38548 25.5082 16.6202 ' + '21.0173 21.0667C16.5265 25.5132 9.21893 25.5133 4.72805 21.0669C3.17644 19.5307 2.10538 ' + '17.6035 1.63081 15.4937C1.53386 15.0627 1.10252 14.7908 0.66697 14.887C0.231645 14.983 ' + '-0.0427272 15.4103 0.0542205 15.8413C0.595668 18.2481 1.81686 20.4461 3.5859 ' + '22.1976C6.14623 24.7325 9.50955 26 12.8727 26C16.236 26 19.5991 24.7326 22.1595 22.1976C27.2802 ' + '17.1277 27.2802 8.87841 22.1595 3.80852Z'); + expect( + path.transformed(AffineMatrix.identity).toFlutterString(), + 'Path()\n' + ' ..moveTo(22.1595, 3.80852)\n' + ' ..cubicTo(19.6789, 1.35254, 16.3807, -4.809659999999999e-7, 12.8727, -4.809659999999999e-7)\n' + ' ..cubicTo(9.36452, -4.809659999999999e-7, 6.06642, 1.35254, 3.5857900000000003, 3.80852)\n' + ' ..cubicTo(1.77297, 5.60333, 0.53896, 7.8599, 0.017188900000000007, 10.3343)\n' + ' ..cubicTo(-0.0738999, 10.7666, 0.20610900000000001, 11.1901, 0.6426500000000002, 11.2803)\n' + ' ..cubicTo(1.07908, 11.3706, 1.50711, 11.0934, 1.5982, 10.661)\n' + ' ..cubicTo(2.05552, 8.49195, 3.13775, 6.51338, 4.72783, 4.9391)\n' + ' ..cubicTo(9.21893, 0.49283800000000005, 16.5262, 0.49272800000000005, 21.0173, 4.9391)\n' + ' ..cubicTo(25.5082, 9.38548, 25.5082, 16.6202, 21.0173, 21.0667)\n' + ' ..cubicTo(16.5265, 25.5132, 9.21893, 25.5133, 4.72805, 21.0669)\n' + ' ..cubicTo(3.17644, 19.5307, 2.10538, 17.6035, 1.63081, 15.4937)\n' + ' ..cubicTo(1.53386, 15.0627, 1.10252, 14.7908, 0.6669700000000002, 14.887)\n' + ' ..cubicTo(0.23164500000000002, 14.983, -0.04272720000000001, 15.4103, 0.05422050000000001, 15.8413)\n' + ' ..cubicTo(0.5956680000000001, 18.2481, 1.8168600000000001, 20.4461, 3.5859, 22.1976)\n' + ' ..cubicTo(6.14623, 24.7325, 9.50955, 26.0, 12.8727, 26.0)\n' + ' ..cubicTo(16.236, 26.0, 19.5991, 24.7326, 22.1595, 22.1976)\n' + ' ..cubicTo(27.2802, 17.1277, 27.2802, 8.87841, 22.1595, 3.80852)\n' + ' ..close();', + ); + + expect( + path + .transformed(AffineMatrix.identity.rotated(math.pi / 2)) + .toFlutterString(), + 'Path()\n' + ' ..moveTo(-3.808519999999999, 22.1595)\n' + ' ..cubicTo(-1.352539999999999, 19.6789, 4.809660010030285e-7, 16.3807, 4.809660007882255e-7, 12.8727)\n' + ' ..cubicTo(4.809660005734114e-7, 9.36452, -1.3525399999999996, 6.06642, -3.80852, 3.5857900000000007)\n' + ' ..cubicTo(-5.60333, 1.7729700000000004, -7.8599, 0.5389600000000004, -10.3343, 0.01718890000000064)\n' + ' ..cubicTo(-10.7666, -0.07389989999999934, -11.1901, 0.2061090000000007, -11.2803, 0.6426500000000008)\n' + ' ..cubicTo(-11.3706, 1.0790800000000007, -11.0934, 1.5071100000000006, -10.661, 1.5982000000000007)\n' + ' ..cubicTo(-8.49195, 2.0555200000000005, -6.51338, 3.1377500000000005, -4.9391, 4.72783)\n' + ' ..cubicTo(-0.4928379999999995, 9.21893, -0.49272799999999906, 16.5262, -4.939099999999999, 21.0173)\n' + ' ..cubicTo(-9.385479999999998, 25.5082, -16.6202, 25.5082, -21.0667, 21.0173)\n' + ' ..cubicTo(-25.5132, 16.5265, -25.5133, 9.218930000000002, -21.0669, 4.7280500000000005)\n' + ' ..cubicTo(-19.5307, 3.1764400000000013, -17.6035, 2.1053800000000007, -15.4937, 1.630810000000001)\n' + ' ..cubicTo(-15.0627, 1.533860000000001, -14.7908, 1.1025200000000008, -14.887, 0.6669700000000011)\n' + ' ..cubicTo(-14.983, 0.23164500000000093, -15.4103, -0.04272719999999906, -15.8413, 0.05422050000000098)\n' + ' ..cubicTo(-18.2481, 0.5956680000000012, -20.4461, 1.8168600000000015, -22.1976, 3.5859000000000014)\n' + ' ..cubicTo(-24.7325, 6.146230000000002, -26.0, 9.509550000000003, -26.0, 12.872700000000002)\n' + ' ..cubicTo(-26.0, 16.236, -24.7326, 19.5991, -22.1976, 22.1595)\n' + ' ..cubicTo(-17.1277, 27.2802, -8.878409999999999, 27.2802, -3.808519999999999, 22.1595)\n' + ' ..close();', + ); + + path = parseSvgPathData('M10 10L20 20'); + + expect( + path + .transformed(AffineMatrix.identity.translated(10, 10)) + .toFlutterString(), + 'Path()\n' + ' ..moveTo(20.0, 20.0)\n' + ' ..lineTo(30.0, 30.0);', + ); + }); + + test('Compute path bounds with rect', () { + final PathBuilder builder = PathBuilder() + ..addRect(const Rect.fromLTWH(5, 5, 95, 95)) + ..close(); + final Path path = builder.toPath(); + + expect(path.bounds(), const Rect.fromLTWH(5, 5, 95, 95)); + }); + + test('Compute path bounds with lines', () { + final PathBuilder builder = PathBuilder() + ..moveTo(0, 0) + ..lineTo(25, 0) + ..lineTo(25, 25) + ..lineTo(0, 25) + ..close(); + final Path path = builder.toPath(); + + expect(path.bounds(), const Rect.fromLTWH(0, 0, 25, 25)); + }); + + test('Compute path bounds with cubics', () { + final PathBuilder builder = PathBuilder() + ..moveTo(0, 0) + ..cubicTo(10, 10, 20, 20, -10, -10) + ..close(); + final Path path = builder.toPath(); + + expect(path.bounds(), const Rect.fromLTRB(-10.0, -10.0, 20.0, 20.0)); + }); + + test('Compute cubic bounds where R and B are negative', () { + const Rect circle = Rect.fromCircle(-83.533, -122.753, 74.461); + final Path path = PathBuilder().addOval(circle).toPath(); + expect(path.bounds(), circle); + }); + + test('Cubic length', () { + // Value is very close to what Skia says for same input. + const CubicToCommand command = + CubicToCommand(1.0, 15.327, 15.326, 1.0, 33.0, 1.0); + expect(command.computeLength(Point.zero), 38.16245134493276); + + // Trivially describes a line. + const CubicToCommand command2 = CubicToCommand(0, 0, 0, 10, 0, 10); + expect(command2.computeLength(Point.zero), 10); + }); + + test('Cubic splitting', () { + expect( + CubicToCommand.subdivide( + Point.zero, + const Point(1, 15), + const Point(15, 1), + const Point(33, 1), + .4, + ), + const [ + Point.zero, + Point(0.4, 6.0), + Point(2.88, 7.359999999999999), + Point(6.864, 6.832), + Point(12.84, 6.04), + Point(22.200000000000003, 1.0), + Point(33.0, 1.0) + ], + ); + }); + + test('Dashed path - cubic 1', () { + final Path cubic1 = parseSvgPathData( + 'M65 33c0 17.673-14.326 32-32 32S1 50.673 1 33C1 15.327 15.326 1 33 1s32 14.327 32 32z'); + + expect( + cubic1.dashed([2, 5.94]), + Path( + commands: const [ + MoveToCommand(65.0, 33.0), + CubicToCommand(65.0, 33.70763536030907, 64.97703198598045, + 34.40990628009675, 64.93180068504239, 35.106107839678536), + MoveToCommand(63.95590821783997, 41.138570321608114), + CubicToCommand( + 63.78388200211823, + 41.79470101955482, + 63.591548343963595, + 42.442608960532986, + 63.37961933238859, + 43.08158186093479), + MoveToCommand(60.9913809985287, 48.519833069956164), + CubicToCommand( + 60.668492690302394, + 49.100962858508964, + 60.32803245363184, + 49.6709842212665, + 59.97071797737027, + 50.22917927324997), + MoveToCommand(56.349259058467936, 54.88222449322346), + CubicToCommand( + 55.900015721696754, + 55.36139696181796, + 55.43619093879707, + 55.82673409716467, + 54.958492499076485, + 56.27752791653569), + MoveToCommand(50.36747808861274, 59.88172317266531), + CubicToCommand( + 49.82068927796797, + 60.23570903270937, + 49.26241388099913, + 60.57346570952925, + 48.6933336310888, + 60.894311283442455), + MoveToCommand(43.3292530683581, 63.29637356816345), + CubicToCommand( + 42.71018120276065, + 63.50737099626945, + 42.082572102791204, + 63.69992743387707, + 41.447080444962786, + 63.87338802556734), + MoveToCommand(35.630945421656286, 64.89340586107826), + CubicToCommand( + 34.97191375239566, + 64.94703220278787, + 34.30723211065745, + 64.98069119320479, + 33.637506242045006, + 64.99377692119126), + MoveToCommand(27.497686697670588, 64.52877424012252), + CubicToCommand( + 26.821431561562925, + 64.41158787290307, + 26.152455351357716, + 64.27317574141253, + 25.49146906822908, + 64.11424904112303), + MoveToCommand(19.829734138714148, 62.17288407887853), + CubicToCommand( + 19.22254029238493, + 61.89834087441918, + 18.625447859490016, + 61.60539834563903, + 18.03916772219419, + 61.29476756896835), + MoveToCommand(13.123453402340228, 58.08021292143201), + CubicToCommand( + 12.606563474017639, + 57.67001905040603, + 12.102648327670325, + 57.244183807034666, + 11.612423197029361, + 56.80342262050364), + MoveToCommand(7.634684007455509, 52.51189122452449), + CubicToCommand( + 7.236391480757161, + 51.99487385892312, + 6.853553118859099, + 51.46533727229267, + 6.486860221302153, + 50.923972953088025), + MoveToCommand(3.672998384894739, 45.82384400992106), + CubicToCommand( + 3.410525278026412, + 45.22442217885262, + 3.1658907799562406, + 44.61540918759221, + 2.9397661639663557, + 43.997476492863385), + MoveToCommand(1.4383854599561599, 38.30920094462239), + CubicToCommand( + 1.3301369848492222, + 37.660898732125794, + 1.2413788962838397, + 37.00599786367147, + 1.1727338823578792, + 36.34512119752162), + MoveToCommand(1.1140602215592654, 30.278918052611246), + CubicToCommand( + 1.1724789949662897, + 29.58512444526761, + 1.2530318675342023, + 28.897630742351822, + 1.355010329306275, + 28.21714564743783), + MoveToCommand(2.813895354454088, 22.352839675947116), + CubicToCommand( + 3.037366664760353, + 21.719242963796532, + 3.2802211823279825, + 21.094791779246986, + 3.5417468824172644, + 20.48019834161591), + MoveToCommand(6.34621623119101, 15.285435280579309), + CubicToCommand( + 6.714032665445101, + 14.73310174588461, + 7.098560403588749, + 14.192851911447924, + 7.499077458587648, + 13.665407961603714), + MoveToCommand(11.461835455942882, 9.332832232296191), + CubicToCommand( + 11.945763342108913, + 8.892180194722316, + 12.44325896152358, + 8.466169418310185, + 12.953622282985162, + 8.055500125561714), + MoveToCommand(17.807192192563488, 4.829368065777091), + CubicToCommand( + 18.384146248584546, + 4.517560722634985, + 18.971766599642926, + 4.22296045700293, + 19.5693656981362, + 3.946255004372108), + MoveToCommand(25.105444010192507, 1.9811251891028818), + CubicToCommand( + 25.74028000900354, + 1.8200561533454764, + 26.382686089997996, + 1.6779627249499476, + 27.032023265982627, + 1.5554840657280102), + MoveToCommand(32.94511841345392, 1.0000460546288212), + CubicToCommand(32.96340867914606, 1.000015355604456, + 32.981702545388494, 1.0, 33.0, 1.0), + CubicToCommand(33.6882562195718, 1.0, 34.37143533540402, + 1.0217263038461621, 35.04888905781622, 1.0645304446974477), + MoveToCommand(41.084875326577645, 2.030039591233501), + CubicToCommand( + 41.74147663234361, + 2.200961034160867, + 42.38987221052307, + 2.3922104375592332, + 43.029349965306906, + 2.6030755110223245), + MoveToCommand(48.472612206885536, 4.982395187695522), + CubicToCommand( + 49.05438116106162, + 5.304353344751444, + 49.62505841296779, + 5.643905011918012, + 50.18392615788486, + 6.000332188318815), + MoveToCommand(54.84351930781497, 9.614505510093846), + CubicToCommand( + 55.323479280858734, + 10.063008836851036, + 55.78961965045844, + 10.526114893814489, + 56.24123242758923, + 11.00311549848448), + MoveToCommand(59.853140416091875, 15.58850419596532), + CubicToCommand( + 60.20803756757811, + 16.134740492295958, + 60.54671980770702, + 16.69248333095327, + 60.868505145713, + 17.261050534801427), + MoveToCommand(63.27947749389385, 22.621402004854463), + CubicToCommand( + 63.49149442978085, + 23.240129771843495, + 63.68508101683114, + 23.867413675066253, + 63.859582179442924, + 24.50259845990592), + MoveToCommand(64.88908365060104, 30.31651883115104), + CubicToCommand( + 64.9437829347059, + 30.975372322751923, + 64.97852291408398, + 31.639891900169225, + 64.99269741829195, + 32.309471227309054), + ], + ), + ); + }); + + test('Dashed paths - cubic 2', () { + final Path cubic2 = parseSvgPathData( + 'M20 39c10.493 0 19-8.507 19-19S30.493 1 20 1 1 9.507 1 20s8.507 19 19 19z'); + + final Path dashed = cubic2.dashed([2, 6]); + expect( + dashed, + Path( + commands: const [ + MoveToCommand(20.0, 39.0), + CubicToCommand(20.707618776439247, 39.0, 21.4062056638081, + 38.96131204414812, 22.093760739330435, 38.88593605522049), + MoveToCommand(27.941787133378586, 37.26550076491443), + CubicToCommand( + 28.552828792067846, + 36.98395247399771, + 29.146376269127835, + 36.67092015975091, + 29.72035001637582, + 36.32848337035675), + MoveToCommand(34.28872657525249, 32.52332807494088), + CubicToCommand( + 34.72002359056755, + 32.03163198310322, + 35.12634784297375, + 31.51748652496024, + 35.50571809622244, + 30.9828729367606), + MoveToCommand(38.15063297069716, 25.634183986854904), + CubicToCommand( + 38.344405068145036, + 25.00930101075243, + 38.506794068808375, + 24.3706162148244, + 38.63595727148749, + 23.71997230027049), + MoveToCommand(38.854396196856676, 17.636338403239797), + CubicToCommand( + 38.76922952234208, + 16.950072134496082, + 38.647462472510895, + 16.275154057296998, + 38.49110936584479, + 15.613598490124222), + MoveToCommand(36.156993272025055, 9.997799228160478), + CubicToCommand( + 35.806275376209854, + 9.432491207486724, + 35.4267166755551, + 8.886947549472037, + 35.02037300554141, + 8.363224089597036), + MoveToCommand(30.714494314385455, 4.307279443052996), + CubicToCommand( + 30.170446598805572, + 3.9350931692753632, + 29.606060167556546, + 3.5904613343389835, + 29.023345848321203, + 3.2753947659266824), + MoveToCommand(23.398670360690218, 1.303142250295933), + CubicToCommand( + 22.75231244012423, + 1.1864084956688012, + 22.094586719261592, + 1.102371988982065, + 21.42726782093816, + 1.0528073530715703), + MoveToCommand(15.312878715409806, 1.5825736677487203), + CubicToCommand( + 14.645620930686587, + 1.7518831987472228, + 13.992442800092594, + 1.956503341694209, + 13.35545017026537, + 2.1943282499521333), + MoveToCommand(8.126992325034307, 5.1659142014116854), + CubicToCommand(7.612222375568647, 5.578470802327974, 7.11938698608979, + 6.017265988457743, 6.6505146434356375, 6.480271272963089), + MoveToCommand(3.144680725594728, 11.22271551538195), + CubicToCommand( + 2.841419027240009, + 11.803860948679425, + 2.5673312422713543, + 12.402645469100445, + 2.324360556848317, + 13.017125890485456), + MoveToCommand(1.0338820704843192, 18.855919027319942), + CubicToCommand(1.0114006643291984, 19.23440453004496, 1.0, + 19.61587149510607, 1.0, 20.0), + CubicToCommand(1.0, 20.30265471646772, 1.0070773712819083, + 20.603657186624655, 1.0210756343203329, 20.902850930945412), + MoveToCommand(2.285459600751064, 26.883674148689185), + CubicToCommand( + 2.530950502455319, + 27.514930621208894, + 2.809211392982629, + 28.12982234128287, + 3.118152746709571, + 28.7262597832877), + MoveToCommand(6.658084504103845, 33.527199707900266), + CubicToCommand( + 7.124854343878676, + 33.98761626048461, + 7.615335906786609, + 34.424054910109746, + 8.127531169442356, + 34.8345176333904), + MoveToCommand(13.322396345361739, 37.79329566146798), + CubicToCommand( + 13.93468962982282, + 38.02320534530446, + 14.561997661390594, + 38.22241500364579, + 15.202445231678627, + 38.38904942810554), + ], + ), + ); + }); + + test('Dashed path - lines/closes', () { + final Path path = parseSvgPathData('M1 20L20 20L20 39L30 30L1 26z'); + expect( + path.dashed([5, 3, 5, 5]), + Path( + commands: const [ + MoveToCommand(1.0, 20.0), + LineToCommand(6.0, 20.0), + MoveToCommand(9.0, 20.0), + LineToCommand(13.999999999999998, 20.0), + MoveToCommand(18.999999999999996, 20.0), + LineToCommand(20.0, 20.0), + LineToCommand(20.0, 24.0), + MoveToCommand(20.0, 27.000000000000004), + LineToCommand(20.0, 32.0), + MoveToCommand(20.0, 37.0), + LineToCommand(20.0, 39.0), + LineToCommand(22.229882438741498, 36.99310580513265), + MoveToCommand(24.459764877482996, 34.9862116102653), + LineToCommand(28.17623560871883, 31.641387952153053), + MoveToCommand(27.47750617803373, 29.65206981765983), + LineToCommand(22.524400531816358, 28.96888283197467), + MoveToCommand(19.55253714408593, 28.55897064056358), + LineToCommand(14.599431497868558, 27.875783654878425), + MoveToCommand(9.646325851651186, 27.19259666919327), + LineToCommand(4.693220205433812, 26.509409683508114), + MoveToCommand(1.7213568177033882, 26.09949749209702), + LineToCommand(1.0, 26.0), + LineToCommand(1.0, 21.72818638368261), + ], + ), + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/resolver_test.dart b/packages/vector_graphics_compiler/test/resolver_test.dart new file mode 100644 index 00000000000..c8ea93b10f7 --- /dev/null +++ b/packages/vector_graphics_compiler/test/resolver_test.dart @@ -0,0 +1,172 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/geometry/basic_types.dart'; +import 'package:vector_graphics_compiler/src/geometry/matrix.dart'; +import 'package:vector_graphics_compiler/src/geometry/path.dart'; +import 'package:vector_graphics_compiler/src/paint.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/src/svg/resolver.dart'; + +import 'helpers.dart'; + +void main() { + test('viewport node inheritence', () { + final Node node = parseToNodeTree(''' + + + +'''); + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final List nodes = + queryChildren(resolvedNode); + + expect(nodes.length, 2); + expect( + nodes.first.paint, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + expect( + nodes.last.paint, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + }); + + test('group opacity node inheritence', () { + final Node node = parseToNodeTree(''' + + + + + +'''); + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final List nodes = + queryChildren(resolvedNode); + final SaveLayerNode saveLayerNode = + queryChildren(resolvedNode).single; + + expect(saveLayerNode.paint.fill!.color, const Color(0x7FFF0000)); + + expect(nodes.length, 2); + + // Opacity is not inherited since it is applied in a saveLayer. + expect( + nodes.first.paint, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + expect( + nodes.last.paint, + const Paint(fill: Fill(color: Color(0xFFFF0000))), + ); + }); + + test( + 'Resolves PathNodes to ResolvedPathNodes by flattening the transform ' + 'and computing bounds', () async { + final Node node = parseToNodeTree(''' + + + + +'''); + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final List nodes = + queryChildren(resolvedNode); + + expect(nodes.length, 1); + + final ResolvedPathNode resolvedPathNode = nodes[0]; + + expect(resolvedPathNode.bounds, const Rect.fromLTWH(10, 10, 10, 10)); + expect( + resolvedPathNode.path, + Path( + commands: const [ + MoveToCommand(10.0, 10.0), + LineToCommand(20.0, 10.0), + LineToCommand(20.0, 20.0), + LineToCommand(10.0, 20.0), + CloseCommand(), + ], + ), + ); + }); + + test('Resolving Nodes replaces empty text with Node.zero', () async { + final Node node = parseToNodeTree(''' + + + '''); + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final List nodes = + queryChildren(resolvedNode); + + expect(nodes, isEmpty); + }); + + test('Resolving Nodes removes unresolved masks', () async { + final Node node = parseToNodeTree(''' + + + + +'''); + + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final List nodes = + queryChildren(resolvedNode); + + expect(nodes, isEmpty); + }); + + test('visitChildren on clips and masks', () { + final ResolvedClipNode clip = ResolvedClipNode( + clips: [], + child: Node.empty, + ); + + final ResolvedMaskNode mask = ResolvedMaskNode( + child: Node.empty, + mask: Node.empty, + blendMode: BlendMode.color, + ); + + int visitCount = 0; + clip.visitChildren((Node child) { + visitCount += 1; + expect(child, Node.empty); + }); + mask.visitChildren((Node child) { + visitCount += 1; + expect(child, Node.empty); + }); + + expect(visitCount, 2); + }); + + test('Image transform', () async { + final Node node = parseToNodeTree(''' + + +'''); + final Node resolvedNode = + node.accept(ResolvingVisitor(), AffineMatrix.identity); + final ResolvedImageNode imageNode = + queryChildren(resolvedNode).single; + expect( + imageNode.transform, + const AffineMatrix(1.0, 0.0, 0.0, -1.0, 50.0, 50.0), + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/tessellator_test.dart b/packages/vector_graphics_compiler/test/tessellator_test.dart new file mode 100644 index 00000000000..46c000dba3b --- /dev/null +++ b/packages/vector_graphics_compiler/test/tessellator_test.dart @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/svg/node.dart'; +import 'package:vector_graphics_compiler/src/svg/parser.dart'; +import 'package:vector_graphics_compiler/src/svg/tessellator.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'helpers.dart'; + +void main() { + setUpAll(() { + if (!initializeTessellatorFromFlutterCache()) { + fail('error in setup'); + } + }); + + test('Can convert simple shape to indexed vertices', () async { + final Node node = parseToNodeTree(''' + + +'''); + Node resolvedNode = node.accept(ResolvingVisitor(), AffineMatrix.identity); + resolvedNode = resolvedNode.accept(Tessellator(), null); + + final ResolvedVerticesNode verticesNode = + queryChildren(resolvedNode).single; + + expect(verticesNode.bounds, const Rect.fromLTWH(0, 0, 10, 10)); + expect(verticesNode.vertices.vertices, [ + 0.0, + 10.0, + 10.0, + 0.0, + 10.0, + 10.0, + 10.0, + 0.0, + 0.0, + 10.0, + 0.0, + 0.0 + ]); + expect(verticesNode.vertices.indices, null); + }); +} diff --git a/packages/vector_graphics_compiler/test/test_svg_strings.dart b/packages/vector_graphics_compiler/test/test_svg_strings.dart new file mode 100644 index 00000000000..66f135a8951 --- /dev/null +++ b/packages/vector_graphics_compiler/test/test_svg_strings.dart @@ -0,0 +1,1493 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:vector_graphics_compiler/src/geometry/path.dart'; + +const List allSvgTestStrings = [ + ghostscriptTiger, + simpleUseCircles, + basicOpacity, + groupOpacity, + basicMask, + groupMask, + basicClip, + multiClip, + blendAndMask, + outOfOrderGradientDef, + xlinkGradient, + basicText, + pathAndParent, + svgInlineImage, + basicOverlap, + opacityOverlap, + solidOverTrasnparent, + transparentOverSolid, + complexOpacityTest, +]; + +const String kBase64ImageContents = + 'iVBORw0KGgoAAAANSUhEUgAAAkoAAACqCAYAAABS+GAyAAAACXBIWXMAAAsSAAALEgHS3X78AAAaD0lEQVR4nO3da4wd1WHA8TP3rnlVzW6gogIUdlVFQSRRvSlNv6VeolYJT6+JIahSxSJFEV8qjPqh/cb6W6UWsXyqGqVlLbVKAgbbqQqtmmA7PA1ts2552g71QhNjG5NdjF/43jvV3D1nPb5779w5Z86cOTPz/ykbDHsfs3PXe/975syZIAxDAXuefL/dePZIe3s7DDcNe9Cse37Y/Yc+/pAbqE8HQjy18bqRe+7+XLOjsXkAAJReg5fQHhVJHRFuCoISbHCKbSSSAAB1RihZEo+kSnxBRBIAAGKEXZCdr5EUZDi8FwTi6TuvJZIAAPXGiFJGlRxJWomku4kkAEDdEUoZFB1JeUyDIpIAALiAUDLESBIAANVHKBkgkgAAqAcmc2vqRtIH7ac6Ipwu1YYnIJIAAOiPESUNq5EUEkkAANQBI0opVTGSGkI8fQeRBADAQIwopUAkAQBQT4TSEEQSAAD1RSglIJIAAKg3QmkAIgkAABBKfRBJAABAEEprRZH0b0QSAAC1Jwili6lIahNJAADUnmAdpQuqGkm3E0kAABhjRKnEkRQmfI5IAgAgu9qHEiNJAAAg4T21vgZFUtJIje+IJAAA7KltKDGSBAAAUry31o+LSAodD0sRSQAA2Fe7UGIkCQAAaLzH1odPkWRrwIlIAgAgP7UJpbSRVKaJ3EQSAAD5qsWCkxcuSyKqc7gtEDtuu4ZIAgAgT5UfUapwJG0mkgAAyFelQ6k3kkJHB9byPOONSAIAwJ3KhpLJSJLv85OIJAAA3KpkKPl+uM0kyIgkAADcq1woDYokV4fd8kAkAQBQjEqFUpaRJFsZZXt+EpEEAEBxKhNKSZFU1tEkIgkAgGJVIpTKtARA2mQjkgAAKF7pQ2lYJKUZTfJtvIlIAgDAD6UOJd9GkmzMTyKSSieswccezRdlVnOfzNb9mwiAv0obSmkiybfRpGHPRSQBAOCXUoYSlyUBAAAulC6U0kaS6zPdshx2I5IAAPBTqULJ9kiSD4fdiCQAAPxVmlDSiaSyjCYRSQAA+K0UoZRHJBW9JACRBACA/7wPpTwmbruOpN7nI5IAACiHEZ+3UjeSirhUie5hNyIJgIdmhBATGps1L4Q47NmXobseF+t3IRVvQymvSCrykBuRBMBTUSht0Ni0PR6G0sOatyeUkIqXh97Ksk5SmtEkdRMiCQCA8vFuRMkkknwfTSKSEHNzCXfGkgfbAACF8CqUyhRJaUeTiCT00L1uGgCgQN4cesszkopCJAEAUG5ehFLekVTEaFJAJAEAUHqFH3orWySlEY0k3UokAQBQeoWOKJUxkoaNJhFJAABUR2GhRCQBAADfFRJKHG4DAABl4DyUynp2W9JoEpEEAEA1OZ3M7eLaba6TikgCAKC6nI0olTmSBo0mEUkAAFSbkxElnUgyPcxGJAEAANtyD6W8IynPQ21EEgAA9ZZrKKWNJN9GkZIQSQAA1EduoZQmkrKczZZ3JPUbTSKSAACol1xCaVgkZT3dn0gCAEBMyI8puSumEnbJkhBiQf55jxDisPyok2j/TMp9Npmwj9RHd/9YD6WkSPI9kASRBADw15gQ3ffWKfkxrrmlG+U/H5b/XJRBsFN+LFXwtZ+OfYymuP3G2J/3CyHmrYZSv0iytVgkkQQgJvqNcEZjh0S/Gc57tgPz/BpmDbZFx8yQ0Ys09siPQdujs29M6O6jfuYdjcpMyf1xn+XHHZcfURw8LoTYJYPJ5d8Vndch6Xum14x8bN2YjFsvhHjUWijFI8n2StpFrctNJAHemoj9VpzGXk9DKa+vQedxTdh6w04Kpby/BhuPvyfnUIre7LfIN2wXNsqPudhH3qNMuq/DsFCakn9PsgTSRawsOKkiqR2GViMpdBhJvaNJRBIAoCCTMggedxhJcaMyYKII3FKSb4IxORq222YkCRuhFI8kO5vkNpAEkQQA8MOYHMn5uRBigwdbFAXTo3IeU7/Jz76YlNu4MY/tyRRKtiPJdSAJIgkA4Ac1ivSgh6/Herltec8bM6H2m9VRpDjjULIZSUUEkiCSAAB+mJZv9kUcZktrVB4K9Gmun4qkNGezGTMKJVuRVFQgCSIJAOCHaJRmR95v9hbdJw9zjRW8HU4iSZiEko1IKjKQBJEEAPDDrBylKRt1KK6oWBqTI1tO4lIrlLJEUuhBIAkiCQDghxkHSyDkab3Gmka2zbo8TJl6HSWTSCo6iuJYTBIA4IkZiyNJy/JQ2B75T7Xu0YJcj0qN+sQvd2LrjLr1cmTH5STvCdcT3lOFUtpI8imM4ogkAIAnJi1F0i4ZKTsTbrOQ8Dl1aY+si4eqOUtzGR8nLdMV1eNBKWJBqUJSXQNuzUjV0FDqF0m+BlE/RBKAGrpZ80ue0zyU8dCQN+E0kla0XjD4GnZr3l738fvR3QdjFg5X7ZKLQGZdEVxd322LjI8sozSPxka08jQh407HttjXmsaYHHVbjcjEUIoi6dnuZUnsLSbpEpEEoKZ034x1L1OxkPP8lCUH81+KmF+TZQLyojzEZXu7l2QszcsP07k/8w4WpdQZ/TINyqWeiJwZGEpljqR+gSQcRtID+07feFY0/yz6cycMg4s3TgSD7heGg7ZcT9oHSfNsYXeLg3D0o48e+tvbr/3UxvYBQA1NZ1g5eq+8f57XXVOrb88bHo5bL0embFxsOItlGZRpR5CSRPt7rm8oEUnmHth35sbTovnm6vYEPV00IJPCpE+uvWHWm2gdPm2cPvMlIgkAjI1lWKhxm+PJ0jOx68zp2iLcXEh3kP0yKK1eqHjN8gBljaQokPyIpMaFSEp5v9TRUkAkBSdPffkfvz72ZoqbAgD622J4yM11JClR1N1vcL/RAi+iu1/OLbIaSaI3lMocSYNUJpIcP1Z32z5c/t1/+uMr37D8sABQJ2OG8bC34GurRbH0mMH9thSwEKWKpFxGslZDqYyRlDSKJEoQSVosPWjah2kf+XD9D26/+n/sPCt6XgLfP6Z4wQBrTEaTFg3O7spDtO17NR/X9ajSct7zt7qhVNZISuIskl41jyTXh9zS+vT/jk3+aNN1/23xIQGgjkxHk2YKnOfTy2RUy+VImPU5Sb0aZYukYaNIwnUkheWJpDS3+3Txg69s3/y5/SkfEgAw2IzBaNJjBV4apJ/Dcq6UjnFHI2LbXOyrlUNvof9rSKYJJFG1SLIozXOeOfj+Tdu/PZ73gmEAUBe6o0nLHpxe34/JNuUdSsuuDvE1oqC45Zrm5oYIdrh4Ql1pA0lUMZIcHnI7887i7+/408//l6WHA4C6m5QjKzqKPLU+icmoUt6h5GxfdUeUfIslFUc6yy+WIZK0ODzkdvqtw1/dcd8X/tPm5gNAzZnM03F1vTQTugs4juZ4Ysiyy321etabD7GkG0dKWSLJ5rwkWw/1yevv/sHO+2/4D3vPCAAwiIRtno4mKTtloOjIK5R2utxXF62jVEQsmYwexVUukhw+XhRJP/7Oja9Z3jQAqLu+V6EfwsYlN/Kmu415hpIza1bmdhFLWeNIqWQkOTrkRiQBQG50A2G5JKGke7JPHhfJdb6v1oSSyCmWbMWRQiSZ345IAoBc6QZCGSJJGITSaA6rdDs/M7tvKImMsRSPIptxpNQ1kmwgkgAgd7qh5NO6SUlMIsX2qJLzfTWS9EkZIpufPdLe3hHhpt7P2w6gNIIaR1LW0SQiyQtbS7CNua5yC9TABs0vcUvB13XL04Tlx3Y+opQYSiIWS88caW3vhGJNLLkSypGk20pw7TYiCQl8XEwOgD0mYaA78btMbIeS8zMDBx56i4vCJBrFiULF2Zb1XDXUVSR996VTX1SRFJYskgYhkgDAGdthgIKlCiXhOJZ6A8VlJJ1tjrwhDOOk6Ejqd1siCQCcsj15uezyOPPNqdShJHKOpXDACA6RZH5bIgkAnCt9GFhW+nDUCiVhOZYGxdHqxjmKpFt3n77lrbONN1qhP5GU9SGJJAAAstMOJZEhlsIUcaS4jKTjreCZMy0h3jvVEW2NkNGew6T52Ka3I5IAALDDKJREyljSCaM4V5F0296z34wiSS1zcLadPpa0B4ZyiKR+iCQAAOwxDiURi6VobaPeKDJ9s3cZSUfPhc/2rgWVJpZ8iaTe2xJJAADPlH5dtkyhJGQs3WZpzpLLw21RJA36fFIsEUkAAKRW+lAauuBkGmpRyn/JsCily0g6dj54ZtjtVCxd/xsN0QwMR8iIJABAsm1CiPkK7yPnK2nbZiWURMZY8i2SlHgsNQLNJyOSAADDHS7Rtd5qKfOhtziTw3Au5yTpRJJypi3E4qmO6GRd1Cj7TYkkAPCf7gjKFK+p36yGktCMJdcTt3XuE5+QflYnlogkAOmwgnM16V6LjO8Dz1kPJZEylnyPpF6pYolIApAeKzhXk+7k5SpfELcScgklMSSWfI2kYcsaDIwlzfUQiCQAqCyTs7yIZo/lFkpiQCz5HElprIklzdPhiCQAEnNTqmuv5lfG94LHcg0l0RNLPkaSyeKYKpbaGl+F7vMQSUDlTfASVxYTuivE2vIASdTSAeLCn3OTNpKyXCYkVGfDne6I8StW1lmy+VxEElB5USSN8zJXlm4obZSTunUngsOB3EeUlCiQqhJJylkZSzYvd0IkAbnwbQ7ItAfbgPyYrIvE94SnnIVS3tJEUpZr0A26b1IsEUmAN0Y9eylmPNgG5Cea0L1f89H5nvBUJUJpWCRlCSSR4r79YolIArzjyzyQCU4JrwXdUaUNzFXyU+lDKSmSbASS1tlwpzuiFRJJgAMmhzZ8Ofw268E2IH8m128rw/fGjJxLNVuXxTJLHUqDIilrIAnD+6+eDZdhTSUiCciND7+tR6NJ93mwHcjfgsHhtw2ez1WKwmhOHsp+WB5irHwwlTaUeiMptBhIWR7jXCd9LBFJQCa6a9Vs9OAHelVGk7jsRjpzBveZ93j/zvfM96tFMJUylOKRZCOObD6OSBlLRBKQmckKyEX+tj5VodGkKqwk7eJriMJiWfM+o4aH7fK2Rf6yMWibVTDNVW2NsNKF0q27T9/ywbnwWZthY+tx4gbFUr/tJpIAIybzlIoa0Rnz9M3PVBVGDlx9DSajShs9G32MovLRFLeLgulBIcT/ej4ypqVUofTNPWe+cfR88Iytx7MZW/30xlK/5yKSAGMmoTRe0GnYOyu2wGQVzs5y9TXMGYwqCTlC48OSAZOGf9cqcwZfaUIpiqTjn4p/tfFYeQdSnIqlVp8nJJKATKJh/kWDB3B9aGBeTtL1me5K0usrcHjF1WHYJXnYysTjBceSiiSTdchmq7LSeClCyVYkuQyk1ecM+58NRyQBVuw0eJBReT8XhwXmSzIvyeQNzbcFEnUn97uMvXmDM+CUxw0P32U1lSGS9lbpULP3oWQjkooIJCEjSTkXiyUiCbDG9IfxevkmkFcsjcnHL8vkbd0RJSFHSXyag2ISey4DJEtYPii/n1yFXTQatNswkpartsq496F0qhP8vel9iwok0RNJShRLr/xk37unn3vZ5IcSgLUWDEYSFBVLts9+mpKHBX0/3BZn8jPJt7OzTObRbHT4ph7t460Z7r9BPkaep+FPyed4OMNjzBqekeot70Pp61c3x68YCX6Z9va21lMyFQVSv0iKHN332vnjB37xO68G4p//6E/+YV1BmwhUTZY36yiWfm7pzWdCHtIz/U28SKbzvTYajszN5PBmahJKQh7a0j3DbEzeR3fu0WyGsBc5rls0Jf8e7c54eZ1tBR0mzJX3obT1y+va3VhqisRYKjKOVrchYQOiSFo6+ItuHLUawTf2NcSP73zgByMONw+oqizzPxT15jOvOcI0Jt/0d8pTogetM9PProxvmraZzPcScqRDvXEnHRqakGFxWMaJ7bMAFwxjT8Re/5kh8TEpQ+CwvI9JqExb+H5VwfRr+boN2+5+JuXrsSADKeth4v0ZJq17LQiT3t098vDr55vPHW29d7otrlVb5cuWD9uF8UiKC9rhM5cdPH7nidf+op3/ViInut+GQQ1eiFnNofutFtaMmZI/7G1Zlm8gapRiSf67OuV5TL7RmB5eW5b31zkjbm/Op1xPytG1rBb7jBZNDAgj238fdL/3BtnfZ85Tv9fJ9Hs3y9lkSRbl96k6lHpYfh0q/tX37aTl516Uj2kyT0z3Z+jNGUYPjZRmRCMaWTry+uL4m791XXTt2WtT3MUJ00jqjoA1g1vP3XD1rqu++lcbT7z2l8QSYC76wfmYnPRqw6h8Y8xrnlEeh56yUvO9sn7N4wWuGTUnRzWyRkCWw09pLGQ8q2wQte91RjazWpajZJVYCqCfUi04+b17P9/64oe/HL+8IX5V9LYkzUVSkiJJaQfitnM3/PaOz2x4pNQXKAY8MJvh0ItLWzMc5spb2a9Ft1SiOTIqlkwWo/TFcmwCeGWV7s05iqUvFRxLaY5WHn1lbSQNmkfVDsQdneuvfHr0a8QSkMGS/M3W5zeebZ7HiBqZK7O5kgSzkIExaWHOUhH2y0OqlT+Lu5RvzCqWrmiER1w+b5pRJCEjafngoXXxKhp2t7YQG1sTVz3VuPf7dZjDAuTF59/St5ZkfZnZkr5xK2UI5rjD8nt2lz+bNNQuuc2VPdwWV9oRjJVY+tX1LmIpbSBFjr786vnlAyuRFET/F6afqdYR4fTll4xsJ5aATHyLpWg77i/RYa2lChwSWijZGVgq7h7yfL8vy22s9JykXqU+1PN33TlL+cWSTiBFjr30auvjbiSFq/fXKqUolsLwrkvXNYklIBsVS0UfgtkfW6OmTFQslXlkaV4GapnMyUNxPo4u7Y0tj1ArpZ8T8z0ZS5cHdmNJd9WEYy/tay0fODiyMpIU3V8+QGxkKd3zhtFHFEtPEktAJmr+RxFzbtRv3pMlnsOhYtOntZ50RbH0lZKNjh2WIzY3exKqizI4p6q24nZalZg83D0Md8JOLOmOIkWOvbiv9fHbh0ZWeijs3j8QsdpKEUsykOL//i1iCchMXbn9Zkdv+MtyLtJERX7zViNLvh8SSrIgX49t/m5iX+ryOpsKilUVSBNVusCtidIsOJnGd394aOSNq65970wYXKN7X9PdcOyFfa2P3zk4EgYrPRMGagm1oPu/7sPKz638e7BmibWk1yAIgqfOnW/f3fnhd6rzQlWL7gKAThdKK8iE5sU7Dzv8TXVKTqi2fbHaRflmMqc5d2NSY0XlpYJHp8ZkdNpYp0jIfebqIq/KhJwrZuv1f8hhEKuVzadzXKdqWS5dMe/wZ5Xuz9AF1/OjKhVKQjOWsn7px194pfXxO4dGOuo/GMRSmv1PLAHWjckf0NMyVkwWGNwr30x21uEU6R7TsQ+daFqO7bMiRynUpWemDRbYXIxtf1Gv+6T8/p2SfzYNp/gK9HX8Pk6lcqEkUsSSjS/5+POvtJbfPjiigihUo0QpY6kT6i3eTywBuVO/2Sb9hrsgR794Q7lAjSAm7bc9HoyIJZmKXd6jn6XYa+/jPJ2x2OjksGsVqpEi5yMzZVXJUBIDYsnWl3rs+VdaJ99aiaSVABJasRTKSUz9DsMN0r1LILZ/2urcQywBAOBGZVeCjk/wNpmgPcjxn73cOvnmgRF12v/KpO2V/wvUc8gnC8ILn1tZljsUYaej9XwiPic8FJsvGWk8wQRvAADcqOyIkqJGlk539Cd4x0V76cO9L7dOvnVgdeL22tEiMXBkKRQrQ0JhI/p8+s7p9/IwsgQAgBuVDyWRMZbU3vlwz0urh9u6/z1lLEX/7DRWAkloBJIYcqiQWAIAIH+1CCVhEEvxvRJF0ifdw20ykIbEUnfUqNkQnYbQGj1afe6ULwmxBABAvmoTSiJlLPXujRO7X5JzksTFgbQmlgLRGWmsfGSYQaT7chBLAADkp1ahJBJiqd9eOPHcixePJIneWIrF0Ugj1ZpIg5jeVd5te6tNLAEAYFvtQknEYulUwsjSiZ++2P7kzXeaQqwdPeqsa4r2Jc1uHCkFRpJCLAEAYFktQyly018vNC+b/ML7/WLpxE9faJ9640BTqLWQRLASRlEgrWtcNCk76/4zuXvCXYglAAAsqm0oiQGxdOInL7RPvf5Os7t69rqmaF060o2k3jPWiggkkRxJCrEEAIAltQ6lyE1/s9C8bP1KLH3078+3T759qNm6bES0Lx1ZOaW/R1GBJNJFkkIsAQBgQe1DSciRpRMnl149dujd3wub/U9Zs7GfchxF6odYAgAgI0JJ+szXHml0xq/c2Q7EHfH/XmQgCfNIUtu9vd0JiSUAAAwRSjG/+YePNMLxK3e0hbjT1n5xHUl9tptYAgDAEKHUI4ql9vWf3dER4s4sj1PgKFI/xBIAAAYa7LSLnfzZn3dG3vv1poYQu0zuH7WKZ5EU2dxsBE807v1+hjXDAQCoH0aUBohGllrXX/lUKMLpNLfPuhtzCqRejCwBAKCBEaUBopGlc632XYEIdiTdLusIknAXSYKRJQAA9BBKCaKRl3Ot9reCIHi691a2AslhJKltJpYAAEiJQ28pRFFx6brm9jAM77Kxu7I8hOnr1Xu3IOAwHAAAwzCilEJ3ZOl8e3M0xyfrY/kQSfK/MbIEAMAQjChpiKLikpHGE1Fk6N7Xl0DqxcgSAACDMaKkIYqJT1ude6K4SHuvLPOQRM6RJBhZAgAgESNKBtKMLGXdq1leF8O7PtkJw28zsgQAwAWEkqFBsWRjb+Y9irTmfnKrAxEQSwAAxBBKGfTGUtlGkcI+W0wsAQBwAaGUURRLI83GE9FijlkeyYdIUoglAABWEEoWZImlAuYiJUaSQiwBAEAoWaMbS1n3e56RpBBLAIC6I5QsShNLZQiknud6UghBLAEAaol1lCyKYqLV7twzaAXvEkZS5G4hxI9YZwkAUDtCiP8H5/u1dCM1SvoAAAAASUVORK5CYII='; + +/// https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/svg.svg +const String useStar = ''' + + + + + + + + + + + +'''; + +const String useColor = ''' + + + + + + +'''; + +const String imageInDefs = ''' + + + + + + + + + + + +'''; + +const String transformedClip = ''' + + + + + + + + + + + + +'''; + +const String strokePropertyButNoStroke = ''' + + + +'''; + +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mask +const String basicMask = ''' + + + + + + + + + + + + + + +'''; + +const String groupMask = ''' + + + + + + + + + + + +'''; + +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath +const String basicClip = ''' + + + + + + + + + + + +'''; + +/// Constructed example where clips cannot be trivially combined. +const String multiClip = ''' + + + + + + + + + + + + + +'''; + +/// Constructed example based on [basicClip] that has refs. +const String useClip = ''' + + + + + + + + + + + + +'''; + +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/opacity +const String basicOpacity = ''' + + + + +'''; + +const String groupOpacity = ''' + + + + + + + + + +'''; + +/// https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg +const String ghostscriptTiger = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +'''; + +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use +const String simpleUseCircles = ''' + + + + + +'''; + +const String simpleUseCirclesOoO = ''' + + + + + +'''; + +const String simpleUseCirclesWithoutHref = ''' + + + + + +'''; + +/// https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text +const String basicText = ''' + + My + cat + is + Grumpy! + +'''; + +const String blendAndMask = ''' + + + + + + + + + + + + + +'''; + +const String outOfOrderGradientDef = ''' + + + + + + + + + +'''; + +const String xlinkGradient = ''' + + + + + + + + + + +'''; + +const String xlinkGradientOoO = ''' + + + + + + + + + + +'''; + +const String linearGradientThatInheritsUnitMode = ''' + + + + + + + + + + + + + + + +'''; + +const String xformObbGradient = ''' + + + + + + + + + +'''; + +const String xformUsosRadial = ''' + + + + + + + + + +'''; + +const String focalRadial = ''' + + + + + + + + + + + +'''; + +/// A constructed SVG with a bunch of missing references. +const String missingRefs = ''' + + + + +'''; + +/// An SVG string to test if path node and parent node count stays the same for [MaskingOptimizer] and [ClippingOptimizer] +const String pathAndParent = + ''' '''; + +const String svgInlineImage = ''' + + + +'''; + +const String basicOverlapWithStroke = ''' + + + + + + +'''; + +const String basicOverlap = ''' + + + + + + +'''; + +const String opacityOverlap = ''' + + + + + + +'''; + +const String transparentOverSolid = ''' + + + + + +'''; + +const String solidOverTrasnparent = ''' + + + + + +'''; + +const String complexOpacityTest = ''' + + + + + + + + + + + + + + + + + + +'''; + +const String alternatingPattern = ''' + + + + + + + + + + + + + +'''; + +const String starPatternCircles = ''' + + + + + + + + + +'''; + +const String textDecorations = ''' + + + Example text05 - Text decoration + + + Overline text + + + Strike text + + + Underline text + + +'''; + +const String textAnchors = ''' + + + Example text 06 - Text anchor + + + Text anchor start + + + Text anchor middle + + + Text anchor end + + +'''; + +/// Excpected groupMask result when [MaskingOptimizer] is applied +List groupMaskForMaskingOptimizer = [ + Path(fillType: PathFillType.evenOdd, commands: const [ + MoveToCommand(12.562000000000001, 20.438), + CubicToCommand(12.562000000000001, 20.748, 12.310000000000002, 21.0, + 12.000000000000002, 21.0), + LineToCommand(4.687000000000001, 21.0), + CubicToCommand(3.755590764649998, 20.998347258709998, 3.0011007948299984, + 20.24341005121, 3.0, 19.312), + LineToCommand(3.0, 4.6880000000000015), + CubicToCommand(3.0011020706799982, 3.7562002166800013, 3.75620021668, + 3.001102070680001, 4.687999999999999, 3.000000000000001), + LineToCommand(12.0, 3.0000000000000004), + CubicToCommand(12.30144139827, 3.0127127094300006, 12.53927764767, + 3.2607906538300004, 12.53927764767, 3.5625000000000004), + CubicToCommand(12.53927764767, 3.864209346170001, 12.30144139827, + 4.11228729057, 12.0, 4.125), + LineToCommand(4.687000000000001, 4.125000000000001), + CubicToCommand(4.378, 4.125000000000001, 4.125, 4.377000000000001, 4.125, + 4.6880000000000015), + LineToCommand(4.125, 19.312), + CubicToCommand(4.125, 19.622, 4.378, 19.875, 4.687999999999999, 19.875), + LineToCommand(12.000000000000002, 19.875), + CubicToCommand(12.310000000000002, 19.875, 12.563000000000002, 20.127, + 12.563000000000002, 20.438), + CloseCommand(), + MoveToCommand(21.0, 12.0), + CubicToCommand(21.0, 12.169, 20.921, 12.316, 20.804000000000002, 12.418), + LineToCommand(20.808, 12.423), + LineToCommand(16.308, 16.361), + CubicToCommand( + 16.205000000000002, 16.442999999999998, 16.079, 16.5, 15.938, 16.5), + CubicToCommand(15.627, 16.5, 15.375, 16.247999999999998, 15.375, 15.937), + CubicToCommand(15.376131882800001, 15.77572417052, 15.44773957195, + 15.62300981305, 15.571000000000002, 15.519), + LineToCommand(15.567, 15.514), + LineToCommand(18.94, 12.563), + LineToCommand(9.188, 12.563), + CubicToCommand(8.87706369162, 12.56299999183, 8.6250000148, 12.31093630838, + 8.6250000148, 12.0), + CubicToCommand(8.6250000148, 11.68906369162, 8.87706369162, 11.43700000817, + 9.188, 11.437), + LineToCommand(18.94, 11.437), + LineToCommand(15.567, 8.486), + CubicToCommand(15.447063979900001, 8.37775998399, 15.37751598331, + 8.22453705399, 15.375, 8.062999999999999), + CubicToCommand( + 15.375, 7.75206368585, 15.62706368585, 7.5, 15.937999999999999, 7.5), + CubicToCommand(16.079, 7.5, 16.205, 7.557, 16.304000000000002, 7.644), + LineToCommand(16.308, 7.639), + LineToCommand(20.808, 11.577), + CubicToCommand(20.9279360201, 11.68524001601, 20.997484016690002, + 11.83846294601, 21.0, 12.0), + CloseCommand() + ]), + Path(fillType: PathFillType.evenOdd, commands: const [ + MoveToCommand(12.418000221252441, 3.196000099182129), + CubicToCommand(12.315999984741211, 3.0789999961853027, 12.168999671936035, + 3.0, 12.0, 3.0), + CubicToCommand(11.838462829589844, 3.002516031265259, 11.685239791870117, + 3.07206392288208, 11.57699966430664, 3.191999912261963), + LineToCommand(7.638999938964844, 7.691999912261963), + LineToCommand(7.644000053405762, 7.696000099182129), + CubicToCommand(7.557000160217285, 7.795000076293945, 7.5, 7.921000003814697, + 7.5, 8.062000274658203), + CubicToCommand(7.5, 8.372936248779297, 7.752063751220703, 8.625, + 8.062999725341797, 8.625), + CubicToCommand(8.224536895751953, 8.62248420715332, 8.37775993347168, + 8.552935600280762, 8.486000061035156, 8.432999610900879), + LineToCommand(11.437000274658203, 5.059999942779541), + LineToCommand(11.437000274658203, 14.812000274658203), + CubicToCommand(11.437000274658203, 15.122936248779297, 11.689064025878906, + 15.375, 12.0, 15.375), + CubicToCommand(12.310935974121094, 15.375, 12.562999725341797, + 15.122936248779297, 12.562999725341797, 14.812000274658203), + LineToCommand(12.562999725341797, 5.059999942779541), + LineToCommand(15.513999938964844, 8.432999610900879), + LineToCommand(15.519000053405762, 8.428999900817871), + CubicToCommand(15.62300968170166, 8.552260398864746, 15.775724411010742, + 8.623867988586426, 15.937000274658203, 8.625), + CubicToCommand(16.24799919128418, 8.625, 16.5, 8.373000144958496, 16.5, + 8.062000274658203), + CubicToCommand(16.5, 7.921000003814697, 16.44300079345703, + 7.795000076293945, 16.361000061035156, 7.691999912261963), + LineToCommand(12.42300033569336, 3.191999912261963), + LineToCommand(12.418000221252441, 3.196000099182129), + CloseCommand(), + MoveToCommand(21.0, 12.0), + CubicToCommand(21.0, 11.6899995803833, 20.74799919128418, + 11.437999725341797, 20.437999725341797, 11.437999725341797), + LineToCommand(20.437999725341797, 11.437000274658203), + CubicToCommand(20.12700080871582, 11.437000274658203, 19.875, + 11.6899995803833, 19.875, 12.0), + LineToCommand(19.875, 19.312000274658203), + CubicToCommand(19.875, 19.621999740600586, 19.621999740600586, 19.875, + 19.312000274658203, 19.875), + LineToCommand(4.688000202178955, 19.875), + CubicToCommand(4.376999855041504, 19.875, 4.125, 19.621999740600586, 4.125, + 19.312999725341797), + LineToCommand(4.125, 12.0), + CubicToCommand(4.112287521362305, 11.698558807373047, 3.8642094135284424, + 11.460721969604492, 3.5625, 11.460721969604492), + CubicToCommand(3.2607905864715576, 11.460721969604492, 3.0127127170562744, + 11.698558807373047, 3.0, 12.0), + LineToCommand(3.0, 19.312000274658203), + CubicToCommand(3.0011019706726074, 20.243799209594727, 3.7562003135681152, + 20.998897552490234, 4.688000202178955, 21.0), + LineToCommand(19.312000274658203, 21.0), + CubicToCommand(20.243410110473633, 20.998899459838867, 20.99834632873535, + 20.244409561157227, 21.0, 19.312999725341797), + LineToCommand(21.0, 12.0), + CloseCommand() + ]) +]; + +/// Excpected groupMask result when [MaskingOptimizer] is applied +List blendsAndMasksForMaskingOptimizer = [ + Path( + commands: const [ + MoveToCommand(50.0, 0.0), + CubicToCommand(77.5957512247, 0.0, 100.0, 22.4042487753, 100.0, 50.0), + CubicToCommand(100.0, 77.5957512247, 77.5957512247, 100.0, 50.0, 100.0), + CubicToCommand(22.4042487753, 100.0, 0.0, 77.5957512247, 0.0, 50.0), + CubicToCommand(0.0, 22.4042487753, 22.4042487753, 0.0, 50.0, 0.0), + CloseCommand(), + ], + ), + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(90.0, 50.0), + CubicToCommand( + 90.0, 27.923398971557617, 72.07659912109375, 10.0, 50.0, 10.0), + CubicToCommand( + 27.923398971557617, 10.0, 10.0, 27.923398971557617, 10.0, 50.0), + CubicToCommand( + 10.0, 72.07659912109375, 27.923398971557617, 90.0, 50.0, 90.0), + CubicToCommand( + 72.07659912109375, 90.0, 90.0, 72.07659912109375, 90.0, 50.0), + CloseCommand(), + ], + ), +]; + +/// Expected basicClips result when [Clipping Optimizer] is applied + +List basicClipsForClippingOptimzer = [ + Path( + fillType: PathFillType.evenOdd, + commands: const [ + MoveToCommand(50.0, 30.0), + CubicToCommand( + 50.0, 18.961700439453125, 41.038299560546875, 10.0, 30.0, 10.0), + CubicToCommand( + 18.961700439453125, 10.0, 10.0, 18.961700439453125, 10.0, 30.0), + CubicToCommand( + 10.0, 41.038299560546875, 18.961700439453125, 50.0, 30.0, 50.0), + CubicToCommand( + 41.038299560546875, 50.0, 50.0, 41.038299560546875, 50.0, 30.0), + CloseCommand(), + MoveToCommand(90.0, 70.0), + CubicToCommand( + 90.0, 58.961700439453125, 81.03829956054688, 50.0, 70.0, 50.0), + CubicToCommand( + 58.961700439453125, 50.0, 50.0, 58.961700439453125, 50.0, 70.0), + CubicToCommand( + 50.0, 81.03829956054688, 58.961700439453125, 90.0, 70.0, 90.0), + CubicToCommand( + 81.03829956054688, 90.0, 90.0, 81.03829956054688, 90.0, 70.0), + CloseCommand() + ], + ), +]; + +/// https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/bzrfeed.svg +const String signWithScaledStroke = ''' + + + + + + + + + + + + + + + + + + + + + + +'''; + +const String textTspan = ''' + + + + Some text + more text. + + + Even more text + text everywhere + so many lines + + + +'''; + +const String numberBubbles = ''' + + + + + + + + 2 + + + 3 + + + 1 + + + +'''; + +/// Via https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule +const String inheritFillRule = ''' + + + + + + + + +'''; diff --git a/packages/vector_graphics_compiler/test/theme_test.dart b/packages/vector_graphics_compiler/test/theme_test.dart new file mode 100644 index 00000000000..ce5a4b9e21e --- /dev/null +++ b/packages/vector_graphics_compiler/test/theme_test.dart @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/paint.dart'; +import 'package:vector_graphics_compiler/src/svg/theme.dart'; + +void main() { + group('SvgTheme', () { + group('constructor', () { + test('sets currentColor', () { + const Color currentColor = Color(0xFFB0E3BE); + + expect( + SvgTheme( + currentColor: currentColor, + ).currentColor, + equals(currentColor), + ); + }); + + test('sets fontSize', () { + const double fontSize = 14.0; + + expect( + SvgTheme( + currentColor: Color(0xFFB0E3BE), + ).fontSize, + equals(fontSize), + ); + }); + + test( + 'sets fontSize to 14 ' + 'by default', () { + expect( + SvgTheme(), + equals( + SvgTheme(), + ), + ); + }); + + test('sets xHeight', () { + const double xHeight = 8.0; + + expect( + SvgTheme( + fontSize: 26.0, + xHeight: xHeight, + ).xHeight, + equals(xHeight), + ); + }); + + test( + 'sets xHeight as fontSize divided by 2 ' + 'by default', () { + const double fontSize = 16.0; + + expect( + SvgTheme( + fontSize: fontSize, + ).xHeight, + equals(fontSize / 2), + ); + }); + }); + + test('supports value equality', () { + expect( + SvgTheme( + currentColor: Color(0xFF6F2173), + xHeight: 6.0, + ), + equals( + SvgTheme( + currentColor: Color(0xFF6F2173), + xHeight: 6.0, + ), + ), + ); + }); + }); +} diff --git a/packages/vector_graphics_compiler/test/util_test.dart b/packages/vector_graphics_compiler/test/util_test.dart new file mode 100644 index 00000000000..662da242508 --- /dev/null +++ b/packages/vector_graphics_compiler/test/util_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/src/util.dart'; + +void main() { + test('listEquals', () { + final List listA = [1, 2, 3]; + final List listB = [1, 2, 3]; + final List listC = [1, 2]; + final List listD = [3, 2, 1]; + + expect(listEquals(null, null), isTrue); + expect(listEquals(listA, null), isFalse); + expect(listEquals(null, listB), isFalse); + expect(listEquals(listA, listA), isTrue); + expect(listEquals(listA, listB), isTrue); + expect(listEquals(listA, listC), isFalse); + expect(listEquals(listA, listD), isFalse); + }); +} diff --git a/packages/vector_graphics_compiler/test/vertices_test.dart b/packages/vector_graphics_compiler/test/vertices_test.dart new file mode 100644 index 00000000000..965c0250adc --- /dev/null +++ b/packages/vector_graphics_compiler/test/vertices_test.dart @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +void main() { + test('Vertices.fromFloat32List', () { + final Vertices vertices = Vertices.fromFloat32List(Float32List.fromList( + [1, 2, 3, 4, 5, 6], + )); + + expect( + vertices.vertexPoints, + const [Point(1, 2), Point(3, 4), Point(5, 6)], + ); + + expect( + () => Vertices.fromFloat32List(Float32List.fromList([1])), + throwsA(isA()), + ); + }); + + test('IndexedVertices - creates valid index', () { + final Vertices vertices = Vertices.fromFloat32List(Float32List.fromList( + [ + 1, + 1, + 2, + 2, + 3, + 3, + 1, + 1, + 4, + 4, + 2, + 2, + 3, + 3, + 5, + 5, + 4, + 4, + 1, + 1, + 2, + 2, + 3, + 3 + ], + )); + + final IndexedVertices indexedVertices = vertices.createIndex(); + expect(indexedVertices.vertices.length, 10); + expect(indexedVertices.indices!.length, 12); + expect(indexedVertices.vertices, [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]); + expect( + indexedVertices.indices, [0, 1, 2, 0, 3, 1, 2, 4, 3, 0, 1, 2]); + }); + + test('IndexedVertices - does not index if index is larger', () { + final Float32List original = Float32List.fromList( + [1, 1, 2, 2, 3, 3, 1, 2, 4, 4, 2, 3, 3, 4, 5, 5, 4, 5], + ); + final Vertices vertices = Vertices.fromFloat32List(original); + + final IndexedVertices indexedVertices = vertices.createIndex(); + expect(indexedVertices.vertices, original); + expect(indexedVertices.indices, null); + }); +} diff --git a/packages/vector_graphics_compiler/test_data/example.svg b/packages/vector_graphics_compiler/test_data/example.svg new file mode 100644 index 00000000000..72aac66e184 --- /dev/null +++ b/packages/vector_graphics_compiler/test_data/example.svg @@ -0,0 +1,4 @@ + + + diff --git a/script/configs/allowed_pinned_deps.yaml b/script/configs/allowed_pinned_deps.yaml index 22aad2078c0..a224cca3a5a 100644 --- a/script/configs/allowed_pinned_deps.yaml +++ b/script/configs/allowed_pinned_deps.yaml @@ -1,4 +1,6 @@ # The list of external dependencies that are allowed as pinned dependencies. +# A pin can be either an exact version, or a range with an explicit, inclusive +# max version, which must a version that already exists, not a future version. # See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#Dependencies # # All entries here should have an explanation for why they are here. @@ -15,3 +17,9 @@ # This should be removed; see # https://github.com/flutter/flutter/issues/130897 - provider + +# Used by vector_graphics_compiler, as a production, user-facing dependency. +# This is allowed only with pinned dependencies, so that any changes can be +# audited before passing them on to clients as transitive updates, to mitigated +# the risk of the package being compromised. +- xml diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 34aa3813a41..c72e9dd8915 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -612,7 +612,8 @@ Please move them to dev_dependencies. if (constraint is VersionRange && constraint.min != null && constraint.max != null && - constraint.min == constraint.max) { + constraint.includeMin && + constraint.includeMax) { return true; } } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 2cf27027c7c..6fec0be3621 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -1674,7 +1674,8 @@ ${_topicsSection()} ); }); - test('passes when a pinned dependency is on the pinned allow list', + test( + 'passes when an exactly-pinned dependency is on the pinned allow list', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir); @@ -1701,6 +1702,34 @@ ${_topicsSection()} ); }); + test( + 'passes when an explicit-range-pinned dependency is on the pinned allow list', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection()} +${_dependenciesSection(['allow_pinned: ">=1.0.0 <=1.3.1"'])} +${_topicsSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + '--allow-pinned-dependencies', + 'allow_pinned' + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package...'), + contains('No issues found!'), + ]), + ); + }); + test('fails when an allowed-when-pinned dependency is unpinned', () async { final RepositoryPackage package =