1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-09-11 20:47:56 +03:00

Support percentages for opacity

Fix #1986.
This commit is contained in:
Guillaume Ayoub 2024-02-03 10:57:04 +01:00
parent e64b69cf1f
commit cf9e7cda2b
6 changed files with 47 additions and 13 deletions

View File

@ -31,7 +31,7 @@ def test_fill_opacity(assert_same_renderings):
assert_same_renderings( assert_same_renderings(
opacity_source % ''' opacity_source % '''
<rect x="2" y="2" width="5" height="5" <rect x="2" y="2" width="5" height="5"
fill="blue" opacity="0.5" /> fill="blue" opacity="50%" />
<rect x="2" y="2" width="5" height="5" stroke-width="2" <rect x="2" y="2" width="5" height="5" stroke-width="2"
stroke="lime" fill="transparent" /> stroke="lime" fill="transparent" />
''', ''',
@ -59,7 +59,7 @@ def test_stroke_opacity(assert_same_renderings):
''', ''',
opacity_source % ''' opacity_source % '''
<rect x="2" y="2" width="5" height="5" stroke-width="2" <rect x="2" y="2" width="5" height="5" stroke-width="2"
stroke="lime" fill="blue" stroke-opacity="0.5" /> 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 @assert_no_logs
def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings): def test_pattern_gradient_stroke_fill_opacity(assert_same_renderings):
assert_same_renderings( assert_same_renderings(
@ -141,6 +140,6 @@ def test_translate_opacity(assert_same_renderings):
''', ''',
opacity_source % ''' opacity_source % '''
<rect x="2" y="2" width="5" height="5" <rect x="2" y="2" width="5" height="5"
fill="blue" opacity="0.5" /> fill="blue" opacity="50%" />
''', ''',
) )

View File

@ -11,23 +11,26 @@ opacity_source = '''
@assert_no_logs @assert_no_logs
def test_opacity_1(assert_same_renderings): def test_opacity_zero(assert_same_renderings):
assert_same_renderings( assert_same_renderings(
opacity_source % '<div></div>', opacity_source % '<div></div>',
opacity_source % '<div></div><div style="opacity: 0"></div>', opacity_source % '<div></div><div style="opacity: 0"></div>',
opacity_source % '<div></div><div style="opacity: 0%"></div>',
) )
@assert_no_logs @assert_no_logs
def test_opacity_2(assert_same_renderings): def test_opacity_normal_range(assert_same_renderings):
assert_same_renderings( assert_same_renderings(
opacity_source % '<div style="background: rgb(102, 102, 102)"></div>', opacity_source % '<div style="background: rgb(102, 102, 102)"></div>',
opacity_source % '<div style="opacity: 0.6"></div>', opacity_source % '<div style="opacity: 0.6"></div>',
opacity_source % '<div style="opacity: 60%"></div>',
opacity_source % '<div style="opacity: 60.0%"></div>',
) )
@assert_no_logs @assert_no_logs
def test_opacity_3(assert_same_renderings): def test_opacity_nested(assert_same_renderings):
assert_same_renderings( assert_same_renderings(
opacity_source % '<div style="background: rgb(102, 102, 102)"></div>', opacity_source % '<div style="background: rgb(102, 102, 102)"></div>',
opacity_source % '<div style="opacity: 0.6"></div>', opacity_source % '<div style="opacity: 0.6"></div>',
@ -37,3 +40,21 @@ def test_opacity_3(assert_same_renderings):
</div> </div>
''', # 0.9 * 0.666666 == 0.6 ''', # 0.9 * 0.666666 == 0.6
) )
@assert_no_logs
def test_opacity_percent_clamp_down(assert_same_renderings):
assert_same_renderings(
opacity_source % '<div></div>',
opacity_source % '<div style="opacity: 1.2"></div>',
opacity_source % '<div style="opacity: 120%"></div>',
)
@assert_no_logs
def test_opacity_percent_clamp_up(assert_same_renderings):
assert_same_renderings(
opacity_source % '<div></div>',
opacity_source % '<div></div><div style="opacity: -0.2"></div>',
opacity_source % '<div></div><div style="opacity: -20%"></div>',
)

View File

@ -1029,6 +1029,8 @@ def opacity(token):
"""Validation for the ``opacity`` property.""" """Validation for the ``opacity`` property."""
if token.type == 'number': if token.type == 'number':
return min(1, max(0, token.value)) return min(1, max(0, token.value))
if token.type == 'percentage':
return min(1, max(0, token.value / 100))
@property() @property()

View File

@ -19,7 +19,8 @@ from .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect from .shapes import circle, ellipse, line, polygon, polyline, rect
from .text import text from .text import text
from .utils import ( from .utils import (
PointError, color, normalize, parse_url, preserve_ratio, size, transform) PointError, alpha_value, color, normalize, parse_url, preserve_ratio, size,
transform)
TAGS = { TAGS = {
'a': text, 'a': text,
@ -412,7 +413,7 @@ class SVG:
self.transform(node.get('transform'), font_size) self.transform(node.get('transform'), font_size)
# Create substream for opacity # Create substream for opacity
opacity = float(node.get('opacity', 1)) opacity = alpha_value(node.get('opacity', 1))
if fill_stroke and 0 <= opacity < 1: if fill_stroke and 0 <= opacity < 1:
original_streams.append(self.stream) original_streams.append(self.stream)
box = self.calculate_bounding_box(node, font_size) box = self.calculate_bounding_box(node, font_size)
@ -662,7 +663,7 @@ class SVG:
# Get fill data # Get fill data
fill_source, fill_color = self.get_paint(node.get('fill', 'black')) 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( fill_drawn = draw_gradient_or_pattern(
self, node, fill_source, font_size, fill_opacity, stroke=False) self, node, fill_source, font_size, fill_opacity, stroke=False)
if fill_color and not fill_drawn: if fill_color and not fill_drawn:
@ -673,7 +674,7 @@ class SVG:
# Get stroke data # Get stroke data
stroke_source, stroke_color = self.get_paint(node.get('stroke')) 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( stroke_drawn = draw_gradient_or_pattern(
self, node, stroke_source, font_size, stroke_opacity, stroke=True) self, node, stroke_source, font_size, stroke_opacity, stroke=True)
if stroke_color and not stroke_drawn: if stroke_color and not stroke_drawn:

View File

@ -5,7 +5,7 @@ from math import ceil, hypot
from ..matrix import Matrix from ..matrix import Matrix
from .bounding_box import is_valid_bounding_box 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): def use(svg, node, font_size):
@ -83,7 +83,7 @@ def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
positions.append(max( positions.append(max(
positions[-1] if positions else 0, positions[-1] if positions else 0,
size(child.get('offset'), font_size, 1))) 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')) stop_color = color(child.get('stop-color', 'black'))
if stop_opacity < 1: if stop_opacity < 1:
stop_color = tuple( stop_color = tuple(

View File

@ -57,6 +57,17 @@ def size(string, font_size=None, percentage_reference=None):
return 0 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): def point(svg, string, font_size):
"""Pop first two size values from a string.""" """Pop first two size values from a string."""
match = re.match('(.*?) (.*?)(?: |$)', string) match = re.match('(.*?) (.*?)(?: |$)', string)