diff --git a/.github/labeler.yml b/.github/labeler.yml index c1a7a366025..ec925d3343e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -138,6 +138,11 @@ - any-glob-to-any-file: - packages/palette_generator/**/* +'p: path_parsing': + - changed-files: + - any-glob-to-any-file: + - third_party/packages/path_parsing/**/* + 'p: path_provider': - changed-files: - any-glob-to-any-file: diff --git a/CODEOWNERS b/CODEOWNERS index bfdb5af89d8..0785e52ba2c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,6 +46,7 @@ packages/webview_flutter/** @bparrishMines packages/xdg_directories/** @stuartmorgan third_party/packages/cupertino_icons/** @MitchellGoodwin third_party/packages/cupertino_icons/test/goldens/** @LongCatIsLooong +third_party/packages/path_parsing/** @domesticmouse # Plugin platform implementation rules. These should stay last, since the last # matching entry takes precedence. diff --git a/README.md b/README.md index 08507abcb9f..e256308ebee 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ These are the packages hosted in this repository: | [metrics\_center](./packages/metrics_center/) | [![pub package](https://img.shields.io/pub/v/metrics_center.svg)](https://pub.dev/packages/metrics_center) | [![pub points](https://img.shields.io/pub/points/metrics_center)](https://pub.dev/packages/metrics_center/score) | [![popularity](https://img.shields.io/pub/popularity/metrics_center)](https://pub.dev/packages/metrics_center/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20metrics_center?label=)](https://github.com/flutter/flutter/labels/p%3A%20metrics_center) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20metrics_center?label=)](https://github.com/flutter/packages/labels/p%3A%20metrics_center) | | [multicast\_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) | [![pub points](https://img.shields.io/pub/points/multicast_dns)](https://pub.dev/packages/multicast_dns/score) | [![popularity](https://img.shields.io/pub/popularity/multicast_dns)](https://pub.dev/packages/multicast_dns/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20multicast_dns?label=)](https://github.com/flutter/flutter/labels/p%3A%20multicast_dns) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20multicast_dns?label=)](https://github.com/flutter/packages/labels/p%3A%20multicast_dns) | | [palette\_generator](./packages/palette_generator/) | [![pub package](https://img.shields.io/pub/v/palette_generator.svg)](https://pub.dev/packages/palette_generator) | [![pub points](https://img.shields.io/pub/points/palette_generator)](https://pub.dev/packages/palette_generator/score) | [![popularity](https://img.shields.io/pub/popularity/palette_generator)](https://pub.dev/packages/palette_generator/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20palette_generator?label=)](https://github.com/flutter/flutter/labels/p%3A%20palette_generator) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20palette_generator?label=)](https://github.com/flutter/packages/labels/p%3A%20palette_generator) | +| [path\_parsing](./third_party/packages/path_parsing/) | [![pub package](https://img.shields.io/pub/v/path_parsing.svg)](https://pub.dev/packages/path_parsing) | [![pub points](https://img.shields.io/pub/points/path_parsing)](https://pub.dev/packages/path_parsing/score) | [![popularity](https://img.shields.io/pub/popularity/path_parsing)](https://pub.dev/packages/path_parsing/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20path_parsing?label=)](https://github.com/flutter/flutter/labels/p%3A%20path_parsing) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20path_parsing?label=)](https://github.com/flutter/packages/labels/p%3A%20path_parsing) | | [path\_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | [![pub points](https://img.shields.io/pub/points/path_provider)](https://pub.dev/packages/path_provider/score) | [![popularity](https://img.shields.io/pub/popularity/path_provider)](https://pub.dev/packages/path_provider/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20path_provider?label=)](https://github.com/flutter/flutter/labels/p%3A%20path_provider) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20path_provider?label=)](https://github.com/flutter/packages/labels/p%3A%20path_provider) | | [pigeon](./packages/pigeon/) | [![pub package](https://img.shields.io/pub/v/pigeon.svg)](https://pub.dev/packages/pigeon) | [![pub points](https://img.shields.io/pub/points/pigeon)](https://pub.dev/packages/pigeon/score) | [![popularity](https://img.shields.io/pub/popularity/pigeon)](https://pub.dev/packages/pigeon/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20pigeon?label=)](https://github.com/flutter/flutter/labels/p%3A%20pigeon) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20pigeon?label=)](https://github.com/flutter/packages/labels/p%3A%20pigeon) | | [pointer\_interceptor](./packages/pointer_interceptor/) | [![pub package](https://img.shields.io/pub/v/pointer_interceptor.svg)](https://pub.dev/packages/pointer_interceptor) | [![pub points](https://img.shields.io/pub/points/pointer_interceptor)](https://pub.dev/packages/pointer_interceptor/score) | [![popularity](https://img.shields.io/pub/popularity/pointer_interceptor)](https://pub.dev/packages/pointer_interceptor/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20pointer_interceptor?label=)](https://github.com/flutter/flutter/labels/p%3A%20pointer_interceptor) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/packages/p%3A%20pointer_interceptor?label=)](https://github.com/flutter/packages/labels/p%3A%20pointer_interceptor) | diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 5f01cca7b96..40e716591b3 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -42,6 +42,13 @@ const Set _ignoredFullBasenameList = { 'resource.h', // Generated by VS. }; +// Third-party packages where the code doesn't have file-level annotation, just +// the package-level LICENSE file. Each entry must be a directory relative to +// third_party/packages, as that is the only directory where this is allowed. +const Set _unannotatedFileThirdPartyDirectories = { + 'path_parsing', +}; + // Copyright and license regexes for third-party code. // // These are intentionally very simple, since there is very little third-party @@ -69,6 +76,16 @@ final List _thirdPartyLicenseBlockRegexes = [ r'// Use of this source code is governed by a BSD-style license that can be\n' r'// found in the LICENSE file\.\n', ), + // packages/third_party/path_parsing. + RegExp( + r'Copyright \(c\) 2018 Dan Field\n\n' + r'Permission is hereby granted, free of charge, to any person obtaining a copy\n' + r'of this software and associated documentation files \(the "Software"\), to deal\n' + r'in the Software without restriction, including without limitation the rights\n' + r'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n' + r'copies of the Software, and to permit persons to whom the Software is\n' + r'furnished to do so, subject to the following conditions:', + ), ]; // The exact format of the BSD license that our license files should contain. @@ -217,10 +234,26 @@ class LicenseCheckCommand extends PackageCommand { for (final File file in codeFiles) { print('Checking ${file.path}'); + // Some third-party directories have code that doesn't annotate each file, + // so for those check the LICENSE file instead. This is done even though + // it's redundant to re-check it for each file because it ensures that we + // are still validating every file individually, rather than having a + // codepath where whole directories of files are ignored, which would have + // a much worse failure mode. + String content; + if (_unannotatedFileThirdPartyDirectories.any( + (String dir) => file.path.contains('/third_party/packages/$dir/'))) { + Directory packageDir = file.parent; + while (packageDir.parent.basename != 'packages') { + packageDir = packageDir.parent; + } + content = await packageDir.childFile('LICENSE').readAsString(); + } else { + content = await file.readAsString(); + } // On Windows, git may auto-convert line endings on checkout; this should // still pass since they will be converted back on commit. - final String content = - (await file.readAsString()).replaceAll('\r\n', '\n'); + content = content.replaceAll('\r\n', '\n'); final String firstParyLicense = firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? diff --git a/third_party/packages/path_parsing/.gitignore b/third_party/packages/path_parsing/.gitignore new file mode 100644 index 00000000000..43b5f446e6e --- /dev/null +++ b/third_party/packages/path_parsing/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ +.idea/ + +build/ + +.flutter-plugins diff --git a/third_party/packages/path_parsing/CHANGELOG.md b/third_party/packages/path_parsing/CHANGELOG.md new file mode 100644 index 00000000000..72251a67cd1 --- /dev/null +++ b/third_party/packages/path_parsing/CHANGELOG.md @@ -0,0 +1,47 @@ +## 1.0.2 + +* Transfers the package source from https://github.com/google/process.dart to + https://github.com/dnfield/dart_path_parsing. + +## 1.0.1 + +* Fix [bug in arc decomposition](https://github.com/dnfield/flutter_svg/issues/742). +* Minor code cleanup for analysis warnings. + +## 1.0.0 + +* Stable release. + +## 0.2.1 + +* Performance improvements to parsing. + +## 0.2.0 + +* Stable nullsafe release + +## 0.2.0-nullsafety.0 + +* Nullsafety migration. + +## 0.1.4 + +* Fix implementation of `_PathOffset`'s `==` operator. + +## 0.1.3 + +* Fix a bug in decompose cubic curve - avoid trying to call `toInt()` on `double.infinity` +* Bump test dependency. + +## 0.1.2 + +* Fix bug with smooth curve commands +* Add deep testing + +## 0.1.1 + +* Fix link to homepage in pubspec, add example + +## 0.1.0 + +* Initial release, based on the 0.2.4 release of path_drawing diff --git a/third_party/packages/path_parsing/LICENSE b/third_party/packages/path_parsing/LICENSE new file mode 100644 index 00000000000..246fd7b208c --- /dev/null +++ b/third_party/packages/path_parsing/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Dan Field + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/packages/path_parsing/METADATA b/third_party/packages/path_parsing/METADATA new file mode 100644 index 00000000000..6b54af3ce82 --- /dev/null +++ b/third_party/packages/path_parsing/METADATA @@ -0,0 +1,15 @@ +name: "path_parsing" +description: + "A pure Dart parsing library for SVG paths." + +third_party { + identifier { + type: "Git" + value: "https://github.com/dnfield/dart_path_parsing/" + primary_source: true + version: "6785396f6c5528c720adb14833b196e529e78998" + } + version: "6785396f6c5528c720adb14833b196e529e78998" + last_upgrade_date { year: 2024 month: 10 day: 24 } + license_type: NOTICE +} diff --git a/third_party/packages/path_parsing/README.md b/third_party/packages/path_parsing/README.md new file mode 100644 index 00000000000..9f725f69396 --- /dev/null +++ b/third_party/packages/path_parsing/README.md @@ -0,0 +1,5 @@ +# path_parsing + +Split from the Flutter path drawing library to create a pure Dart parsing +library for SVG paths and code generation (without dependencies on Flutter +runtime). diff --git a/third_party/packages/path_parsing/example/main.dart b/third_party/packages/path_parsing/example/main.dart new file mode 100644 index 00000000000..1d378235a46 --- /dev/null +++ b/third_party/packages/path_parsing/example/main.dart @@ -0,0 +1,51 @@ +// ignore_for_file: avoid_print + +import 'package:path_parsing/path_parsing.dart'; + +/// A [PathProxy] that dumps Flutter `Path` commands to the console. +class PathPrinter extends PathProxy { + @override + void close() { + print('Path.close();'); + } + + @override + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + print('Path.cubicTo($x1, $y1, $x2, $y2, $x3, $y3);'); + } + + @override + void lineTo(double x, double y) { + print('Path.lineTo($x, $y);'); + } + + @override + void moveTo(double x, double y) { + print('Path.moveTo($x, $y);'); + } +} + +void main() { + const String pathData = + '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'; + + writeSvgPathDataToPath(pathData, PathPrinter()); +} diff --git a/third_party/packages/path_parsing/example/pubspec.yaml b/third_party/packages/path_parsing/example/pubspec.yaml new file mode 100644 index 00000000000..dd9a6eef8dc --- /dev/null +++ b/third_party/packages/path_parsing/example/pubspec.yaml @@ -0,0 +1,9 @@ +name: path_parsing_example +publish_to: none + +environment: + sdk: ^3.3.0 + +dependencies: + path_parsing: + path: ../ diff --git a/third_party/packages/path_parsing/lib/path_parsing.dart b/third_party/packages/path_parsing/lib/path_parsing.dart new file mode 100644 index 00000000000..dd0ef045b24 --- /dev/null +++ b/third_party/packages/path_parsing/lib/path_parsing.dart @@ -0,0 +1 @@ +export 'src/path_parsing.dart'; diff --git a/third_party/packages/path_parsing/lib/src/path_parsing.dart b/third_party/packages/path_parsing/lib/src/path_parsing.dart new file mode 100644 index 00000000000..1a750a861c6 --- /dev/null +++ b/third_party/packages/path_parsing/lib/src/path_parsing.dart @@ -0,0 +1,773 @@ +// This code has been "translated" largely from the Chromium/blink source +// for SVG path parsing. +// The following files can be cross referenced to the classes and methods here: +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_parser_utilities.cc +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_parser_utilities.h +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_string_source.cc +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_string_source.h +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser.cc +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser.h +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/html/parser/html_parser_idioms.h (IsHTMLSpace) +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser_test.cc + +// TODO(stuartmorgan): Remove public_member_api_docs, adding documentation for +// all public members. +// TODO(stuartmorgan): Remove library_private_types_in_public_api and do a +// breaking change to not use _PathOffset in public APIs. +// ignore_for_file: public_member_api_docs, library_private_types_in_public_api + +import 'dart:math' as math show atan2, cos, max, pi, pow, sin, sqrt, tan; + +import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math.dart' show Matrix4, radians; + +import './path_segment_type.dart'; + +/// Parse `svg`, emitting the segment data to `path`. +void writeSvgPathDataToPath(String? svg, PathProxy path) { + if (svg == null || svg == '') { + return; + } + + final SvgPathStringSource parser = SvgPathStringSource(svg); + final SvgPathNormalizer normalizer = SvgPathNormalizer(); + for (final PathSegmentData seg in parser.parseSegments()) { + normalizer.emitSegment(seg, path); + } +} + +/// A receiver for normalized [PathSegmentData]. +abstract class PathProxy { + void moveTo(double x, double y); + void lineTo(double x, double y); + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ); + void close(); +} + +/// Provides a minimal implementation of a [Point] or [Offset]. +// Takes care of a few things Point doesn't, without requiring Flutter as dependency +@immutable +class _PathOffset { + const _PathOffset(this.dx, this.dy) + : assert(dx != null), // ignore: unnecessary_null_comparison + assert(dy != null); // ignore: unnecessary_null_comparison + + static _PathOffset get zero => const _PathOffset(0.0, 0.0); + final double dx; + final double dy; + + double get direction => math.atan2(dy, dx); + + _PathOffset translate(double translateX, double translateY) => + _PathOffset(dx + translateX, dy + translateY); + + _PathOffset operator +(_PathOffset other) => + _PathOffset(dx + other.dx, dy + other.dy); + _PathOffset operator -(_PathOffset other) => + _PathOffset(dx - other.dx, dy - other.dy); + + _PathOffset operator *(double operand) => + _PathOffset(dx * operand, dy * operand); + + @override + String toString() => 'PathOffset{$dx,$dy}'; + + @override + bool operator ==(Object other) { + return other is _PathOffset && other.dx == dx && other.dy == dy; + } + + // TODO(dnfield): Use a real hashing function - but this should at least be better than the default. + @override + int get hashCode => (((17 * 23) ^ dx.hashCode) * 23) ^ dy.hashCode; +} + +const double _twoPiFloat = math.pi * 2.0; +const double _piOverTwoFloat = math.pi / 2.0; + +class SvgPathStringSource { + SvgPathStringSource(this._string) + : assert(_string != null), // ignore: unnecessary_null_comparison + _previousCommand = SvgPathSegType.unknown, + _idx = 0, + _length = _string.length { + _skipOptionalSvgSpaces(); + } + + final String _string; + + SvgPathSegType _previousCommand; + int _idx; + final int _length; + + bool _isHtmlSpace(int character) { + // Histogram from Apple's page load test combined with some ad hoc browsing + // some other test suites. + // + // 82%: 216330 non-space characters, all > U+0020 + // 11%: 30017 plain space characters, U+0020 + // 5%: 12099 newline characters, U+000A + // 2%: 5346 tab characters, U+0009 + // + // No other characters seen. No U+000C or U+000D, and no other control + // characters. Accordingly, we check for non-spaces first, then space, then + // newline, then tab, then the other characters. + + return character <= AsciiConstants.space && + (character == AsciiConstants.space || + character == AsciiConstants.slashN || + character == AsciiConstants.slashT || + character == AsciiConstants.slashR || + character == AsciiConstants.slashF); + } + + /// Increments _idx to the first non-space character. + /// + /// Returns the code unit of the first non-space, or -1 if at end of string. + int _skipOptionalSvgSpaces() { + while (true) { + if (_idx >= _length) { + return -1; + } + + final int c = _string.codeUnitAt(_idx); + if (!_isHtmlSpace(c)) { + return c; + } + + _idx++; + } + } + + void _skipOptionalSvgSpacesOrDelimiter( + [int delimiter = AsciiConstants.comma]) { + final int c = _skipOptionalSvgSpaces(); + if (c == delimiter) { + _idx++; + _skipOptionalSvgSpaces(); + } + } + + static bool _isNumberStart(int lookahead) { + return (lookahead >= AsciiConstants.number0 && + lookahead <= AsciiConstants.number9) || + lookahead == AsciiConstants.plus || + lookahead == AsciiConstants.minus || + lookahead == AsciiConstants.period; + } + + SvgPathSegType _maybeImplicitCommand( + int lookahead, + SvgPathSegType nextCommand, + ) { + // Check if the current lookahead may start a number - in which case it + // could be the start of an implicit command. The 'close' command does not + // have any parameters though and hence can't have an implicit + // 'continuation'. + if (!_isNumberStart(lookahead) || + _previousCommand == SvgPathSegType.close) { + return nextCommand; + } + // Implicit continuations of moveto command translate to linetos. + if (_previousCommand == SvgPathSegType.moveToAbs) { + return SvgPathSegType.lineToAbs; + } + if (_previousCommand == SvgPathSegType.moveToRel) { + return SvgPathSegType.lineToRel; + } + return _previousCommand; + } + + bool _isValidRange(double x) => + -double.maxFinite <= x && x <= double.maxFinite; + + bool _isValidExponent(double x) => -37 <= x && x <= 38; + + /// Reads a code unit and advances the index. + /// + /// Returns -1 if at end of string. + @pragma('vm:prefer-inline') + int _readCodeUnit() { + if (_idx >= _length) { + return -1; + } + return _string.codeUnitAt(_idx++); + } + + // We use this generic parseNumber function to allow the Path parsing code to + // work at a higher precision internally, without any unnecessary runtime cost + // or code complexity. + double _parseNumber() { + _skipOptionalSvgSpaces(); + + // Read the sign. + int sign = 1; + int c = _readCodeUnit(); + if (c == AsciiConstants.plus) { + c = _readCodeUnit(); + } else if (c == AsciiConstants.minus) { + sign = -1; + c = _readCodeUnit(); + } + + if ((c < AsciiConstants.number0 || c > AsciiConstants.number9) && + c != AsciiConstants.period) { + throw StateError('First character of a number must be one of [0-9+-.].'); + } + + // Read the integer part, build left-to-right. + double integer = 0.0; + while (AsciiConstants.number0 <= c && c <= AsciiConstants.number9) { + integer = integer * 10 + (c - AsciiConstants.number0); + c = _readCodeUnit(); + } + + // Bail out early if this overflows. + if (!_isValidRange(integer)) { + throw StateError('Numeric overflow'); + } + + double decimal = 0.0; + if (c == AsciiConstants.period) { + // read the decimals + c = _readCodeUnit(); + + // There must be a least one digit following the . + if (c < AsciiConstants.number0 || c > AsciiConstants.number9) { + throw StateError('There must be at least one digit following the .'); + } + + double frac = 1.0; + while (AsciiConstants.number0 <= c && c <= AsciiConstants.number9) { + frac *= 0.1; + decimal += (c - AsciiConstants.number0) * frac; + c = _readCodeUnit(); + } + } + + double number = integer + decimal; + number *= sign; + + // read the exponent part + if (_idx < _length && + (c == AsciiConstants.lowerE || c == AsciiConstants.upperE) && + (_string.codeUnitAt(_idx) != AsciiConstants.lowerX && + _string.codeUnitAt(_idx) != AsciiConstants.lowerM)) { + c = _readCodeUnit(); + + // read the sign of the exponent + bool exponentIsNegative = false; + if (c == AsciiConstants.plus) { + c = _readCodeUnit(); + } else if (c == AsciiConstants.minus) { + c = _readCodeUnit(); + exponentIsNegative = true; + } + + // There must be an exponent + if (c < AsciiConstants.number0 || c > AsciiConstants.number9) { + throw StateError('Missing exponent'); + } + + double exponent = 0.0; + while (c >= AsciiConstants.number0 && c <= AsciiConstants.number9) { + exponent *= 10.0; + exponent += c - AsciiConstants.number0; + c = _readCodeUnit(); + } + if (exponentIsNegative) { + exponent = -exponent; + } + // Make sure exponent is valid. + if (!_isValidExponent(exponent)) { + throw StateError('Invalid exponent $exponent'); + } + if (exponent != 0) { + number *= math.pow(10.0, exponent); + } + } + + // Don't return Infinity() or NaN(). + if (!_isValidRange(number)) { + throw StateError('Numeric overflow'); + } + + // At this stage, c contains an unprocessed character, and _idx has + // already been incremented. + + // If c == -1, the input was already at the end of the string, so no + // further processing needs to occur. + if (c != -1) { + --_idx; // Put the unprocessed character back. + + // if (mode & kAllowTrailingWhitespace) + _skipOptionalSvgSpacesOrDelimiter(); + } + return number; + } + + bool _parseArcFlag() { + if (!hasMoreData) { + throw StateError('Expected more data'); + } + final int flagChar = _string.codeUnitAt(_idx++); + _skipOptionalSvgSpacesOrDelimiter(); + + if (flagChar == AsciiConstants.number0) { + return false; + } else if (flagChar == AsciiConstants.number1) { + return true; + } else { + throw StateError('Invalid flag value'); + } + } + + bool get hasMoreData => _idx < _length; + + Iterable parseSegments() sync* { + while (hasMoreData) { + yield parseSegment(); + } + } + + PathSegmentData parseSegment() { + assert(hasMoreData); + final PathSegmentData segment = PathSegmentData(); + final int lookahead = _string.codeUnitAt(_idx); + SvgPathSegType command = AsciiConstants.mapLetterToSegmentType(lookahead); + if (_previousCommand == SvgPathSegType.unknown) { + // First command has to be a moveto. + if (command != SvgPathSegType.moveToRel && + command != SvgPathSegType.moveToAbs) { + throw StateError('Expected to find moveTo command'); + } + // Consume command letter. + _idx++; + } else if (command == SvgPathSegType.unknown) { + // Possibly an implicit command. + assert(_previousCommand != SvgPathSegType.unknown); + command = _maybeImplicitCommand(lookahead, command); + if (command == SvgPathSegType.unknown) { + throw StateError('Expected a path command'); + } + } else { + // Valid explicit command. + _idx++; + } + + segment.command = _previousCommand = command; + + switch (segment.command) { + case SvgPathSegType.cubicToRel: + case SvgPathSegType.cubicToAbs: + segment.point1 = _PathOffset(_parseNumber(), _parseNumber()); + continue cubic_smooth; + case SvgPathSegType.smoothCubicToRel: + cubic_smooth: + case SvgPathSegType.smoothCubicToAbs: + segment.point2 = _PathOffset(_parseNumber(), _parseNumber()); + continue quad_smooth; + case SvgPathSegType.moveToRel: + case SvgPathSegType.moveToAbs: + case SvgPathSegType.lineToRel: + case SvgPathSegType.lineToAbs: + case SvgPathSegType.smoothQuadToRel: + quad_smooth: + case SvgPathSegType.smoothQuadToAbs: + segment.targetPoint = _PathOffset(_parseNumber(), _parseNumber()); + case SvgPathSegType.lineToHorizontalRel: + case SvgPathSegType.lineToHorizontalAbs: + segment.targetPoint = + _PathOffset(_parseNumber(), segment.targetPoint.dy); + case SvgPathSegType.lineToVerticalRel: + case SvgPathSegType.lineToVerticalAbs: + segment.targetPoint = + _PathOffset(segment.targetPoint.dx, _parseNumber()); + case SvgPathSegType.close: + _skipOptionalSvgSpaces(); + case SvgPathSegType.quadToRel: + case SvgPathSegType.quadToAbs: + segment.point1 = _PathOffset(_parseNumber(), _parseNumber()); + segment.targetPoint = _PathOffset(_parseNumber(), _parseNumber()); + case SvgPathSegType.arcToRel: + case SvgPathSegType.arcToAbs: + segment.point1 = _PathOffset(_parseNumber(), _parseNumber()); + segment.arcAngle = _parseNumber(); + segment.arcLarge = _parseArcFlag(); + segment.arcSweep = _parseArcFlag(); + segment.targetPoint = _PathOffset(_parseNumber(), _parseNumber()); + case SvgPathSegType.unknown: + throw StateError('Unknown segment command'); + } + + return segment; + } +} + +_PathOffset reflectedPoint( + _PathOffset reflectedIn, _PathOffset pointToReflect) { + return _PathOffset(2 * reflectedIn.dx - pointToReflect.dx, + 2 * reflectedIn.dy - pointToReflect.dy); +} + +const double _kOneOverThree = 1.0 / 3.0; + +/// Blend the points with a ratio (1/3):(2/3). +_PathOffset blendPoints(_PathOffset p1, _PathOffset p2) { + return _PathOffset((p1.dx + 2 * p2.dx) * _kOneOverThree, + (p1.dy + 2 * p2.dy) * _kOneOverThree); +} + +bool isCubicCommand(SvgPathSegType command) { + return command == SvgPathSegType.cubicToAbs || + command == SvgPathSegType.cubicToRel || + command == SvgPathSegType.smoothCubicToAbs || + command == SvgPathSegType.smoothCubicToRel; +} + +bool isQuadraticCommand(SvgPathSegType command) { + return command == SvgPathSegType.quadToAbs || + command == SvgPathSegType.quadToRel || + command == SvgPathSegType.smoothQuadToAbs || + command == SvgPathSegType.smoothQuadToRel; +} + +// TODO(dnfield): This can probably be cleaned up a bit. Some of this was designed in such a way to pack data/optimize for C++ +// There are probably better/clearer ways to do it for Dart. +class PathSegmentData { + PathSegmentData() + : command = SvgPathSegType.unknown, + arcSweep = false, + arcLarge = false; + + _PathOffset get arcRadii => point1; + + /// Angle in degrees. + double get arcAngle => point2.dx; + + /// In degrees. + set arcAngle(double angle) => point2 = _PathOffset(angle, point2.dy); + + double get r1 => arcRadii.dx; + double get r2 => arcRadii.dy; + + bool get largeArcFlag => arcLarge; + bool get sweepFlag => arcSweep; + + double get x => targetPoint.dx; + double get y => targetPoint.dy; + + double get x1 => point1.dx; + double get y1 => point1.dy; + + double get x2 => point2.dx; + double get y2 => point2.dy; + + SvgPathSegType command; + _PathOffset targetPoint = _PathOffset.zero; + _PathOffset point1 = _PathOffset.zero; + _PathOffset point2 = _PathOffset.zero; + bool arcSweep; + bool arcLarge; + + @override + String toString() { + return 'PathSegmentData{$command $targetPoint $point1 $point2 $arcSweep $arcLarge}'; + } +} + +class SvgPathNormalizer { + _PathOffset _currentPoint = _PathOffset.zero; + _PathOffset _subPathPoint = _PathOffset.zero; + _PathOffset _controlPoint = _PathOffset.zero; + SvgPathSegType _lastCommand = SvgPathSegType.unknown; + + void emitSegment(PathSegmentData segment, PathProxy path) { + final PathSegmentData normSeg = segment; + assert(_currentPoint != null); // ignore: unnecessary_null_comparison + // Convert relative points to absolute points. + switch (segment.command) { + case SvgPathSegType.quadToRel: + normSeg.point1 += _currentPoint; + normSeg.targetPoint += _currentPoint; + case SvgPathSegType.cubicToRel: + normSeg.point1 += _currentPoint; + continue smooth_rel; + smooth_rel: + case SvgPathSegType.smoothCubicToRel: + normSeg.point2 += _currentPoint; + continue arc_rel; + case SvgPathSegType.moveToRel: + case SvgPathSegType.lineToRel: + case SvgPathSegType.lineToHorizontalRel: + case SvgPathSegType.lineToVerticalRel: + case SvgPathSegType.smoothQuadToRel: + arc_rel: + case SvgPathSegType.arcToRel: + normSeg.targetPoint += _currentPoint; + case SvgPathSegType.lineToHorizontalAbs: + normSeg.targetPoint = + _PathOffset(normSeg.targetPoint.dx, _currentPoint.dy); + case SvgPathSegType.lineToVerticalAbs: + normSeg.targetPoint = + _PathOffset(_currentPoint.dx, normSeg.targetPoint.dy); + case SvgPathSegType.close: + // Reset m_currentPoint for the next path. + normSeg.targetPoint = _subPathPoint; + // This switch is intentionally non-exhaustive. + // ignore: no_default_cases + default: + break; + } + + // Update command verb, handle smooth segments and convert quadratic curve + // segments to cubics. + switch (segment.command) { + case SvgPathSegType.moveToRel: + case SvgPathSegType.moveToAbs: + _subPathPoint = normSeg.targetPoint; + // normSeg.command = SvgPathSegType.moveToAbs; + path.moveTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); + case SvgPathSegType.lineToRel: + case SvgPathSegType.lineToAbs: + case SvgPathSegType.lineToHorizontalRel: + case SvgPathSegType.lineToHorizontalAbs: + case SvgPathSegType.lineToVerticalRel: + case SvgPathSegType.lineToVerticalAbs: + // normSeg.command = SvgPathSegType.lineToAbs; + path.lineTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); + case SvgPathSegType.close: + // normSeg.command = SvgPathSegType.close; + path.close(); + case SvgPathSegType.smoothCubicToRel: + case SvgPathSegType.smoothCubicToAbs: + if (!isCubicCommand(_lastCommand)) { + normSeg.point1 = _currentPoint; + } else { + normSeg.point1 = reflectedPoint( + _currentPoint, + _controlPoint, + ); + } + continue cubic_abs2; + case SvgPathSegType.cubicToRel: + cubic_abs2: + case SvgPathSegType.cubicToAbs: + _controlPoint = normSeg.point2; + // normSeg.command = SvgPathSegType.cubicToAbs; + path.cubicTo( + normSeg.point1.dx, + normSeg.point1.dy, + normSeg.point2.dx, + normSeg.point2.dy, + normSeg.targetPoint.dx, + normSeg.targetPoint.dy, + ); + case SvgPathSegType.smoothQuadToRel: + case SvgPathSegType.smoothQuadToAbs: + if (!isQuadraticCommand(_lastCommand)) { + normSeg.point1 = _currentPoint; + } else { + normSeg.point1 = reflectedPoint( + _currentPoint, + _controlPoint, + ); + } + continue quad_abs2; + case SvgPathSegType.quadToRel: + quad_abs2: + case SvgPathSegType.quadToAbs: + // Save the unmodified control point. + _controlPoint = normSeg.point1; + normSeg.point1 = blendPoints(_currentPoint, _controlPoint); + normSeg.point2 = blendPoints( + normSeg.targetPoint, + _controlPoint, + ); + // normSeg.command = SvgPathSegType.cubicToAbs; + path.cubicTo( + normSeg.point1.dx, + normSeg.point1.dy, + normSeg.point2.dx, + normSeg.point2.dy, + normSeg.targetPoint.dx, + normSeg.targetPoint.dy, + ); + case SvgPathSegType.arcToRel: + case SvgPathSegType.arcToAbs: + if (!_decomposeArcToCubic(_currentPoint, normSeg, path)) { + // On failure, emit a line segment to the target point. + // normSeg.command = SvgPathSegType.lineToAbs; + path.lineTo(normSeg.targetPoint.dx, normSeg.targetPoint.dy); + // } else { + // // decomposeArcToCubic() has already emitted the normalized + // // segments, so set command to PathSegArcAbs, to skip any further + // // emit. + // // normSeg.command = SvgPathSegType.arcToAbs; + } + // This switch is intentionally non-exhaustive. + // ignore: no_default_cases + default: + throw StateError('Invalid command type in path'); + } + + _currentPoint = normSeg.targetPoint; + + if (!isCubicCommand(segment.command) && + !isQuadraticCommand(segment.command)) { + _controlPoint = _currentPoint; + } + + _lastCommand = segment.command; + } + +// This works by converting the SVG arc to "simple" beziers. +// Partly adapted from Niko's code in kdelibs/kdecore/svgicons. +// See also SVG implementation notes: +// http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter + bool _decomposeArcToCubic( + _PathOffset currentPoint, + PathSegmentData arcSegment, + PathProxy path, + ) { + // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a + // "lineto") joining the endpoints. + // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters + double rx = arcSegment.arcRadii.dx.abs(); + double ry = arcSegment.arcRadii.dy.abs(); + if (rx == 0 || ry == 0) { + return false; + } + + // If the current point and target point for the arc are identical, it should + // be treated as a zero length path. This ensures continuity in animations. + if (arcSegment.targetPoint == currentPoint) { + return false; + } + + final double angle = radians(arcSegment.arcAngle); + + final _PathOffset midPointDistance = + (currentPoint - arcSegment.targetPoint) * 0.5; + + final Matrix4 pointTransform = Matrix4.identity(); + pointTransform.rotateZ(-angle); + + final _PathOffset transformedMidPoint = _mapPoint( + pointTransform, + _PathOffset( + midPointDistance.dx, + midPointDistance.dy, + ), + ); + + final double squareRx = rx * rx; + final double squareRy = ry * ry; + final double squareX = transformedMidPoint.dx * transformedMidPoint.dx; + final double squareY = transformedMidPoint.dy * transformedMidPoint.dy; + + // Check if the radii are big enough to draw the arc, scale radii if not. + // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii + final double radiiScale = squareX / squareRx + squareY / squareRy; + if (radiiScale > 1.0) { + rx *= math.sqrt(radiiScale); + ry *= math.sqrt(radiiScale); + } + pointTransform.setIdentity(); + + pointTransform.scale(1.0 / rx, 1.0 / ry); + pointTransform.rotateZ(-angle); + + _PathOffset point1 = _mapPoint(pointTransform, currentPoint); + _PathOffset point2 = _mapPoint(pointTransform, arcSegment.targetPoint); + _PathOffset delta = point2 - point1; + + final double d = delta.dx * delta.dx + delta.dy * delta.dy; + final double scaleFactorSquared = math.max(1.0 / d - 0.25, 0.0); + double scaleFactor = math.sqrt(scaleFactorSquared); + if (!scaleFactor.isFinite) { + scaleFactor = 0.0; + } + + if (arcSegment.arcSweep == arcSegment.arcLarge) { + scaleFactor = -scaleFactor; + } + + delta = delta * scaleFactor; + final _PathOffset centerPoint = + ((point1 + point2) * 0.5).translate(-delta.dy, delta.dx); + + final double theta1 = (point1 - centerPoint).direction; + final double theta2 = (point2 - centerPoint).direction; + + double thetaArc = theta2 - theta1; + + if (thetaArc < 0.0 && arcSegment.arcSweep) { + thetaArc += _twoPiFloat; + } else if (thetaArc > 0.0 && !arcSegment.arcSweep) { + thetaArc -= _twoPiFloat; + } + + pointTransform.setIdentity(); + pointTransform.rotateZ(angle); + pointTransform.scale(rx, ry); + + // Some results of atan2 on some platform implementations are not exact + // enough. So that we get more cubic curves than expected here. Adding 0.001f + // reduces the count of segments to the correct count. + final int segments = (thetaArc / (_piOverTwoFloat + 0.001)).abs().ceil(); + for (int i = 0; i < segments; ++i) { + final double startTheta = theta1 + i * thetaArc / segments; + final double endTheta = theta1 + (i + 1) * thetaArc / segments; + + final double t = (8.0 / 6.0) * math.tan(0.25 * (endTheta - startTheta)); + if (!t.isFinite) { + return false; + } + final double sinStartTheta = math.sin(startTheta); + final double cosStartTheta = math.cos(startTheta); + final double sinEndTheta = math.sin(endTheta); + final double cosEndTheta = math.cos(endTheta); + + point1 = _PathOffset( + cosStartTheta - t * sinStartTheta, + sinStartTheta + t * cosStartTheta, + ).translate(centerPoint.dx, centerPoint.dy); + final _PathOffset targetPoint = _PathOffset( + cosEndTheta, + sinEndTheta, + ).translate(centerPoint.dx, centerPoint.dy); + point2 = targetPoint.translate(t * sinEndTheta, -t * cosEndTheta); + + final PathSegmentData cubicSegment = PathSegmentData(); + cubicSegment.command = SvgPathSegType.cubicToAbs; + cubicSegment.point1 = _mapPoint(pointTransform, point1); + cubicSegment.point2 = _mapPoint(pointTransform, point2); + cubicSegment.targetPoint = _mapPoint(pointTransform, targetPoint); + + path.cubicTo(cubicSegment.x1, cubicSegment.y1, cubicSegment.x2, + cubicSegment.y2, cubicSegment.x, cubicSegment.y); + //consumer_->EmitSegment(cubicSegment); + } + return true; + } + + _PathOffset _mapPoint(Matrix4 transform, _PathOffset point) { + // a, b, 0.0, 0.0, c, d, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, e, f, 0.0, 1.0 + return _PathOffset( + transform.storage[0] * point.dx + + transform.storage[4] * point.dy + + transform.storage[12], + transform.storage[1] * point.dx + + transform.storage[5] * point.dy + + transform.storage[13], + ); + } +} diff --git a/third_party/packages/path_parsing/lib/src/path_segment_type.dart b/third_party/packages/path_parsing/lib/src/path_segment_type.dart new file mode 100644 index 00000000000..0c2c097cb6d --- /dev/null +++ b/third_party/packages/path_parsing/lib/src/path_segment_type.dart @@ -0,0 +1,234 @@ +/// SvgPathSegType enumerates the various path segment commands. +/// +/// [AsciiConstants] houses the ASCII numeric values of these commands +enum SvgPathSegType { + /// Indicates initial state or error + unknown, + + /// Z or z + close, + + /// M + moveToAbs, + + /// m + moveToRel, + + /// L + lineToAbs, + + /// l + lineToRel, + + /// C + cubicToAbs, + + /// c + cubicToRel, + + /// Q + quadToAbs, + + /// q + quadToRel, + + /// A + arcToAbs, + + /// a + arcToRel, + + /// H + lineToHorizontalAbs, + + /// h + lineToHorizontalRel, + + /// V + lineToVerticalAbs, + + /// v + lineToVerticalRel, + + /// S + smoothCubicToAbs, + + /// s + smoothCubicToRel, + + /// T + smoothQuadToAbs, + + /// t + smoothQuadToRel +} + +/// Character constants used internally. Note that this parser does not +/// properly support non-ascii characters in the path, but it does support +/// unicode encoding. +/// +/// Only contains values that are used by the parser (does not contain the full +/// ASCII set). +class AsciiConstants { + const AsciiConstants._(); + + /// Returns the segment type corresponding to the letter constant [lookahead]. + static SvgPathSegType mapLetterToSegmentType(int lookahead) { + return AsciiConstants.letterToSegmentType[lookahead] ?? + SvgPathSegType.unknown; + } + + /// Map to go from ASCII constant to [SvgPathSegType] + static const Map letterToSegmentType = + { + upperZ: SvgPathSegType.close, + lowerZ: SvgPathSegType.close, + upperM: SvgPathSegType.moveToAbs, + lowerM: SvgPathSegType.moveToRel, + upperL: SvgPathSegType.lineToAbs, + lowerL: SvgPathSegType.lineToRel, + upperC: SvgPathSegType.cubicToAbs, + lowerC: SvgPathSegType.cubicToRel, + upperQ: SvgPathSegType.quadToAbs, + lowerQ: SvgPathSegType.quadToRel, + upperA: SvgPathSegType.arcToAbs, + lowerA: SvgPathSegType.arcToRel, + upperH: SvgPathSegType.lineToHorizontalAbs, + lowerH: SvgPathSegType.lineToHorizontalRel, + upperV: SvgPathSegType.lineToVerticalAbs, + lowerV: SvgPathSegType.lineToVerticalRel, + upperS: SvgPathSegType.smoothCubicToAbs, + lowerS: SvgPathSegType.smoothCubicToRel, + upperT: SvgPathSegType.smoothQuadToAbs, + lowerT: SvgPathSegType.smoothQuadToRel, + }; + + /// `\t` (horizontal tab). + static const int slashT = 9; + + /// `\n` (newline). + static const int slashN = 10; + + /// `\f` (form feed). + static const int slashF = 12; + + /// `\r` (carriage return). + static const int slashR = 13; + + /// ` ` (space). + static const int space = 32; + + /// `+` (plus). + static const int plus = 43; + + /// `,` (comma). + static const int comma = 44; + + /// `-` (minus). + static const int minus = 45; + + /// `.` (period). + static const int period = 46; + + /// 0 (the number zero). + static const int number0 = 48; + + /// 1 (the number one). + static const int number1 = 49; + + /// 2 (the number two). + static const int number2 = 50; + + /// 3 (the number three). + static const int number3 = 51; + + /// 4 (the number four). + static const int number4 = 52; + + /// 5 (the number five). + static const int number5 = 53; + + /// 6 (the number six). + static const int number6 = 54; + + /// 7 (the number seven). + static const int number7 = 55; + + /// 8 (the number eight). + static const int number8 = 56; + + /// 9 (the number nine). + static const int number9 = 57; + + /// A + static const int upperA = 65; + + /// C + static const int upperC = 67; + + /// E + static const int upperE = 69; + + /// H + static const int upperH = 72; + + /// L + static const int upperL = 76; + + /// M + static const int upperM = 77; + + /// Q + static const int upperQ = 81; + + /// S + static const int upperS = 83; + + /// T + static const int upperT = 84; + + /// V + static const int upperV = 86; + + /// Z + static const int upperZ = 90; + + /// a + static const int lowerA = 97; + + /// c + static const int lowerC = 99; + + /// e + static const int lowerE = 101; + + /// h + static const int lowerH = 104; + + /// l + static const int lowerL = 108; + + /// m + static const int lowerM = 109; + + /// q + static const int lowerQ = 113; + + /// s + static const int lowerS = 115; + + /// t + static const int lowerT = 116; + + /// v + static const int lowerV = 118; + + /// x + static const int lowerX = 120; + + /// z + static const int lowerZ = 122; + + /// `~` (tilde) + static const int tilde = 126; +} diff --git a/third_party/packages/path_parsing/pubspec.yaml b/third_party/packages/path_parsing/pubspec.yaml new file mode 100644 index 00000000000..49bc7b2b55a --- /dev/null +++ b/third_party/packages/path_parsing/pubspec.yaml @@ -0,0 +1,21 @@ +name: path_parsing +description: > + A Dart library to help with SVG Path parsing and code generation. Used by Flutter SVG. +repository: https://github.com/flutter/packages/tree/main/third_party/packages/path_parsing +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_parsing%22 +version: 1.0.2 + +environment: + sdk: ^3.3.0 + +dependencies: + meta: ^1.3.0 + vector_math: ^2.1.0 + +dev_dependencies: + path: ^1.8.0 + test: ^1.16.0 + +topics: + - svg + - vector-graphics diff --git a/third_party/packages/path_parsing/test/parse_path_deep_test.dart b/third_party/packages/path_parsing/test/parse_path_deep_test.dart new file mode 100644 index 00000000000..8839f068f3e --- /dev/null +++ b/third_party/packages/path_parsing/test/parse_path_deep_test.dart @@ -0,0 +1,71 @@ +import 'package:path_parsing/path_parsing.dart'; +import 'package:test/test.dart'; + +class DeepTestPathProxy extends PathProxy { + DeepTestPathProxy(this.expectedCommands); + + final List expectedCommands; + final List actualCommands = []; + + @override + void close() { + actualCommands.add('close()'); + } + + @override + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + actualCommands.add( + 'cubicTo(${x1.toStringAsFixed(4)}, ${y1.toStringAsFixed(4)}, ${x2.toStringAsFixed(4)}, ${y2.toStringAsFixed(4)}, ${x3.toStringAsFixed(4)}, ${y3.toStringAsFixed(4)})'); + } + + @override + void lineTo(double x, double y) { + actualCommands + .add('lineTo(${x.toStringAsFixed(4)}, ${y.toStringAsFixed(4)})'); + } + + @override + void moveTo(double x, double y) { + actualCommands + .add('moveTo(${x.toStringAsFixed(4)}, ${y.toStringAsFixed(4)})'); + } + + void validate() { + expect(expectedCommands, orderedEquals(actualCommands)); + } +} + +void main() { + void assertValidPath(String input, List commands) { + final DeepTestPathProxy proxy = DeepTestPathProxy(commands); + writeSvgPathDataToPath(input, proxy); + proxy.validate(); + } + + test('Deep path validation', () { + assertValidPath('M20,30 Q40,5 60,30 T100,30', [ + 'moveTo(20.0000, 30.0000)', + 'cubicTo(33.3333, 13.3333, 46.6667, 13.3333, 60.0000, 30.0000)', + 'cubicTo(73.3333, 46.6667, 86.6667, 46.6667, 100.0000, 30.0000)' + ]); + + assertValidPath( + 'M5.5 5.5a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5z', [ + 'moveTo(5.5000, 5.5000)', + 'cubicTo(5.2319, 5.9667, 4.9001, 6.3513, 4.6307, 6.5077)', + 'cubicTo(4.3612, 6.6640, 4.1953, 6.5683, 4.1960, 6.2567)', + 'cubicTo(4.1967, 5.9451, 4.3638, 5.4655, 4.6340, 5.0000)', + 'cubicTo(4.9021, 4.5333, 5.2339, 4.1487, 5.5033, 3.9923)', + 'cubicTo(5.7728, 3.8360, 5.9387, 3.9317, 5.9380, 4.2433)', + 'cubicTo(5.9373, 4.5549, 5.7702, 5.0345, 5.5000, 5.5000)', + 'close()' + ]); + }); +} diff --git a/third_party/packages/path_parsing/test/parse_path_test.dart b/third_party/packages/path_parsing/test/parse_path_test.dart new file mode 100644 index 00000000000..0f8a5117e2a --- /dev/null +++ b/third_party/packages/path_parsing/test/parse_path_test.dart @@ -0,0 +1,248 @@ +// Test paths taken from: +// * https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/svg/svg_path_parser_test.cc + +import 'package:path_parsing/path_parsing.dart'; +import 'package:path_parsing/src/path_segment_type.dart'; +import 'package:test/test.dart'; + +// TODO(dnfield): a bunch of better tests could be written to track that commands are actually called with expected values/order +// For now we just want to know that something gets emitted and no exceptions are thrown (that's all the legacy tests really did anyway). +class TestPathProxy extends PathProxy { + bool called = false; + @override + void close() { + called = true; + } + + @override + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + called = true; + } + + @override + void lineTo(double x, double y) { + called = true; + } + + @override + void moveTo(double x, double y) { + called = true; + } +} + +void main() { + void assertValidPath(String input) { + final TestPathProxy proxy = TestPathProxy(); + // these shouldn't throw or assert + writeSvgPathDataToPath(input, proxy); + expect(proxy.called, true); + } + + void assertInvalidPath(String input) { + expect( + () => writeSvgPathDataToPath(input, TestPathProxy()), throwsStateError); + } + + test('Valid Paths', () { + assertValidPath('M1,2'); + assertValidPath('m1,2'); + assertValidPath('M100,200 m3,4'); + assertValidPath('M100,200 L3,4'); + assertValidPath('M100,200 l3,4'); + assertValidPath('M100,200 H3'); + assertValidPath('M100,200 h3'); + assertValidPath('M100,200 V3'); + assertValidPath('M100,200 v3'); + assertValidPath('M100,200 Z'); + assertValidPath('M100,200 z'); + assertValidPath('M100,200 C3,4,5,6,7,8'); + assertValidPath('M100,200 c3,4,5,6,7,8'); + assertValidPath('M100,200 S3,4,5,6'); + assertValidPath('M100,200 s3,4,5,6'); + assertValidPath('M100,200 Q3,4,5,6'); + assertValidPath('M100,200 q3,4,5,6'); + assertValidPath('M100,200 T3,4'); + assertValidPath('M100,200 t3,4'); + assertValidPath('M100,200 A3,4,5,0,0,6,7'); + assertValidPath('M100,200 A3,4,5,1,0,6,7'); + assertValidPath('M100,200 A3,4,5,0,1,6,7'); + assertValidPath('M100,200 A3,4,5,1,1,6,7'); + assertValidPath('M100,200 a3,4,5,0,0,6,7'); + assertValidPath('M100,200 a3,4,5,0,1,6,7'); + assertValidPath('M100,200 a3,4,5,1,0,6,7'); + assertValidPath('M100,200 a3,4,5,1,1,6,7'); + assertValidPath('M100,200 a3,4,5,006,7'); + assertValidPath('M100,200 a3,4,5,016,7'); + assertValidPath('M100,200 a3,4,5,106,7'); + assertValidPath('M100,200 a3,4,5,116,7'); + assertValidPath(''' +M19.0281,19.40466 20.7195,19.40466 20.7195,15.71439 24.11486,15.71439 24.11486,14.36762 20.7195,14.36762 +20.7195,11.68641 24.74134,11.68641 24.74134,10.34618 19.0281,10.34618 z'''); + + assertValidPath( + 'M100,200 a0,4,5,0,0,10,0 a4,0,5,0,0,0,10 a0,0,5,0,0,-10,0 z'); + + assertValidPath('M1,2,3,4'); + assertValidPath('m100,200,3,4'); + + assertValidPath('M 100-200'); + assertValidPath('M 0.6.5'); + + assertValidPath(' M1,2'); + assertValidPath(' M1,2'); + assertValidPath('\tM1,2'); + assertValidPath('\nM1,2'); + assertValidPath('\rM1,2'); + assertValidPath('M1,2 '); + assertValidPath('M1,2\t'); + assertValidPath('M1,2\n'); + assertValidPath('M1,2\r'); + // assertValidPath(''); + // assertValidPath(' '); + assertValidPath('M.1 .2 L.3 .4 .5 .6'); + assertValidPath('M1,1h2,3'); + assertValidPath('M1,1H2,3'); + assertValidPath('M1,1v2,3'); + assertValidPath('M1,1V2,3'); + assertValidPath('M1,1c2,3 4,5 6,7 8,9 10,11 12,13'); + assertValidPath('M1,1C2,3 4,5 6,7 8,9 10,11 12,13'); + assertValidPath('M1,1s2,3 4,5 6,7 8,9'); + assertValidPath('M1,1S2,3 4,5 6,7 8,9'); + assertValidPath('M1,1q2,3 4,5 6,7 8,9'); + assertValidPath('M1,1Q2,3 4,5 6,7 8,9'); + assertValidPath('M1,1t2,3 4,5'); + assertValidPath('M1,1T2,3 4,5'); + assertValidPath('M1,1a2,3,4,0,0,5,6 7,8,9,0,0,10,11'); + assertValidPath('M1,1A2,3,4,0,0,5,6 7,8,9,0,0,10,11'); + assertValidPath( + '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'); + assertValidPath( + 'm18 11.8a.41.41 0 0 1 .24.08l.59.43h.05.72a.4.4 0 0 1 .39.28l.22.69a.08.08 0 ' + '0 0 0 0l.58.43a.41.41 0 0 1 .15.45l-.22.68a.09.09 0 0 0 0 .07l.22.68a.4.4 0 0 1 ' + '-.15.46l-.58.42a.1.1 0 0 0 0 0l-.22.68a.41.41 0 0 1 -.38.29h-.79l-.58.43a.41.41 0 ' + '0 1 -.24.08.46.46 0 0 1 -.24-.08l-.58-.43h-.06-.72a.41.41 0 0 1 -.39-.28l-.22-.68a.1.1 ' + '0 0 0 0 0l-.58-.43a.42.42 0 0 1 -.15-.46l.23-.67v-.02l-.29-.68a.43.43 0 0 1 ' + '.15-.46l.58-.42a.1.1 0 0 0 0-.05l.27-.69a.42.42 0 0 1 .39-.28h.78l.58-.43a.43.43 0 ' + '0 1 .25-.09m0-1a1.37 1.37 0 0 0 -.83.27l-.34.25h-.43a1.42 1.42 0 0 0 -1.34 ' + '1l-.13.4-.35.25a1.42 1.42 0 0 0 -.51 1.58l.13.4-.13.4a1.39 1.39 0 0 0 .52 ' + '1.59l.34.25.13.4a1.41 1.41 0 0 0 1.34 1h.43l.34.26a1.44 1.44 0 0 0 .83.27 1.38 1.38 0 0 0 ' + '.83-.28l.35-.24h.43a1.4 1.4 0 0 0 1.33-1l.13-.4.35-.26a1.39 1.39 0 0 0 ' + '.51-1.57l-.13-.4.13-.41a1.4 1.4 0 0 0 -.51-1.56l-.35-.25-.13-.41a1.4 1.4 0 0 0 ' + '-1.34-1h-.42l-.34-.26a1.43 1.43 0 0 0 -.84-.28z'); + }); + + test('Malformed Paths', () { + assertInvalidPath('M100,200 a3,4,5,2,1,6,7'); + assertInvalidPath('M100,200 a3,4,5,1,2,6,7'); + + assertInvalidPath('\vM1,2'); + assertInvalidPath('xM1,2'); + assertInvalidPath('M1,2\v'); + assertInvalidPath('M1,2x'); + assertInvalidPath('M1,2 L40,0#90'); + + assertInvalidPath('x'); + assertInvalidPath('L1,2'); + + assertInvalidPath('M'); + assertInvalidPath('M0'); + + assertInvalidPath('M1,1Z0'); + assertInvalidPath('M1,1z0'); + + assertInvalidPath('M1,1c2,3 4,5 6,7 8'); + assertInvalidPath('M1,1C2,3 4,5 6,7 8'); + assertInvalidPath('M1,1s2,3 4,5 6'); + assertInvalidPath('M1,1S2,3 4,5 6'); + assertInvalidPath('M1,1q2,3 4,5 6'); + assertInvalidPath('M1,1Q2,3 4,5 6'); + assertInvalidPath('M1,1t2,3 4'); + assertInvalidPath('M1,1T2,3 4'); + assertInvalidPath('M1,1a2,3,4,0,0,5,6 7'); + assertInvalidPath('M1,1A2,3,4,0,0,5,6 7'); + }); + + test('Missing commands/numbers/flags', () { + // Missing initial moveto. + assertInvalidPath(' 10 10'); + assertInvalidPath('L 10 10'); + // Invalid command letter. + assertInvalidPath('M 10 10 #'); + assertInvalidPath('M 10 10 E 100 100'); + // Invalid number. + assertInvalidPath('M 10 10 L100 '); + assertInvalidPath('M 10 10 L100 #'); + assertInvalidPath('M 10 10 L100#100'); + assertInvalidPath('M0,0 A#,10 0 0,0 20,20'); + assertInvalidPath('M0,0 A10,# 0 0,0 20,20'); + assertInvalidPath('M0,0 A10,10 # 0,0 20,20'); + assertInvalidPath('M0,0 A10,10 0 0,0 #,20'); + assertInvalidPath('M0,0 A10,10 0 0,0 20,#'); + // Invalid arc-flag. + assertInvalidPath('M0,0 A10,10 0 #,0 20,20'); + assertInvalidPath('M0,0 A10,10 0 0,# 20,20'); + assertInvalidPath('M0,0 A10,10 0 0,2 20,20'); + }); + + test('Check character constants', () { + expect(AsciiConstants.slashT, '\t'.codeUnitAt(0)); + expect(AsciiConstants.slashN, '\n'.codeUnitAt(0)); + expect(AsciiConstants.slashF, '\f'.codeUnitAt(0)); + expect(AsciiConstants.slashR, '\r'.codeUnitAt(0)); + expect(AsciiConstants.space, ' '.codeUnitAt(0)); + expect(AsciiConstants.period, '.'.codeUnitAt(0)); + expect(AsciiConstants.plus, '+'.codeUnitAt(0)); + expect(AsciiConstants.comma, ','.codeUnitAt(0)); + expect(AsciiConstants.minus, '-'.codeUnitAt(0)); + expect(AsciiConstants.number0, '0'.codeUnitAt(0)); + expect(AsciiConstants.number1, '1'.codeUnitAt(0)); + expect(AsciiConstants.number2, '2'.codeUnitAt(0)); + expect(AsciiConstants.number3, '3'.codeUnitAt(0)); + expect(AsciiConstants.number4, '4'.codeUnitAt(0)); + expect(AsciiConstants.number5, '5'.codeUnitAt(0)); + expect(AsciiConstants.number6, '6'.codeUnitAt(0)); + expect(AsciiConstants.number7, '7'.codeUnitAt(0)); + expect(AsciiConstants.number8, '8'.codeUnitAt(0)); + expect(AsciiConstants.number9, '9'.codeUnitAt(0)); + expect(AsciiConstants.upperA, 'A'.codeUnitAt(0)); + expect(AsciiConstants.upperC, 'C'.codeUnitAt(0)); + expect(AsciiConstants.upperE, 'E'.codeUnitAt(0)); + expect(AsciiConstants.upperH, 'H'.codeUnitAt(0)); + expect(AsciiConstants.upperL, 'L'.codeUnitAt(0)); + expect(AsciiConstants.upperM, 'M'.codeUnitAt(0)); + expect(AsciiConstants.upperQ, 'Q'.codeUnitAt(0)); + expect(AsciiConstants.upperS, 'S'.codeUnitAt(0)); + expect(AsciiConstants.upperT, 'T'.codeUnitAt(0)); + expect(AsciiConstants.upperV, 'V'.codeUnitAt(0)); + expect(AsciiConstants.upperZ, 'Z'.codeUnitAt(0)); + expect(AsciiConstants.lowerA, 'a'.codeUnitAt(0)); + expect(AsciiConstants.lowerC, 'c'.codeUnitAt(0)); + expect(AsciiConstants.lowerE, 'e'.codeUnitAt(0)); + expect(AsciiConstants.lowerH, 'h'.codeUnitAt(0)); + expect(AsciiConstants.lowerL, 'l'.codeUnitAt(0)); + expect(AsciiConstants.lowerM, 'm'.codeUnitAt(0)); + expect(AsciiConstants.lowerQ, 'q'.codeUnitAt(0)); + expect(AsciiConstants.lowerS, 's'.codeUnitAt(0)); + expect(AsciiConstants.lowerT, 't'.codeUnitAt(0)); + expect(AsciiConstants.lowerV, 'v'.codeUnitAt(0)); + expect(AsciiConstants.lowerX, 'x'.codeUnitAt(0)); + expect(AsciiConstants.lowerZ, 'z'.codeUnitAt(0)); + expect(AsciiConstants.tilde, '~'.codeUnitAt(0)); + }); +}