diff --git a/packages/vector_graphics_compiler/CHANGELOG.md b/packages/vector_graphics_compiler/CHANGELOG.md index b43c464030a..357ba8d9cb3 100644 --- a/packages/vector_graphics_compiler/CHANGELOG.md +++ b/packages/vector_graphics_compiler/CHANGELOG.md @@ -2,6 +2,10 @@ * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. +## 1.1.20 + +* Adds support for percentage units in SVG shape attributes (rect, circle, ellipse, line). + ## 1.1.19 * Updates allowed version range of `xml` to include up to 6.6.1. diff --git a/packages/vector_graphics_compiler/lib/src/svg/numbers.dart b/packages/vector_graphics_compiler/lib/src/svg/numbers.dart index 8798422f9f4..d8093a2a9b0 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/numbers.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/numbers.dart @@ -8,7 +8,7 @@ import 'theme.dart'; /// Parses a [rawDouble] `String` to a `double`. /// -/// The [rawDouble] might include a unit (`px`, `em` or `ex`) +/// The [rawDouble] might include a unit (`px`, `pt`, `em`, `ex`, `rem`, or `%`) /// which is stripped off when parsed to a `double`. /// /// Passing `null` will return `null`. @@ -24,6 +24,7 @@ double? parseDouble(String? rawDouble, {bool tryParse = false}) { .replaceFirst('ex', '') .replaceFirst('px', '') .replaceFirst('pt', '') + .replaceFirst('%', '') .trim(); if (tryParse) { @@ -56,6 +57,10 @@ const double kPointsToPixelFactor = kCssPixelsPerInch / kCssPointsPerInch; /// relative to the provided [xHeight]: /// 1 ex = 1 * `xHeight`. /// +/// Passing a `%` value will calculate the result +/// relative to the provided [percentageRef]: +/// 50% with percentageRef=100 = 50. +/// /// The `rawDouble` might include a unit which is /// stripped off when parsed to a `double`. /// @@ -64,9 +69,30 @@ double? parseDoubleWithUnits( String? rawDouble, { bool tryParse = false, required SvgTheme theme, + double? percentageRef, }) { var unit = 1.0; + // Handle percentage values first. + // Check inline to avoid circular import with parsers.dart. + final bool isPercent = rawDouble?.endsWith('%') ?? false; + if (isPercent) { + if (percentageRef == null || percentageRef.isInfinite) { + // If no reference dimension is available, treat as 0. + // This maintains backwards compatibility for cases where + // percentages can't be resolved. + if (tryParse) { + return null; + } + throw FormatException( + 'Percentage value "$rawDouble" requires a reference dimension ' + '(viewport width/height) but none was available.', + ); + } + final double? value = parseDouble(rawDouble, tryParse: tryParse); + return value != null ? (value / 100) * percentageRef : null; + } + // 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. diff --git a/packages/vector_graphics_compiler/lib/src/svg/parser.dart b/packages/vector_graphics_compiler/lib/src/svg/parser.dart index b8f67adc89c..0bc7c723823 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/parser.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/parser.dart @@ -6,6 +6,7 @@ import 'dart:collection'; import 'dart:convert'; +import 'dart:math' as math; import 'dart:typed_data'; import 'package:meta/meta.dart'; @@ -217,9 +218,11 @@ class _Elements { .translated( parserState.parseDoubleWithUnits( parserState.attribute('x', def: '0'), + percentageRef: parserState.viewportWidth, )!, parserState.parseDoubleWithUnits( parserState.attribute('y', def: '0'), + percentageRef: parserState.viewportHeight, )!, ); @@ -482,14 +485,23 @@ class _Elements { // ignore: avoid_classes_with_only_static_members class _Paths { static Path circle(SvgParser parserState) { + final double? vw = parserState.viewportWidth; + final double? vh = parserState.viewportHeight; final double cx = parserState.parseDoubleWithUnits( parserState.attribute('cx', def: '0'), + percentageRef: vw, )!; final double cy = parserState.parseDoubleWithUnits( parserState.attribute('cy', def: '0'), + percentageRef: vh, )!; + // For radius percentage, use the normalized diagonal per SVG spec. + final double? diagRef = (vw != null && vh != null) + ? math.sqrt(vw * vw + vh * vh) / math.sqrt(2) + : null; final double r = parserState.parseDoubleWithUnits( parserState.attribute('r', def: '0'), + percentageRef: diagRef, )!; final oval = Rect.fromCircle(cx, cy, r); return PathBuilder( @@ -503,17 +515,23 @@ class _Paths { } static Path rect(SvgParser parserState) { + final double? vw = parserState.viewportWidth; + final double? vh = parserState.viewportHeight; final double x = parserState.parseDoubleWithUnits( parserState.attribute('x', def: '0'), + percentageRef: vw, )!; final double y = parserState.parseDoubleWithUnits( parserState.attribute('y', def: '0'), + percentageRef: vh, )!; final double w = parserState.parseDoubleWithUnits( parserState.attribute('width', def: '0'), + percentageRef: vw, )!; final double h = parserState.parseDoubleWithUnits( parserState.attribute('height', def: '0'), + percentageRef: vh, )!; String? rxRaw = parserState.attribute('rx'); String? ryRaw = parserState.attribute('ry'); @@ -521,8 +539,14 @@ class _Paths { ryRaw ??= rxRaw; if (rxRaw != null && rxRaw != '') { - final double rx = parserState.parseDoubleWithUnits(rxRaw)!; - final double ry = parserState.parseDoubleWithUnits(ryRaw)!; + final double rx = parserState.parseDoubleWithUnits( + rxRaw, + percentageRef: vw, + )!; + final double ry = parserState.parseDoubleWithUnits( + ryRaw, + percentageRef: vh, + )!; return PathBuilder( parserState._currentAttributes.fillRule, ).addRRect(Rect.fromLTWH(x, y, w, h), rx, ry).toPath(); @@ -552,17 +576,23 @@ class _Paths { } static Path ellipse(SvgParser parserState) { + final double? vw = parserState.viewportWidth; + final double? vh = parserState.viewportHeight; final double cx = parserState.parseDoubleWithUnits( parserState.attribute('cx', def: '0'), + percentageRef: vw, )!; final double cy = parserState.parseDoubleWithUnits( parserState.attribute('cy', def: '0'), + percentageRef: vh, )!; final double rx = parserState.parseDoubleWithUnits( parserState.attribute('rx', def: '0'), + percentageRef: vw, )!; final double ry = parserState.parseDoubleWithUnits( parserState.attribute('ry', def: '0'), + percentageRef: vh, )!; final r = Rect.fromLTWH(cx - rx, cy - ry, rx * 2, ry * 2); @@ -572,17 +602,23 @@ class _Paths { } static Path line(SvgParser parserState) { + final double? vw = parserState.viewportWidth; + final double? vh = parserState.viewportHeight; final double x1 = parserState.parseDoubleWithUnits( parserState.attribute('x1', def: '0'), + percentageRef: vw, )!; final double x2 = parserState.parseDoubleWithUnits( parserState.attribute('x2', def: '0'), + percentageRef: vw, )!; final double y1 = parserState.parseDoubleWithUnits( parserState.attribute('y1', def: '0'), + percentageRef: vh, )!; final double y2 = parserState.parseDoubleWithUnits( parserState.attribute('y2', def: '0'), + percentageRef: vh, )!; return PathBuilder( @@ -968,18 +1004,33 @@ class SvgParser { /// relative to the provided [xHeight]: /// 1 ex = 1 * `xHeight`. /// + /// Passing a `%` value will calculate the result + /// relative to the provided [percentageRef]: + /// 50% with percentageRef=100 = 50. + /// /// 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}) { + double? parseDoubleWithUnits( + String? rawDouble, { + bool tryParse = false, + double? percentageRef, + }) { return numbers.parseDoubleWithUnits( rawDouble, tryParse: tryParse, theme: theme, + percentageRef: percentageRef, ); } + /// Returns the viewport width, or null if not yet parsed. + double? get viewportWidth => _root?.width; + + /// Returns the viewport height, or null if not yet parsed. + double? get viewportHeight => _root?.height; + static final Map _kTextSizeMap = { 'xx-small': 10, 'x-small': 12, diff --git a/packages/vector_graphics_compiler/pubspec.yaml b/packages/vector_graphics_compiler/pubspec.yaml index 3e6911fc9a3..43d627e60eb 100644 --- a/packages/vector_graphics_compiler/pubspec.yaml +++ b/packages/vector_graphics_compiler/pubspec.yaml @@ -2,7 +2,7 @@ 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 -version: 1.1.19 +version: 1.1.20 executables: vector_graphics_compiler: diff --git a/packages/vector_graphics_compiler/test/parser_test.dart b/packages/vector_graphics_compiler/test/parser_test.dart index 61644f6351b..31c346a364f 100644 --- a/packages/vector_graphics_compiler/test/parser_test.dart +++ b/packages/vector_graphics_compiler/test/parser_test.dart @@ -3155,6 +3155,73 @@ void main() { expect(parseWithoutOptimizers(svgStr), isA()); }); + + test('Parse rect with percentage width and height', () { + // This SVG uses percentage values for rect dimensions, like placeholder images + const svgStr = ''' + + + + + '''; + + final VectorInstructions instructions = parseWithoutOptimizers(svgStr); + + // Expect 2 rect paths + expect(instructions.paths.length, 2); + + // First rect should be full size (100% = 600x400) + expect(instructions.paths[0].commands, const [ + MoveToCommand(0.0, 0.0), + LineToCommand(600.0, 0.0), + LineToCommand(600.0, 400.0), + LineToCommand(0.0, 400.0), + CloseCommand(), + ]); + + // Second rect should be at 25%,25% (150,100) with 50% size (300x200) + expect(instructions.paths[1].commands, const [ + MoveToCommand(150.0, 100.0), + LineToCommand(450.0, 100.0), + LineToCommand(450.0, 300.0), + LineToCommand(150.0, 300.0), + CloseCommand(), + ]); + }); + + test('Parse circle with percentage cx, cy', () { + const svgStr = ''' + + + + '''; + + final VectorInstructions instructions = parseWithoutOptimizers(svgStr); + + // Expect 1 circle path centered at 50%,50% = 100,100 + expect(instructions.paths.length, 1); + // Circle paths are represented as ovals, check they're centered correctly + final commands = instructions.paths[0].commands.toList(); + expect(commands.isNotEmpty, true); + // The first command should move to the top of the circle (100, 100-40 = 60) + expect(commands[0], const MoveToCommand(100.0, 60.0)); + }); + + test('Parse line with percentage coordinates', () { + const svgStr = ''' + + + + '''; + + final VectorInstructions instructions = parseWithoutOptimizers(svgStr); + + expect(instructions.paths.length, 1); + expect(instructions.paths[0].commands, const [ + MoveToCommand(0.0, 0.0), + LineToCommand(100.0, 100.0), + ]); + }); } const List ghostScriptTigerPaints = [