From cf9e7cda2b03f71c9f395a36447de00c1905209e Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 3 Feb 2024 10:57:04 +0100 Subject: [PATCH] Support percentages for opacity Fix #1986. --- tests/draw/svg/test_opacity.py | 7 +++---- tests/draw/test_opacity.py | 27 ++++++++++++++++++++++--- weasyprint/css/validation/properties.py | 2 ++ weasyprint/svg/__init__.py | 9 +++++---- weasyprint/svg/defs.py | 4 ++-- weasyprint/svg/utils.py | 11 ++++++++++ 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/tests/draw/svg/test_opacity.py b/tests/draw/svg/test_opacity.py index b3f780de..c137722b 100644 --- a/tests/draw/svg/test_opacity.py +++ b/tests/draw/svg/test_opacity.py @@ -31,7 +31,7 @@ def test_fill_opacity(assert_same_renderings): assert_same_renderings( opacity_source % ''' + fill="blue" opacity="50%" /> ''', @@ -59,7 +59,7 @@ def test_stroke_opacity(assert_same_renderings): ''', opacity_source % ''' + stroke="lime" fill="blue" stroke-opacity="50%" /> ''', ) @@ -82,7 +82,6 @@ def test_stroke_fill_opacity(assert_same_renderings): ) -@pytest.mark.xfail @assert_no_logs def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings): assert_same_renderings( @@ -141,6 +140,6 @@ def test_translate_opacity(assert_same_renderings): ''', opacity_source % ''' + fill="blue" opacity="50%" /> ''', ) diff --git a/tests/draw/test_opacity.py b/tests/draw/test_opacity.py index ce88f4dc..3d56665a 100644 --- a/tests/draw/test_opacity.py +++ b/tests/draw/test_opacity.py @@ -11,23 +11,26 @@ opacity_source = ''' @assert_no_logs -def test_opacity_1(assert_same_renderings): +def test_opacity_zero(assert_same_renderings): assert_same_renderings( opacity_source % '
', opacity_source % '
', + opacity_source % '
', ) @assert_no_logs -def test_opacity_2(assert_same_renderings): +def test_opacity_normal_range(assert_same_renderings): assert_same_renderings( opacity_source % '
', opacity_source % '
', + opacity_source % '
', + opacity_source % '
', ) @assert_no_logs -def test_opacity_3(assert_same_renderings): +def test_opacity_nested(assert_same_renderings): assert_same_renderings( opacity_source % '
', opacity_source % '
', @@ -37,3 +40,21 @@ def test_opacity_3(assert_same_renderings): ''', # 0.9 * 0.666666 == 0.6 ) + + +@assert_no_logs +def test_opacity_percent_clamp_down(assert_same_renderings): + assert_same_renderings( + opacity_source % '
', + opacity_source % '
', + opacity_source % '
', + ) + + +@assert_no_logs +def test_opacity_percent_clamp_up(assert_same_renderings): + assert_same_renderings( + opacity_source % '
', + opacity_source % '
', + opacity_source % '
', + ) diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index ef2e1ea0..0e6d120c 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -1029,6 +1029,8 @@ def opacity(token): """Validation for the ``opacity`` property.""" if token.type == 'number': return min(1, max(0, token.value)) + if token.type == 'percentage': + return min(1, max(0, token.value / 100)) @property() diff --git a/weasyprint/svg/__init__.py b/weasyprint/svg/__init__.py index 4f0cc611..fc911cca 100644 --- a/weasyprint/svg/__init__.py +++ b/weasyprint/svg/__init__.py @@ -19,7 +19,8 @@ from .path import path from .shapes import circle, ellipse, line, polygon, polyline, rect from .text import text from .utils import ( - PointError, color, normalize, parse_url, preserve_ratio, size, transform) + PointError, alpha_value, color, normalize, parse_url, preserve_ratio, size, + transform) TAGS = { 'a': text, @@ -412,7 +413,7 @@ class SVG: self.transform(node.get('transform'), font_size) # Create substream for opacity - opacity = float(node.get('opacity', 1)) + opacity = alpha_value(node.get('opacity', 1)) if fill_stroke and 0 <= opacity < 1: original_streams.append(self.stream) box = self.calculate_bounding_box(node, font_size) @@ -662,7 +663,7 @@ class SVG: # Get fill data fill_source, fill_color = self.get_paint(node.get('fill', 'black')) - fill_opacity = float(node.get('fill-opacity', 1)) + fill_opacity = alpha_value(node.get('fill-opacity', 1)) fill_drawn = draw_gradient_or_pattern( self, node, fill_source, font_size, fill_opacity, stroke=False) if fill_color and not fill_drawn: @@ -673,7 +674,7 @@ class SVG: # Get stroke data stroke_source, stroke_color = self.get_paint(node.get('stroke')) - stroke_opacity = float(node.get('stroke-opacity', 1)) + stroke_opacity = alpha_value(node.get('stroke-opacity', 1)) stroke_drawn = draw_gradient_or_pattern( self, node, stroke_source, font_size, stroke_opacity, stroke=True) if stroke_color and not stroke_drawn: diff --git a/weasyprint/svg/defs.py b/weasyprint/svg/defs.py index a311ab8e..efe31b25 100644 --- a/weasyprint/svg/defs.py +++ b/weasyprint/svg/defs.py @@ -5,7 +5,7 @@ from math import ceil, hypot from ..matrix import Matrix from .bounding_box import is_valid_bounding_box -from .utils import color, parse_url, size, transform +from .utils import alpha_value, color, parse_url, size, transform def use(svg, node, font_size): @@ -83,7 +83,7 @@ def draw_gradient(svg, node, gradient, font_size, opacity, stroke): positions.append(max( positions[-1] if positions else 0, size(child.get('offset'), font_size, 1))) - stop_opacity = float(child.get('stop-opacity', 1)) * opacity + stop_opacity = alpha_value(child.get('stop-opacity', 1)) * opacity stop_color = color(child.get('stop-color', 'black')) if stop_opacity < 1: stop_color = tuple( diff --git a/weasyprint/svg/utils.py b/weasyprint/svg/utils.py index aa86982e..a6d15ff3 100644 --- a/weasyprint/svg/utils.py +++ b/weasyprint/svg/utils.py @@ -57,6 +57,17 @@ def size(string, font_size=None, percentage_reference=None): return 0 +def alpha_value(value): + """Return opacity between 0 and 1 from str, number or percentage.""" + ratio = 1 + if isinstance(value, str): + value = value.strip() + if value.endswith('%'): + ratio = 100 + value = value[:-1].strip() + return min(1, max(0, float(value) / ratio)) + + def point(svg, string, font_size): """Pop first two size values from a string.""" match = re.match('(.*?) (.*?)(?: |$)', string)