From 899b202c0382243bf02e03be21b4e54f2eeb8afb Mon Sep 17 00:00:00 2001 From: Bill Foote Date: Wed, 1 May 2024 13:22:06 -0700 Subject: [PATCH] Expose tools to modify SVG path data: #95 --- lib/dom.dart | 3 + lib/src/common_noui.dart | 6 +- lib/src/compact.dart | 2 +- lib/src/compact_noui.dart | 8 +- lib/src/dag.dart | 4 +- lib/src/path.dart | 4 +- lib/src/path_noui.dart | 203 +++++++++++++++++++++++++++++++------- lib/src/svg_graph.dart | 19 ++-- pubspec.yaml | 2 +- 9 files changed, 197 insertions(+), 54 deletions(-) diff --git a/lib/dom.dart b/lib/dom.dart index 5a47f86..a0becd3 100644 --- a/lib/dom.dart +++ b/lib/dom.dart @@ -100,8 +100,11 @@ export 'src/svg_graph.dart' SvgFontWeight, SvgFontSize; +export 'src/path_noui.dart' show PathParser, PathBuilder, StringPathBuilder; + export 'src/common_noui.dart' show + ParseError, SIFillType, SIStrokeJoin, SIStrokeCap, diff --git a/lib/src/common_noui.dart b/lib/src/common_noui.dart index 4ab8eb0..3f2b5de 100644 --- a/lib/src/common_noui.dart +++ b/lib/src/common_noui.dart @@ -136,7 +136,7 @@ abstract class SIBuilder extends SIVisitor { /// will return null, and the scalable image will re-use the previously /// built, equivalent path. /// - PathBuilder? startPath(SIPaint paint, Object key); + EnhancedPathBuilder? startPath(SIPaint paint, Object key); } class SIImageData { @@ -496,10 +496,10 @@ class SIColorVisitor { /// Mixin for SIBuilder that builds paths from strings /// mixin SIStringPathMaker { - void makePath(String pathData, PathBuilder pb, + void makePath(String pathData, EnhancedPathBuilder pb, {required void Function(String) warn}) { try { - PathParser(pb, pathData).parse(); + RealPathParser(pb, pathData).parse(); } catch (e) { warn(e.toString()); // As per the SVG spec, paths shall be parsed up to the first error, diff --git a/lib/src/compact.dart b/lib/src/compact.dart index b5cf4da..be1e5e6 100644 --- a/lib/src/compact.dart +++ b/lib/src/compact.dart @@ -1225,7 +1225,7 @@ class _ExportedIDContext { } mixin _SICompactPathBuilder { - void makePath(CompactChildData pathData, PathBuilder pb, + void makePath(CompactChildData pathData, EnhancedPathBuilder pb, {required void Function(String) warn}) { CompactPathParser(pathData, pb).parse(); } diff --git a/lib/src/compact_noui.dart b/lib/src/compact_noui.dart index 9a79e40..a738f19 100644 --- a/lib/src/compact_noui.dart +++ b/lib/src/compact_noui.dart @@ -971,7 +971,7 @@ class CompactChildData { } // This is the dual of _CompactPathBuilder -class CompactPathParser extends AbstractPathParser { +class CompactPathParser extends AbstractPathParser { final ByteBufferDataInputStream children; final FloatBufferInputStream args; bool _nextNybble = false; @@ -1527,7 +1527,7 @@ abstract class SIGenericCompactBuilder } @override - PathBuilder? startPath(SIPaint paint, Object? key) { + EnhancedPathBuilder? startPath(SIPaint paint, Object? key) { final int? pathNumber = _pathShare[key]; final int? paintNumber = _paintShare[paint]; children.writeByte(PATH_CODE | @@ -1550,7 +1550,7 @@ abstract class SIGenericCompactBuilder } } - void makePath(PathDataT pathData, PathBuilder pb, + void makePath(PathDataT pathData, EnhancedPathBuilder pb, {required void Function(String) warn}); PathDataT immutableKey(PathDataT pathData); @@ -1657,7 +1657,7 @@ enum _PathCommand { arcToPointEllipseLargeCW } -class CompactPathBuilder extends PathBuilder { +class CompactPathBuilder extends EnhancedPathBuilder { final DataOutputSink _children; final FloatSink _args; diff --git a/lib/src/dag.dart b/lib/src/dag.dart index 4fe89ff..b6fa2cb 100644 --- a/lib/src/dag.dart +++ b/lib/src/dag.dart @@ -712,7 +712,7 @@ abstract class SIGenericDagBuilder } @override - PathBuilder? startPath(SIPaint paint, Object key) { + EnhancedPathBuilder? startPath(SIPaint paint, Object key) { final p = paths[key]; if (p != null) { final sip = _daggerize(SIPath(p, paint)); @@ -726,7 +726,7 @@ abstract class SIGenericDagBuilder }); } - void makePath(PathDataT pathData, PathBuilder pb, + void makePath(PathDataT pathData, EnhancedPathBuilder pb, {required void Function(String) warn}); @override diff --git a/lib/src/path.dart b/lib/src/path.dart index e499521..1c17d30 100644 --- a/lib/src/path.dart +++ b/lib/src/path.dart @@ -36,9 +36,9 @@ import 'common_noui.dart'; import 'dart:math' show pi; /// -/// Buidler of a Flutter UI path. See [PathBuilder] for usage. +/// Buidler of a Flutter UI path. See [EnhancedPathBuilder] for usage. /// -class UIPathBuilder implements PathBuilder { +class UIPathBuilder implements EnhancedPathBuilder { final void Function(UIPathBuilder)? _onEnd; UIPathBuilder({void Function(UIPathBuilder)? onEnd}) : _onEnd = onEnd; diff --git a/lib/src/path_noui.dart b/lib/src/path_noui.dart index af23c6a..a3f5dc5 100644 --- a/lib/src/path_noui.dart +++ b/lib/src/path_noui.dart @@ -32,10 +32,15 @@ library jovial_svg.path_noui; import 'dart:math'; +import 'package:meta/meta.dart'; + import 'common_noui.dart'; /// -/// A builder of a path. +/// A builder of a path whose source is a SVG `path` element +/// +/// +/// {@category SVG DOM} /// abstract class PathBuilder { /// @@ -73,14 +78,113 @@ abstract class PathBuilder { required bool largeArc, required bool clockwise}); - void addOval(RectT rect); - /// /// Finish the path. /// void end(); } +/// +/// A builder of a path, including building an oval (which isn't part of +/// SVG's `path` syntax, but is used for the circle and ellipse node types). +/// +abstract class EnhancedPathBuilder extends PathBuilder { + /// + /// Add an oval (ellipse) that fills the given rectangle. + /// + void addOval(RectT rect); +} + +/// +/// A [PathBuilder] that produces a path string. This can be used with +/// a [RealPathParser] if you have a path string that you want to parse, modify, +/// and then reconstitute as a path string. +/// +/// Usage: +/// One possible use is to intercept path builder calls, and transform them to +/// something else. For example, if for some reason you wanted to remove all +/// lineTo commands (`'L'`/`'l'`, or additional coordinates in an `'M'`/`'m'`) +/// from the path of an `SvgPath` node, you could do this: +/// ``` +/// class NoLinesPathBuilder extends StringPathBuilder { +/// @override +/// void lineTo(PointT p) {} +/// } +/// +/// void removeLinesFrom(final SvgPath node) { +/// final pb = NoLinesPathBuilder(); +/// PathParser(pb, node.pathData).parse(); +/// node.pathData = pb.result; +/// } +/// ``` +/// +/// {@category SVG DOM} +/// +class StringPathBuilder extends PathBuilder { + final _result = StringBuffer(); + + String get result => _result.toString(); + + @override + void moveTo(PointT p) { + _result.write('M ${p.x} ${p.y} '); + } + + @override + void close() { + _result.write('Z '); + } + + @override + void lineTo(PointT p) { + _result.write('L ${p.x} ${p.y} '); + } + + @override + void cubicTo(PointT c1, PointT c2, PointT p, bool shorthand) { + if (shorthand) { + _result.write('S '); + } else { + _result.write('C ${c1.x} ${c1.y} '); + } + _result.write('${c2.x} ${c2.y} ${p.x} ${p.y} '); + } + + @override + void quadraticBezierTo(PointT control, PointT p, bool shorthand) { + if (shorthand) { + _result.write('T '); + } else { + _result.write('Q ${control.x} ${control.y} '); + } + _result.write('${p.x} ${p.y} '); + } + + @override + void arcToPoint(PointT arcEnd, + {required RadiusT radius, + required double rotation, + required bool largeArc, + required bool clockwise}) { + _result.write('A '); + _result.write(radius.x); + _result.write(' '); + _result.write(radius.y); + _result.write(' '); + _result.write(rotation * 180.0 / pi); + _result.write(' '); + _result.write(largeArc ? '1 ' : '0 '); + _result.write(clockwise ? '1 ' : '0 '); + _result.write(arcEnd.x); + _result.write(' '); + _result.write(arcEnd.y); + _result.write(' '); + } + + @override + void end() {} +} + /// /// Some helpful support for parsing a path. Paths include some logic /// for calculating control points for the "shorthand" versions of @@ -88,8 +192,8 @@ abstract class PathBuilder { /// control point of the last such curve, and tracking the current point. /// This helper implements that logic. /// -abstract class AbstractPathParser { - final PathBuilder builder; +abstract class AbstractPathParser { + final BT builder; PointT _initialPoint; // https://www.w3.org/TR/SVG/ s. 9.3.1 PointT? _lastCubicControl; @@ -104,11 +208,12 @@ abstract class AbstractPathParser { _currentPoint = const PointT(0, 0); /// - /// Run a command that adds to the path. The commands first argument is + /// Run a command that adds to the path. The command's first argument is /// [firstValue], but it may take other arguments. Splitting out the first /// argument value like this makes it easier to deal with repeated /// commands in a String path. /// + @protected void runPathCommand(double firstValue, PointT Function(double) command) { _nextQuadControl = null; _nextCubicControl = null; @@ -119,8 +224,9 @@ abstract class AbstractPathParser { /// /// moveTo is special, because it sets the initial point. It isn't - /// run through runPathCommand, like the other commands are. + /// run through [runPathCommand], like the other commands are. /// + @protected void buildMoveTo(PointT c) { _currentPoint = c; _initialPoint = c; @@ -129,6 +235,7 @@ abstract class AbstractPathParser { _lastCubicControl = null; } + @protected PointT buildCubicBezier(PointT? control1, PointT control2, PointT dest) { final shorthand = control1 == null; final c1 = control1 ?? _shorthandControl(_lastCubicControl); @@ -138,6 +245,7 @@ abstract class AbstractPathParser { return dest; } + @protected PointT buildQuadraticBezier(PointT? control, PointT dest) { final shorthand = control == null; final c = control ?? _shorthandControl(_lastQuadControl); @@ -151,6 +259,7 @@ abstract class AbstractPathParser { /// buildClose() is not run through runCommand. It can't be, since it /// takes no argument. /// + @protected void buildClose() { builder.close(); _currentPoint = _initialPoint; @@ -162,6 +271,7 @@ abstract class AbstractPathParser { /// buildEnd() (which is for the end of the path) is not run through /// runCommand. It can't be, since it takes no argument. /// + @protected void buildEnd() { builder.end(); } @@ -181,96 +291,119 @@ abstract class AbstractPathParser { } /// -/// Parse an SVG Path. See the specification at +/// Parse an SVG Path. The path syntax is specified at at /// https://www.w3.org/TR/2018/CR-SVG2-20181004/paths.html /// /// Usage: /// ``` /// String src = "M 125,75 a100,50 0 0,1 100,50" -/// final builder = UIPathBuilder(); +/// final PathBuilder builder = ...; /// PathParser(builder, src).parse(); -/// Path p = builder.path; -/// ... render p on a Canvas... +/// ... do something with whatever builder produces ... /// ``` -class PathParser extends AbstractPathParser { +/// +/// {@category SVG DOM} +/// +final class PathParser { + final RealPathParser _hidden; + + /// + /// Create a parser to parse [source]. It will call the appropriate methods + /// on [builder] to build a result. + /// + PathParser(PathBuilder builder, String source) + : _hidden = RealPathParser(builder, source); + + /// + /// Parse the string. On error, this throws a [ParseError], but it leaves + /// the path up to where the error occurred in builder.path. The error + /// behavior specified in s. 9.5.4 of + /// https://www.w3.org/TR/2018/CR-SVG2-20181004/paths.html can be had + /// by catching the exception, reporting it to the user if appropriate, + /// and rendering the partial path. + /// + void parse() => _hidden.parse(); +} + +class RealPathParser extends AbstractPathParser { final BnfLexer _lexer; // Is the current command relative? bool _relative = false; - PathParser(super.builder, String source) : _lexer = BnfLexer(source); + RealPathParser(super.builder, String source) : _lexer = BnfLexer(source); - static final Map _action = { - 'M': (PathParser p) { + static final Map _action = { + 'M': (RealPathParser p) { p._relative = false; p._moveTo(); }, - 'm': (PathParser p) { + 'm': (RealPathParser p) { p._relative = true; p._moveTo(); }, - 'Z': (PathParser p) => p.buildClose(), - 'z': (PathParser p) => p.buildClose(), - 'L': (PathParser p) { + 'Z': (RealPathParser p) => p.buildClose(), + 'z': (RealPathParser p) => p.buildClose(), + 'L': (RealPathParser p) { p._relative = false; p._repeat(p._lineTo); }, - 'l': (PathParser p) { + 'l': (RealPathParser p) { p._relative = true; p._repeat(p._lineTo); }, - 'H': (PathParser p) { + 'H': (RealPathParser p) { p._relative = false; p._repeat(p._horizontalLineTo); }, - 'h': (PathParser p) { + 'h': (RealPathParser p) { p._relative = true; p._repeat(p._horizontalLineTo); }, - 'V': (PathParser p) { + 'V': (RealPathParser p) { p._relative = false; p._repeat(p._verticalLineTo); }, - 'v': (PathParser p) { + 'v': (RealPathParser p) { p._relative = true; p._repeat(p._verticalLineTo); }, - 'C': (PathParser p) { + 'C': (RealPathParser p) { p._relative = false; p._repeat(p._cubicBezier); }, - 'c': (PathParser p) { + 'c': (RealPathParser p) { p._relative = true; p._repeat(p._cubicBezier); }, - 'S': (PathParser p) { + 'S': (RealPathParser p) { p._relative = false; p._repeat(p._shorthandCubicBezier); }, - 's': (PathParser p) { + 's': (RealPathParser p) { p._relative = true; p._repeat(p._shorthandCubicBezier); }, - 'Q': (PathParser p) { + 'Q': (RealPathParser p) { p._relative = false; p._repeat(p._quadraticBezier); }, - 'q': (PathParser p) { + 'q': (RealPathParser p) { p._relative = true; p._repeat(p._quadraticBezier); }, - 'T': (PathParser p) { + 'T': (RealPathParser p) { p._relative = false; p._repeat(p._shorthandQuadraticBezier); }, - 't': (PathParser p) { + 't': (RealPathParser p) { p._relative = true; p._repeat(p._shorthandQuadraticBezier); }, - 'A': (PathParser p) { + 'A': (RealPathParser p) { p._relative = false; p._repeat(p._arcToPoint); }, - 'a': (PathParser p) { + 'a': (RealPathParser p) { p._relative = true; p._repeat(p._arcToPoint); }, @@ -294,7 +427,7 @@ class PathParser extends AbstractPathParser { _lexer.skipWhitespace(); while (!_lexer.eof) { String cmd = _lexer.nextPathCommand(); - final void Function(PathParser)? a = _action[cmd]; + final void Function(RealPathParser)? a = _action[cmd]; if (a == null) { _lexer.error('Unrecognized command "$cmd"'); } else { diff --git a/lib/src/svg_graph.dart b/lib/src/svg_graph.dart index 63e61fa..61cdbeb 100644 --- a/lib/src/svg_graph.dart +++ b/lib/src/svg_graph.dart @@ -270,7 +270,7 @@ class _CollectCanonBuilder implements SIBuilder { collectPaint(paint); @override - PathBuilder? startPath(SIPaint paint, Object key) { + EnhancedPathBuilder? startPath(SIPaint paint, Object key) { collectPaint(paint); return null; } @@ -1554,6 +1554,13 @@ class _PathKey { /// {@category SVG DOM} /// class SvgPath extends SvgPathMaker { + /// + /// The path commands. The syntax is specified at at + /// https://www.w3.org/TR/2018/CR-SVG2-20181004/paths.html + /// + /// See [StringPathBuilder] for one tool that can be used to modify the + /// path data. + /// String pathData; SvgPath(this.pathData); @@ -1584,7 +1591,7 @@ class SvgPath extends SvgPathMaker { return null; } final builder = _SvgPathBoundsBuilder(); - PathParser(builder, pathData).parse(); + RealPathParser(builder, pathData).parse(); return builder.bounds; } @@ -1616,7 +1623,7 @@ class SvgPath extends SvgPathMaker { // We use pathData as our path key } -class _SvgPathBoundsBuilder implements PathBuilder { +class _SvgPathBoundsBuilder implements EnhancedPathBuilder { RectT? bounds; void _addToBounds(RectT rect) { @@ -1715,7 +1722,7 @@ class SvgRect extends SvgPathMaker { if (exportedID != null) { builder.exportedID(null, canon.strings[exportedID!]); } - PathBuilder? pb = builder.startPath(curr, _PathKey(this)); + EnhancedPathBuilder? pb = builder.startPath(curr, _PathKey(this)); if (pb != null) { if (rx <= 0 || ry <= 0) { pb.moveTo(PointT(x, y)); @@ -1831,7 +1838,7 @@ class SvgEllipse extends SvgPathMaker { if (exportedID != null) { builder.exportedID(null, canon.strings[exportedID!]); } - PathBuilder? pb = builder.startPath(curr, _PathKey(this)); + EnhancedPathBuilder? pb = builder.startPath(curr, _PathKey(this)); if (pb != null) { pb.addOval(RectT(cx - rx, cy - ry, 2 * rx, 2 * ry)); pb.end(); @@ -1922,7 +1929,7 @@ class SvgPoly extends SvgPathMaker { if (exportedID != null) { builder.exportedID(null, canon.strings[exportedID!]); } - PathBuilder? pb = builder.startPath(curr, _PathKey(this)); + EnhancedPathBuilder? pb = builder.startPath(curr, _PathKey(this)); if (pb != null) { pb.moveTo(points[0]); for (int i = 1; i < points.length; i++) { diff --git a/pubspec.yaml b/pubspec.yaml index d846961..e469ee2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: jovial_svg description: SVG - Robust rendering of Scalable Vector Graphic images, supporting a well-defined profile of SVG, a fast-loading binary storage format, and animation. -version: 1.1.21-rc.3 +version: 1.1.21-rc.4 homepage: https://bill.jovial.com/ environment: