Skip to content

Commit

Permalink
Handle simple cases of clip-path
Browse files Browse the repository at this point in the history
Fix #1374.
  • Loading branch information
liZe committed Jul 18, 2021
1 parent aee609d commit c7b97fa
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 20 deletions.
105 changes: 105 additions & 0 deletions tests/draw/svg/test_clip.py
Original file line number Diff line number Diff line change
@@ -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__
_________
_________
''', '''
<style>
@page { size: 9px }
svg { display: block }
</style>
<svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clip">
<rect x="2" y="2" width="5" height="5" />
</clipPath>
</defs>
<rect x="2" y="2" width="5" height="5" stroke-width="2"
stroke="red" fill="blue" clip-path="url(#clip)" />
</svg>
''')


@assert_no_logs
def test_clip_path_on_group():
assert_pixels('clip_path_on_group', 9, 9, '''
_________
_________
__BBBB___
__BRRRR__
__BRRRR__
__BRRRR__
___RRRR__
_________
_________
''', '''
<style>
@page { size: 9px }
svg { display: block }
</style>
<svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clip">
<rect x="2" y="2" width="5" height="5" />
</clipPath>
</defs>
<g clip-path="url(#clip)">
<rect x="1" y="1" width="5" height="5" fill="blue" />
<rect x="3" y="3" width="5" height="5" fill="red" />
</g>
</svg>
''')


@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__
_________
_________
''', '''
<style>
@page { size: 9px }
svg { display: block }
</style>
<svg width="9px" height="9px" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clip">
<rect x="2" y="2" width="2" height="2" />
<rect x="3" y="3" width="2" height="2" />
</clipPath>
</defs>
<g clip-path="url(#clip)">
<rect x="1" y="1" width="5" height="5" fill="blue" />
<rect x="3" y="3" width="5" height="5" fill="red" />
</g>
</svg>
''')
21 changes: 21 additions & 0 deletions weasyprint/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 48 additions & 20 deletions weasyprint/svg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -73,13 +74,14 @@
))

DEF_TYPES = frozenset((
'marker',
'gradient',
'pattern',
'path',
'mask',
'clipPath',
'filter',
'gradient',
'image',
'marker',
'mask',
'path',
'pattern',
))


Expand Down Expand Up @@ -341,15 +343,16 @@ 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

# 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)
Expand All @@ -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):
Expand All @@ -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)

Expand All @@ -383,21 +409,22 @@ 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)
if mask:
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)
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)
6 changes: 6 additions & 0 deletions weasyprint/svg/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c7b97fa

Please sign in to comment.