Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ex units #618

Merged
merged 16 commits into from Dec 8, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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