Skip to content

Commit

Permalink
Add support for ex units (#618)
Browse files Browse the repository at this point in the history
* feat: add xHeight to SvgTheme

* test: add SvgTheme xHeight tests

* feat: use xHeight when updating PictureProvider in SvgPicture

* test: add xHeight to DefaultSvgTheme and PictureProvider tests

* feat: add support for parsing ex units

* test: fix existing tests after introducing xHeight

* test: add DefaultSvgTheme xHeight tests

* test: add parseStyle xHeight tests (stroke width, dash array, dash offset)

* test: add SvgParser xHeight tests (svg, use, text, radialGradient, linearGradient, image)

* test: add SvgPicture xHeight tests for shapes (circle, rect, ellipse, line)

* test: PictureProvider rebuilds a decoder when the theme changes

* test: fix relative font size tests

* test: add ex font size tests

* test: fix DefaultSvgTheme tests

* revert: SvgTheme equals operator should use runtimeType
  • Loading branch information
bselwe committed Dec 8, 2021
1 parent fa5f0c3 commit dc9e644
Show file tree
Hide file tree
Showing 17 changed files with 812 additions and 248 deletions.
105 changes: 84 additions & 21 deletions lib/src/svg/parser_state.dart

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion lib/src/svg/parsers.dart
Expand Up @@ -21,10 +21,12 @@ final Map<String, double> _kTextSizeMap = <String, double>{

/// Parses a `font-size` attribute.
///
/// Uses [fontSize] to calculate the font size including em units.
/// Uses [fontSize] and [xHeight] to calculate the font size
/// that includes em or ex units.
double? parseFontSize(
String? raw, {
required double fontSize,
required double xHeight,
double? parentValue,
}) {
if (raw == null || raw == '') {
Expand All @@ -35,6 +37,7 @@ double? parseFontSize(
raw,
tryParse: true,
fontSize: fontSize,
xHeight: xHeight,
);
if (ret != null) {
return ret;
Expand Down
14 changes: 11 additions & 3 deletions lib/src/svg/theme.dart
Expand Up @@ -12,7 +12,8 @@ class SvgTheme {
const SvgTheme({
this.currentColor,
this.fontSize = 14,
});
double? xHeight,
}) : xHeight = xHeight ?? fontSize / 2;

/// The default color applied to SVG elements that inherit the color property.
/// See: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword
Expand All @@ -22,16 +23,23 @@ class SvgTheme {
/// See: https://www.w3.org/TR/SVG11/coords.html#Units
final double fontSize;

/// The x-height (corpus size) of the font used when calculating ex units of SVG elements.
/// Defaults to [fontSize] / 2 if not provided.
/// See: https://www.w3.org/TR/SVG11/coords.html#Units, https://en.wikipedia.org/wiki/X-height
final double xHeight;

@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) {
return false;
}

return other is SvgTheme &&
currentColor == other.currentColor &&
fontSize == other.fontSize;
fontSize == other.fontSize &&
xHeight == other.xHeight;
}

@override
int get hashCode => hashValues(currentColor, fontSize);
int get hashCode => hashValues(currentColor, fontSize, xHeight);
}
32 changes: 22 additions & 10 deletions lib/src/svg/xml_parsers.dart
Expand Up @@ -13,6 +13,7 @@ import 'parsers.dart';
double _parseRawWidthHeight(
String? raw, {
required double fontSize,
required double xHeight,
}) {
if (raw == '100%' || raw == '') {
return double.infinity;
Expand All @@ -21,18 +22,21 @@ double _parseRawWidthHeight(
final RegExp notDigits = RegExp(r'[^\d\.]');
if (!raw!.endsWith('px') &&
!raw.endsWith('em') &&
!raw.endsWith('ex') &&
raw.contains(notDigits)) {
print(
'Warning: Flutter SVG only supports the following formats for `width` and `height` on the SVG root:\n'
' width="100%"\n'
' width="100em"\n'
' width="100ex"\n'
' width="100px"\n'
' width="100" (where the number will be treated as pixels).\n'
'The supplied value ($raw) will be discarded and treated as if it had not been specified.');
}
return true;
}());
return parseDoubleWithUnits(raw, fontSize: fontSize, tryParse: true) ??
return parseDoubleWithUnits(raw,
fontSize: fontSize, xHeight: xHeight, tryParse: true) ??
double.infinity;
}

Expand All @@ -46,6 +50,7 @@ double _parseRawWidthHeight(
DrawableViewport? parseViewBox(
Map<String, String> svg, {
required double fontSize,
required double xHeight,
bool nullOk = false,
}) {
final String? viewBox = getAttribute(svg, 'viewBox');
Expand All @@ -64,15 +69,11 @@ DrawableViewport? parseViewBox(
' $svg');
}

final double width = _parseRawWidthHeight(
rawWidth,
fontSize: fontSize,
);
final double width =
_parseRawWidthHeight(rawWidth, fontSize: fontSize, xHeight: xHeight);

final double height = _parseRawWidthHeight(
rawHeight,
fontSize: fontSize,
);
final double height =
_parseRawWidthHeight(rawHeight, fontSize: fontSize, xHeight: xHeight);

if (viewBox == '') {
return DrawableViewport(
Expand Down Expand Up @@ -128,6 +129,7 @@ TileMode parseTileMode(Map<String, String> attributes) {
CircularIntervalList<double>? parseDashArray(
Map<String, String> attributes, {
required double fontSize,
required double xHeight,
}) {
final String? rawDashArray = getAttribute(attributes, 'stroke-dasharray');
if (rawDashArray == '') {
Expand All @@ -138,14 +140,16 @@ CircularIntervalList<double>? parseDashArray(

final List<String> parts = rawDashArray!.split(RegExp(r'[ ,]+'));
return CircularIntervalList<double>(parts
.map((String part) => parseDoubleWithUnits(part, fontSize: fontSize)!)
.map((String part) =>
parseDoubleWithUnits(part, fontSize: fontSize, xHeight: xHeight)!)
.toList());
}

/// Parses a @stroke-dashoffset into a [DashOffset].
DashOffset? parseDashOffset(
Map<String, String> attributes, {
required double fontSize,
required double xHeight,
}) {
final String? rawDashOffset = getAttribute(attributes, 'stroke-dashoffset');
if (rawDashOffset == '') {
Expand All @@ -158,6 +162,7 @@ DashOffset? parseDashOffset(
return DashOffset.absolute(parseDoubleWithUnits(
rawDashOffset,
fontSize: fontSize,
xHeight: xHeight,
)!);
}
}
Expand Down Expand Up @@ -200,6 +205,7 @@ DrawablePaint? parseStroke(
DrawablePaint? parentStroke,
Color? currentColor,
double fontSize,
double xHeight,
) {
final String rawStroke = getAttribute(attributes, 'stroke')!;
final String? rawStrokeOpacity = getAttribute(
Expand Down Expand Up @@ -264,6 +270,7 @@ DrawablePaint? parseStroke(
: parseDoubleWithUnits(
rawStrokeWidth,
fontSize: fontSize,
xHeight: xHeight,
),
);
return paint;
Expand Down Expand Up @@ -499,6 +506,7 @@ DrawableStyle parseStyle(
Rect? bounds,
DrawableStyle? parentStyle, {
required double fontSize,
required double xHeight,
Color? defaultFillColor,
Color? currentColor,
}) {
Expand All @@ -512,14 +520,17 @@ DrawableStyle parseStyle(
parentStyle?.stroke,
currentColor,
fontSize,
xHeight,
),
dashArray: parseDashArray(
attributes,
fontSize: fontSize,
xHeight: xHeight,
),
dashOffset: parseDashOffset(
attributes,
fontSize: fontSize,
xHeight: xHeight,
),
fill: parseFill(
key,
Expand All @@ -544,6 +555,7 @@ DrawableStyle parseStyle(
getAttribute(attributes, 'font-size'),
parentValue: parentStyle?.textStyle?.fontSize,
fontSize: fontSize,
xHeight: xHeight,
),
fontWeight: parseFontWeight(
getAttribute(attributes, 'font-weight', def: null),
Expand Down
19 changes: 16 additions & 3 deletions lib/src/utilities/numbers.dart
@@ -1,6 +1,6 @@
/// Parses a [rawDouble] `String` to a `double`.
///
/// The [rawDouble] might include a unit (`px` or `em`)
/// The [rawDouble] might include a unit (`px`, `em` or `ex`)
/// which is stripped off when parsed to a `double`.
///
/// Passing `null` will return `null`.
Expand All @@ -10,7 +10,11 @@ double? parseDouble(String? rawDouble, {bool tryParse = false}) {
return null;
}

rawDouble = rawDouble.replaceFirst('em', '').replaceFirst('px', '').trim();
rawDouble = rawDouble
.replaceFirst('em', '')
.replaceFirst('ex', '')
.replaceFirst('px', '')
.trim();

if (tryParse) {
return double.tryParse(rawDouble);
Expand All @@ -20,19 +24,24 @@ double? parseDouble(String? rawDouble, {bool tryParse = false}) {

/// Parses a [rawDouble] `String` to a `double`
/// taking into account absolute and relative units
/// (`px` or `em`).
/// (`px`, `em` or `ex`).
///
/// Passing an `em` value will calculate the result
/// relative to the provided [fontSize]:
/// 1 em = 1 * [fontSize].
///
/// Passing an `ex` value will calculate the result
/// relative to the provided [xHeight]:
/// 1 ex = 1 * [xHeight].
///
/// The [rawDouble] might include a unit which is
/// stripped off when parsed to a `double`.
///
/// Passing `null` will return `null`.
double? parseDoubleWithUnits(
String? rawDouble, {
required double fontSize,
required double xHeight,
bool tryParse = false,
}) {
double unit = 1.0;
Expand All @@ -41,6 +50,10 @@ double? parseDoubleWithUnits(
if (rawDouble?.contains('em') ?? false) {
unit = fontSize;
}
// 1 ex unit is equal to the current x-height.
else if (rawDouble?.contains('ex') ?? false) {
unit = xHeight;
}

final double? value = parseDouble(
rawDouble,
Expand Down
6 changes: 6 additions & 0 deletions lib/svg.dart
Expand Up @@ -787,9 +787,15 @@ class _SvgPictureState extends State<SvgPicture> {
// See: https://api.flutter.dev/flutter/painting/TextStyle/fontSize.html
14.0;

final double xHeight = widget.theme?.xHeight ??
defaultSvgTheme?.xHeight ??
// Fallback to the font size divided by 2.
fontSize / 2;

widget.pictureProvider.theme = SvgTheme(
currentColor: currentColor,
fontSize: fontSize,
xHeight: xHeight,
);
}

Expand Down
55 changes: 55 additions & 0 deletions test/default_theme_test.dart
Expand Up @@ -12,6 +12,7 @@ void main() {
const SvgTheme svgTheme = SvgTheme(
currentColor: Color(0xFF733821),
fontSize: 14.0,
xHeight: 6.0,
);

final SvgPicture svgPictureWidget = SvgPicture.string('''
Expand All @@ -34,6 +35,7 @@ void main() {
const SvgTheme anotherSvgTheme = SvgTheme(
currentColor: Color(0xFF05290E),
fontSize: 12.0,
xHeight: 7.0,
);

await tester.pumpWidget(DefaultSvgTheme(
Expand Down Expand Up @@ -157,5 +159,58 @@ void main() {
equals(14.0),
);
});

testWidgets(
'xHeight from the widget\'s theme takes precedence over '
'the theme from DefaultSvgTheme', (WidgetTester tester) async {
const SvgTheme svgTheme = SvgTheme(
fontSize: 14.0,
xHeight: 6.5,
);

final SvgPicture svgPictureWidget = SvgPicture.string(
'''
<svg viewBox="0 0 10 10">
<rect x="0" y="0" width="10ex" height="10ex" />
</svg>''',
theme: SvgTheme(
fontSize: 12.0,
xHeight: 7.0,
),
);

await tester.pumpWidget(DefaultSvgTheme(
theme: svgTheme,
child: svgPictureWidget,
));

final SvgPicture svgPicture = tester.firstWidget(find.byType(SvgPicture));
expect(svgPicture, isNotNull);
expect(
svgPicture.pictureProvider.theme.xHeight,
equals(7.0),
);
});

testWidgets(
'xHeight defaults to the font size divided by 2 (7.0) '
'if no widget\'s theme or DefaultSvgTheme is provided',
(WidgetTester tester) async {
final SvgPicture svgPictureWidget = SvgPicture.string(
'''
<svg viewBox="0 0 10 10">
<rect x="0" y="0" width="10ex" height="10ex" />
</svg>''',
);

await tester.pumpWidget(svgPictureWidget);

final SvgPicture svgPicture = tester.firstWidget(find.byType(SvgPicture));
expect(svgPicture, isNotNull);
expect(
svgPicture.pictureProvider.theme.xHeight,
equals(7.0),
);
});
});
}
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
1 change: 1 addition & 0 deletions test/picture_cache_test.dart
Expand Up @@ -80,6 +80,7 @@ void main() {
svgString,
)..theme = const SvgTheme(
fontSize: 14.0,
xHeight: 7.0,
);

await precachePicture(
Expand Down

0 comments on commit dc9e644

Please sign in to comment.