diff --git a/packages/vector_graphics_compiler/CHANGELOG.md b/packages/vector_graphics_compiler/CHANGELOG.md index ecf4910e102..712063bdca9 100644 --- a/packages/vector_graphics_compiler/CHANGELOG.md +++ b/packages/vector_graphics_compiler/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.4 + +* Fixes `em` units in text `x`, `y`, `dx` and `dy` being treated as pixels. + ## 1.3.3 * Throws an `UnimplementedError` instead of a `FormatException` when `text` or diff --git a/packages/vector_graphics_compiler/lib/src/svg/constants.dart b/packages/vector_graphics_compiler/lib/src/svg/constants.dart index 0f1cf3621eb..ef171b8eec4 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/constants.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/constants.dart @@ -8,3 +8,8 @@ const int kMaxReferenceExpansions = 1000; /// The error message thrown when the nested reference expansions limit is exceeded. const String kMaxReferenceExpansionsErrorMessage = 'SVG contains too many nested reference expansions (possible Denial of Service exploit).'; + +/// The font size used for text without a `font-size`. +/// +/// This is the size of the CSS initial value `medium` in browsers. +const double kDefaultFontSize = 16; diff --git a/packages/vector_graphics_compiler/lib/src/svg/node.dart b/packages/vector_graphics_compiler/lib/src/svg/node.dart index 87ffdd75f32..49e93a13fff 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/node.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/node.dart @@ -11,6 +11,7 @@ import '../geometry/matrix.dart'; import '../geometry/path.dart'; import '../image/image_info.dart'; import '../paint.dart'; +import 'constants.dart'; import 'parser.dart' show SvgAttributes; import 'visitor.dart'; @@ -241,10 +242,11 @@ class TextPositionNode extends ParentNode { TextPosition computeTextPosition(Rect bounds, AffineMatrix transform, {TextPosition? parent}) { 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 double fontSize = attributes.fontSize ?? kDefaultFontSize; + double? x = attributes.x?.calculate(bounds.width, fontSize: fontSize); + double? y = attributes.y?.calculate(bounds.height, fontSize: fontSize); + double? dx = attributes.dx?.calculate(bounds.width, fontSize: fontSize); + double? dy = attributes.dy?.calculate(bounds.height, fontSize: fontSize); final bool hasXY = x != null && y != null; final bool hasDxDy = dx != null && dy != null; @@ -509,7 +511,7 @@ class TextNode extends AttributedNode { attributes.textAnchorMultiplier ?? 0, attributes.fontFamily, attributes.fontWeight ?? normalFontWeight, - attributes.fontSize ?? 16, // default in many browsers + attributes.fontSize ?? kDefaultFontSize, attributes.textDecoration ?? TextDecoration.none, attributes.textDecorationStyle ?? TextDecorationStyle.solid, attributes.textDecorationColor ?? Color.opaqueBlack, diff --git a/packages/vector_graphics_compiler/lib/src/svg/parser.dart b/packages/vector_graphics_compiler/lib/src/svg/parser.dart index 3c663fc9162..023eb82bd17 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/parser.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/parser.dart @@ -2085,11 +2085,23 @@ class SvgAttributes { } } -/// A value that represents either an absolute pixel coordinate or a percentage -/// of a bounding dimension. +/// The unit of a [DoubleOrPercentage]. +enum _DoubleOrPercentageUnit { + /// An absolute pixel coordinate. + pixels, + + /// A fraction of a bounding dimension. + percentage, + + /// A multiple of the font size. + em, +} + +/// A value that represents either an absolute pixel coordinate, a percentage +/// of a bounding dimension, or a multiple of the font size (`em`). @immutable class DoubleOrPercentage { - const DoubleOrPercentage._(this._value, this._isPercentage); + const DoubleOrPercentage._(this._value, this._unit); /// Constructs a [DoubleOrPercentage] from a raw SVG attribute string. static DoubleOrPercentage? fromString(String? raw) { @@ -2098,33 +2110,36 @@ class DoubleOrPercentage { } if (isPercentage(raw)) { - return DoubleOrPercentage._(parsePercentage(raw), true); + return DoubleOrPercentage._(parsePercentage(raw), _DoubleOrPercentageUnit.percentage); } - return DoubleOrPercentage._(parseDouble(raw)!, false); + if (isEm(raw)) { + return DoubleOrPercentage._(parseEm(raw), _DoubleOrPercentageUnit.em); + } + return DoubleOrPercentage._(parseDouble(raw)!, _DoubleOrPercentageUnit.pixels); } final double _value; - final bool _isPercentage; + final _DoubleOrPercentageUnit _unit; /// 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; + /// If this is a percentage, it will return a percentage of bound. If this is + /// an `em` value, it will return a multiple of [fontSize]. Otherwise, it + /// returns the value of this. + double calculate(double bound, {double fontSize = kDefaultFontSize}) { + return switch (_unit) { + _DoubleOrPercentageUnit.pixels => _value, + _DoubleOrPercentageUnit.percentage => _value * bound, + _DoubleOrPercentageUnit.em => _value * fontSize, + }; } @override - int get hashCode => Object.hash(_value, _isPercentage); + int get hashCode => Object.hash(_value, _unit); @override bool operator ==(Object other) { - return other is DoubleOrPercentage && - other._isPercentage == _isPercentage && - other._value == _value; + return other is DoubleOrPercentage && other._unit == _unit && other._value == _value; } } diff --git a/packages/vector_graphics_compiler/lib/src/svg/parsers.dart b/packages/vector_graphics_compiler/lib/src/svg/parsers.dart index cae31634a53..b351a4d2ccf 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/parsers.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/parsers.dart @@ -167,6 +167,23 @@ double parsePercentage(String val, {double multiplier = 1.0}) { /// Whether a string should be treated as a percentage (i.e. if it ends with a `'%'`). bool isPercentage(String? val) => val?.endsWith('%') ?? false; +/// Parses values in the form of '1.5em' as a multiple of the font size. +double parseEm(String val) { + final String trimmed = val.trim(); + return double.parse(trimmed.substring(0, trimmed.length - 2)); +} + +/// Whether a string should be treated as an `em` value (i.e. if it is a number +/// followed by `'em'`, in any case). +bool isEm(String? val) { + if (val == null) { + return false; + } + final String trimmed = val.trim(); + return trimmed.toLowerCase().endsWith('em') && + double.tryParse(trimmed.substring(0, trimmed.length - 2)) != null; +} + /// 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. diff --git a/packages/vector_graphics_compiler/pubspec.yaml b/packages/vector_graphics_compiler/pubspec.yaml index 0654ab800ee..25a4d916e55 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.3.3 +version: 1.3.4 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 62e2fbbea38..a6d9419a15f 100644 --- a/packages/vector_graphics_compiler/test/parser_test.dart +++ b/packages/vector_graphics_compiler/test/parser_test.dart @@ -470,6 +470,26 @@ ${[for (var i = 2; i <= 30; i++) ' + 21 + + inherited + + default + upper case + +'''); + + expect(instructions.textPositions, const [ + TextPosition(x: 500, y: 250, dy: 200, reset: true), + TextPosition(x: 20, y: 40, dx: 20, dy: -20, reset: true), + TextPosition(x: 10, y: 10, dy: 16, reset: true), + TextPosition(x: 10, y: 10, dy: 10, reset: true), + ]); + }); + test('Non-standard root font-weight="regular" uses initial weight', () { final VectorInstructions instructions = parseWithoutOptimizers(''' diff --git a/packages/vector_graphics_compiler/test/parsers_test.dart b/packages/vector_graphics_compiler/test/parsers_test.dart index 306bc0078e4..f787892207a 100644 --- a/packages/vector_graphics_compiler/test/parsers_test.dart +++ b/packages/vector_graphics_compiler/test/parsers_test.dart @@ -478,6 +478,19 @@ void main() { expect(parser.parseMaskType(' luminance '), MaskType.luminance); expect(parser.parseMaskType('unknown'), null); }); + + test('em parsing', () { + expect(isEm(null), false); + expect(isEm('1'), false); + expect(isEm('em'), false); + expect(isEm('1rem'), false); + expect(isEm('1xem'), false); + expect(isEm('1em'), true); + expect(isEm(' -.5EM '), true); + expect(parseEm('1em'), 1); + expect(parseEm(' -.5EM '), -0.5); + expect(parseEm('1e2em'), 100); + }); } class TestColorMapper extends ColorMapper {