diff --git a/cairosvg/helpers.py b/cairosvg/helpers.py index 65a5331a..edfabad0 100644 --- a/cairosvg/helpers.py +++ b/cairosvg/helpers.py @@ -388,3 +388,24 @@ def size(surface, string, reference='xy'): # Unknown size return 0 + + +def parse_opacity(string, default=1): + """Return the opacity of ``string`` as a float clamped to ``[0, 1]``. + + ``string`` may be a number or a percentage (e.g. ``'50%'``). Any value + that cannot be parsed falls back to ``default`` instead of raising, so an + invalid ``*-opacity`` attribute never aborts the whole rendering. + + """ + if string is None: + return default + string = string.strip() + try: + if string.endswith('%'): + value = float(string[:-1]) / 100 + else: + value = float(string) + except ValueError: + return default + return min(1, max(0, value)) diff --git a/cairosvg/surface.py b/cairosvg/surface.py index ee7920f7..e60ce6aa 100644 --- a/cairosvg/surface.py +++ b/cairosvg/surface.py @@ -14,7 +14,7 @@ filter_, gradient_or_pattern, linear_gradient, marker, mask, paint_mask, parse_all_defs, pattern, prepare_filter, radial_gradient, use) from .helpers import ( - UNITS, PointError, clip_rect, node_format, normalize, paint, + UNITS, PointError, clip_rect, node_format, normalize, paint, parse_opacity, preserve_ratio, size, transform) from .image import image, invert_image from .parser import Tree @@ -322,7 +322,7 @@ def draw(self, node): # Find and prepare opacity, masks and filters mask = parse_url(node.get('mask')).fragment filter_ = parse_url(node.get('filter')).fragment - opacity = float(node.get('opacity', 1)) + opacity = parse_opacity(node.get('opacity')) if filter_: prepare_filter(self, node, filter_) @@ -412,8 +412,8 @@ def draw(self, node): pass # Get stroke and fill opacity - stroke_opacity = float(node.get('stroke-opacity', 1)) - fill_opacity = float(node.get('fill-opacity', 1)) + stroke_opacity = parse_opacity(node.get('stroke-opacity')) + fill_opacity = parse_opacity(node.get('fill-opacity')) if opacity < 1 and not node.children: stroke_opacity *= opacity fill_opacity *= opacity diff --git a/cairosvg/test_api.py b/cairosvg/test_api.py index 9f1da3f4..f63d5486 100644 --- a/cairosvg/test_api.py +++ b/cairosvg/test_api.py @@ -37,6 +37,31 @@ def test_formats(format_name): assert content.startswith(MAGIC_NUMBERS[format_name]) +def _rect_svg(attribute, value): + return ( + '' + f'' + '').encode('ascii') + + +@pytest.mark.parametrize('attribute', ( + 'opacity', 'fill-opacity', 'stroke-opacity')) +def test_opacity_percentage(attribute): + """Percentage opacities render like their equivalent fractions (#422).""" + percentage = svg2png(bytestring=_rect_svg(attribute, '50%')) + fraction = svg2png(bytestring=_rect_svg(attribute, '0.5')) + assert percentage == fraction + + +@pytest.mark.parametrize('attribute', ( + 'opacity', 'fill-opacity', 'stroke-opacity')) +def test_opacity_invalid(attribute): + """Invalid opacities fall back to opaque instead of crashing (#422).""" + invalid = svg2png(bytestring=_rect_svg(attribute, 'null')) + opaque = svg2png(bytestring=_rect_svg(attribute, '1')) + assert invalid == opaque + + def read_file(filename): """Shortcut to return the whole content of a file as a byte string.""" with open(filename, 'rb') as file_object: