Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/vector_graphics_compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/vector_graphics_compiler/lib/src/svg/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
12 changes: 7 additions & 5 deletions packages/vector_graphics_compiler/lib/src/svg/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
49 changes: 32 additions & 17 deletions packages/vector_graphics_compiler/lib/src/svg/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}

Expand Down
17 changes: 17 additions & 0 deletions packages/vector_graphics_compiler/lib/src/svg/parsers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/vector_graphics_compiler/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions packages/vector_graphics_compiler/test/parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,26 @@ ${[for (var i = 2; i <= 30; i++) ' <pattern id="lvl$i" width="10" height="10"
]);
});

test('em units in text positions are relative to the font size', () {
final VectorInstructions instructions = parseWithoutOptimizers('''
<svg width="1000" height="500" xmlns="http://www.w3.org/2000/svg">
<text x="500" y="250" dy=".5em" font-size="400">21</text>
<g font-size="20">
<text x="1em" y="2em" dx="1em" dy="-1em">inherited</text>
</g>
<text x="10" y="10" dy="1em">default</text>
<text x="10" y="10" dy="1EM" font-size="10">upper case</text>
</svg>
''');

expect(instructions.textPositions, const <TextPosition>[
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('''
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
Expand Down
13 changes: 13 additions & 0 deletions packages/vector_graphics_compiler/test/parsers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading