diff --git a/tests/draw/svg/test_clip.py b/tests/draw/svg/test_clip.py new file mode 100644 index 000000000..034667a05 --- /dev/null +++ b/tests/draw/svg/test_clip.py @@ -0,0 +1,105 @@ +""" + weasyprint.tests.test_draw.svg.test_clip + ---------------------------------------- + + Test clip-path attribute. + +""" + +import pytest + +from ...testing_utils import assert_no_logs +from .. import assert_pixels + + +@assert_no_logs +def test_clip_path(): + assert_pixels('clip_path', 9, 9, ''' + _________ + _________ + __RRRRR__ + __RBBBR__ + __RBBBR__ + __RBBBR__ + __RRRRR__ + _________ + _________ + ''', ''' + + + + + + + + + + ''') + + +@assert_no_logs +def test_clip_path_on_group(): + assert_pixels('clip_path_on_group', 9, 9, ''' + _________ + _________ + __BBBB___ + __BRRRR__ + __BRRRR__ + __BRRRR__ + ___RRRR__ + _________ + _________ + ''', ''' + + + + + + + + + + + + + ''') + + +@pytest.mark.xfail +@assert_no_logs +def test_clip_path_group_on_group(): + assert_pixels('clip_path_group_on_group', 9, 9, ''' + _________ + _________ + __BB_____ + __BR_____ + _________ + _____RR__ + _____RR__ + _________ + _________ + ''', ''' + + + + + + + + + + + + + + ''') diff --git a/weasyprint/document.py b/weasyprint/document.py index 9c7ae87c0..d99c6f7fd 100644 --- a/weasyprint/document.py +++ b/weasyprint/document.py @@ -562,6 +562,27 @@ def __matmul__(self, other): [sum(self[i][k] * other[k][j] for k in range(3)) for j in range(3)] for i in range(len(self))]) + @property + def invert(self): + d = self.determinant + return Matrix(matrix=[ + [ + (self[1][1] * self[2][2] - self[1][2] * self[2][1]) / d, + (self[0][1] * self[2][2] - self[0][2] * self[2][1]) / -d, + (self[0][1] * self[1][2] - self[0][2] * self[1][1]) / d, + ], + [ + (self[1][0] * self[2][2] - self[1][2] * self[2][0]) / -d, + (self[0][0] * self[2][2] - self[0][2] * self[2][0]) / d, + (self[0][0] * self[1][2] - self[0][2] * self[1][0]) / -d, + ], + [ + (self[1][0] * self[2][1] - self[1][1] * self[2][0]) / d, + (self[0][0] * self[2][1] - self[0][1] * self[2][0]) / -d, + (self[0][0] * self[1][1] - self[0][1] * self[1][0]) / d, + ], + ]) + @property def determinant(self): assert len(self) == len(self[0]) == 3 diff --git a/weasyprint/svg/__init__.py b/weasyprint/svg/__init__.py index 78ce6f8de..3f3fe0579 100644 --- a/weasyprint/svg/__init__.py +++ b/weasyprint/svg/__init__.py @@ -14,17 +14,18 @@ from .bounding_box import bounding_box, is_valid_bounding_box from .css import parse_declarations, parse_stylesheets -from .defs import apply_filters, draw_gradient_or_pattern, paint_mask, use +from .defs import ( + apply_filters, clip_path, draw_gradient_or_pattern, paint_mask, use) from .images import image, svg from .path import path from .shapes import circle, ellipse, line, polygon, polyline, rect from .text import text from .utils import color, normalize, parse_url, preserve_ratio, size, transform -# TODO: clipPath TAGS = { 'a': text, 'circle': circle, + 'clipPath': clip_path, 'ellipse': ellipse, 'image': image, 'line': line, @@ -73,13 +74,14 @@ )) DEF_TYPES = frozenset(( - 'marker', - 'gradient', - 'pattern', - 'path', - 'mask', + 'clipPath', 'filter', + 'gradient', 'image', + 'marker', + 'mask', + 'path', + 'pattern', )) @@ -341,7 +343,7 @@ def draw(self, stream, concrete_width, concrete_height, base_url, self.draw_node(self.tree, size('12pt')) - def draw_node(self, node, font_size): + def draw_node(self, node, font_size, fill_stroke=True): """Draw a node.""" if node.tag == 'defs': return @@ -349,7 +351,8 @@ def draw_node(self, node, font_size): # Update font size font_size = size(node.get('font-size', '1em'), font_size, font_size) - self.stream.push_state() + if fill_stroke: + self.stream.push_state() # Apply filters filter_ = self.filters.get(parse_url(node.get('filter')).fragment) @@ -358,7 +361,7 @@ def draw_node(self, node, font_size): # Create substream for opacity opacity = float(node.get('opacity', 1)) - if 0 <= opacity < 1: + if fill_stroke and 0 <= opacity < 1: original_stream = self.stream box = self.calculate_bounding_box(node, font_size) if is_valid_bounding_box(box): @@ -367,8 +370,31 @@ def draw_node(self, node, font_size): coords = (0, 0, self.concrete_width, self.concrete_height) self.stream = self.stream.add_group(coords) - # Apply transformations + # Apply transform attribute self.transform(node.get('transform'), font_size) + + # Clip + clip_path = parse_url(node.get('clip-path')).fragment + if clip_path and clip_path in self.paths: + old_ctm = self.stream.ctm + clip_path = self.paths[clip_path] + if clip_path.get('clipPathUnits') == 'objectBoundingBox': + x, y = self.point(node.get('x'), node.get('y'), font_size) + width, height = self.point( + node.get('width'), node.get('height'), font_size) + self.stream.transform(a=width, d=height, e=x, f=y) + clip_path._etree_node.tag = 'g' + self.draw_node(clip_path, font_size, fill_stroke=False) + # At least set the clipping area to an empty path, so that it’s + # totally clipped when the clipping path is empty. + self.stream.rectangle(0, 0, 0, 0) + self.stream.clip() + self.stream.end() + new_ctm = self.stream.ctm + if new_ctm.determinant: + self.stream.transform(*(old_ctm @ new_ctm.invert).values) + + # Apply x/y transformations x, y = self.point(node.get('x'), node.get('y'), font_size) self.stream.transform(e=x, f=y) @@ -383,7 +409,7 @@ def draw_node(self, node, font_size): # Draw node children if display and node.tag not in DEF_TYPES: for child in node: - self.draw_node(child, font_size) + self.draw_node(child, font_size, fill_stroke) # Apply mask mask = self.masks.get(parse_url(node.get('mask')).fragment) @@ -391,13 +417,14 @@ def draw_node(self, node, font_size): paint_mask(self, node, mask, opacity) # Fill and stroke - self.fill_stroke(node, font_size) + if fill_stroke: + self.fill_stroke(node, font_size) # Draw markers - self.draw_markers(node, font_size) + self.draw_markers(node, font_size, fill_stroke) # Apply opacity stream and restore original stream - if 0 <= opacity < 1: + if fill_stroke and 0 <= opacity < 1: group_id = self.stream.id self.stream = original_stream self.stream.set_alpha(opacity, stroke=True, fill=True) @@ -409,9 +436,10 @@ def draw_node(self, node, font_size): self.cursor_d_position = [0, 0] self.text_path_width = 0 - self.stream.pop_state() + if fill_stroke: + self.stream.pop_state() - def draw_markers(self, node, font_size): + def draw_markers(self, node, font_size, fill_stroke): """Draw markers defined in a node.""" if not node.vertices: return @@ -530,7 +558,7 @@ def draw_markers(self, node, font_size): self.stream.pop_state() self.stream.clip() - self.draw_node(child, font_size) + self.draw_node(child, font_size, fill_stroke) self.stream.pop_state() position = 'mid' if angles else 'start' @@ -689,9 +717,9 @@ def __init__(self, tree, svg): self.patterns = {} self.paths = {} - def draw_node(self, node, font_size): + def draw_node(self, node, font_size, fill_stroke=True): # Store the original tree in self.tree when calling draw(), so that we # can reach defs outside the pattern if node == self.tree: self.tree = self.svg.tree - super().draw_node(node, font_size) + super().draw_node(node, font_size, fill_stroke=True) diff --git a/weasyprint/svg/defs.py b/weasyprint/svg/defs.py index 2d67675c2..cc2514abf 100644 --- a/weasyprint/svg/defs.py +++ b/weasyprint/svg/defs.py @@ -585,3 +585,9 @@ def paint_mask(svg, node, mask, font_size): svg.stream = alpha_stream svg.draw_node(mask, font_size) svg.stream = svg_stream + + +def clip_path(svg, node, font_size): + """Store a clip path definition.""" + if 'id' in node.attrib: + svg.paths[node.attrib['id']] = node