# This file is part of CairoSVG
# Copyright © 2010-2018 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see .
"""
Externally defined elements managers.
This module handles clips, gradients, masks, patterns and external nodes.
"""
from .bounding_box import calculate_bounding_box, is_non_empty_bounding_box
from .colors import color
from .features import match_features
from .helpers import paint, size, transform
from .parser import Tree
from .shapes import rect
from .surface import cairo
from .url import parse_url
BLEND_OPERATORS = {
'darken': cairo.OPERATOR_DARKEN,
'lighten': cairo.OPERATOR_LIGHTEN,
'multiply': cairo.OPERATOR_MULTIPLY,
'normal': cairo.OPERATOR_OVER,
'screen': cairo.OPERATOR_SCREEN,
}
EXTEND_OPERATORS = {
'none': cairo.EXTEND_NONE,
'pad': cairo.EXTEND_PAD,
'reflect': cairo.EXTEND_REFLECT,
'repeat': cairo.EXTEND_REPEAT,
}
def update_def_href(surface, def_name, def_dict):
"""Update the attributes of the def according to its href attribute."""
def_node = def_dict[def_name]
href = parse_url(def_node.get_href()).fragment
if href in def_dict:
update_def_href(surface, href, def_dict)
href_node = def_dict[href]
def_dict[def_name] = Tree(
url='#{}'.format(def_name), url_fetcher=def_node.url_fetcher,
parent=href_node, parent_children=(not def_node.children),
tree_cache=surface.tree_cache, unsafe=def_node.unsafe)
# Inherit attributes generally not inherited
for key, value in href_node.items():
if key not in def_dict[def_name]:
def_dict[def_name][key] = value
def parse_all_defs(surface, node):
"""Recursively visit all child nodes and process definition elements."""
# Handle node
parse_def(surface, node)
# Visit all children recursively
if node.children:
for child in node.children:
parse_all_defs(surface, child)
def parse_def(surface, node):
"""Parse the SVG definitions."""
for def_type in (
'marker', 'gradient', 'pattern', 'path', 'mask', 'filter'):
if def_type in node.tag.lower() and 'id' in node:
getattr(surface, '{}s'.format(def_type))[node['id']] = node
def gradient_or_pattern(surface, node, name):
"""Gradient or pattern color."""
if name in surface.gradients:
update_def_href(surface, name, surface.gradients)
return draw_gradient(surface, node, name)
elif name in surface.patterns:
update_def_href(surface, name, surface.patterns)
return draw_pattern(surface, node, name)
def marker(surface, node):
"""Store a marker definition."""
parse_def(surface, node)
def mask(surface, node):
"""Store a mask definition."""
parse_def(surface, node)
def filter_(surface, node):
"""Store a filter definition."""
parse_def(surface, node)
def linear_gradient(surface, node):
"""Store a linear gradient definition."""
parse_def(surface, node)
def radial_gradient(surface, node):
"""Store a radial gradient definition."""
parse_def(surface, node)
def pattern(surface, node):
"""Store a pattern definition."""
parse_def(surface, node)
def clip_path(surface, node):
"""Store a clip path definition."""
if 'id' in node:
surface.paths[node['id']] = node
def paint_mask(surface, node, name, opacity):
"""Paint the mask of the current surface."""
mask_node = surface.masks[name]
mask_node.tag = 'g'
mask_node['opacity'] = opacity
if mask_node.get('maskUnits') == 'userSpaceOnUse':
width_ref, height_ref = 'x', 'y'
else:
x = size(surface, node.get('x'), 'x')
y = size(surface, node.get('y'), 'y')
width = size(surface, node.get('width'), 'x')
height = size(surface, node.get('height'), 'y')
width_ref = width or surface.width
height_ref = height or surface.height
mask_node['x'] = size(surface, mask_node.get('x', '-10%'), width_ref)
mask_node['y'] = size(surface, mask_node.get('y', '-10%'), height_ref)
mask_node['height'] = size(
surface, mask_node.get('height', '120%'), height_ref)
mask_node['width'] = size(
surface, mask_node.get('width', '120%'), width_ref)
if mask_node.get('maskUnits') == 'userSpaceOnUse':
x = mask_node['x']
y = mask_node['y']
mask_node['viewBox'] = '{} {} {} {}'.format(
mask_node['x'], mask_node['y'],
mask_node['width'], mask_node['height'])
from .surface import SVGSurface # circular import
mask_surface = SVGSurface(mask_node, None, surface.dpi, surface)
surface.context.save()
surface.context.translate(x, y)
surface.context.scale(
mask_node['width'] / mask_surface.width,
mask_node['height'] / mask_surface.height)
surface.context.mask_surface(mask_surface.cairo)
surface.context.restore()
def draw_gradient(surface, node, name):
"""Gradients colors."""
gradient_node = surface.gradients[name]
if gradient_node.get('gradientUnits') == 'userSpaceOnUse':
width_ref, height_ref = 'x', 'y'
diagonal_ref = 'xy'
else:
bounding_box = calculate_bounding_box(surface, node)
if not is_non_empty_bounding_box(bounding_box):
return False
x = size(surface, bounding_box[0], 'x')
y = size(surface, bounding_box[1], 'y')
width = size(surface, bounding_box[2], 'x')
height = size(surface, bounding_box[3], 'y')
width_ref = height_ref = diagonal_ref = 1
if gradient_node.tag == 'linearGradient':
x1 = size(surface, gradient_node.get('x1', '0%'), width_ref)
x2 = size(surface, gradient_node.get('x2', '100%'), width_ref)
y1 = size(surface, gradient_node.get('y1', '0%'), height_ref)
y2 = size(surface, gradient_node.get('y2', '0%'), height_ref)
gradient_pattern = cairo.LinearGradient(x1, y1, x2, y2)
elif gradient_node.tag == 'radialGradient':
r = size(surface, gradient_node.get('r', '50%'), diagonal_ref)
cx = size(surface, gradient_node.get('cx', '50%'), width_ref)
cy = size(surface, gradient_node.get('cy', '50%'), height_ref)
fx = size(surface, gradient_node.get('fx', str(cx)), width_ref)
fy = size(surface, gradient_node.get('fy', str(cy)), height_ref)
gradient_pattern = cairo.RadialGradient(fx, fy, 0, cx, cy, r)
# Apply matrix to set coordinate system for gradient
if gradient_node.get('gradientUnits') != 'userSpaceOnUse':
gradient_pattern.set_matrix(cairo.Matrix(
1 / width, 0, 0, 1 / height, - x / width, - y / height))
# Apply transform of gradient
transform(
surface, gradient_node.get('gradientTransform'), gradient_pattern)
# Apply gradient ( by )
offset = 0
for child in gradient_node.children:
offset = max(offset, size(surface, child.get('offset'), 1))
stop_color = color(
child.get('stop-color', 'black'),
float(child.get('stop-opacity', 1)))
gradient_pattern.add_color_stop_rgba(offset, *stop_color)
# Set spread method for gradient outside target bounds
gradient_pattern.set_extend(EXTEND_OPERATORS.get(
gradient_node.get('spreadMethod', 'pad'), EXTEND_OPERATORS['pad']))
surface.context.set_source(gradient_pattern)
return True
def draw_pattern(surface, node, name):
"""Draw a pattern image."""
pattern_node = surface.patterns[name]
pattern_node.tag = 'g'
transform(surface, pattern_node.get('patternTransform'))
if pattern_node.get('viewBox'):
if not (size(surface, pattern_node.get('width', 1), 1) and
size(surface, pattern_node.get('height', 1), 1)):
return False
else:
if not (size(surface, pattern_node.get('width', 0), 1) and
size(surface, pattern_node.get('height', 0), 1)):
return False
if pattern_node.get('patternUnits') == 'userSpaceOnUse':
x = size(surface, pattern_node.get('x'), 'x')
y = size(surface, pattern_node.get('y'), 'y')
pattern_width = size(surface, pattern_node.get('width', 0), 1)
pattern_height = size(surface, pattern_node.get('height', 0), 1)
else:
width = size(surface, node.get('width'), 'x')
height = size(surface, node.get('height'), 'y')
x = size(surface, pattern_node.get('x'), 1) * width
y = size(surface, pattern_node.get('y'), 1) * height
pattern_width = (
size(surface, pattern_node.pop('width', '1'), 1) * width)
pattern_height = (
size(surface, pattern_node.pop('height', '1'), 1) * height)
if 'viewBox' not in pattern_node:
pattern_node['width'] = pattern_width
pattern_node['height'] = pattern_height
if pattern_node.get('patternContentUnits') == 'objectBoundingBox':
pattern_node['transform'] = 'scale({}, {})'.format(
width, height)
# Fail if pattern has an invalid size
if pattern_width == 0.0 or pattern_height == 0.0:
return False
from .surface import SVGSurface # circular import
pattern_surface = SVGSurface(pattern_node, None, surface.dpi, surface)
pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo)
pattern_pattern.set_extend(cairo.EXTEND_REPEAT)
pattern_pattern.set_matrix(cairo.Matrix(
pattern_surface.width / pattern_width, 0, 0,
pattern_surface.height / pattern_height, -x, -y))
surface.context.set_source(pattern_pattern)
return True
def prepare_filter(surface, node, name):
"""Apply a filter transforming the context."""
if 'id' in node and node['id'] in surface.masks:
return
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Offset
if child.tag == 'feOffset':
if filter_node.get('primitiveUnits') == 'objectBoundingBox':
width = size(surface, node.get('width'), 'x')
height = size(surface, node.get('height'), 'y')
dx = size(surface, child.get('dx', 0), 1) * width
dy = size(surface, child.get('dy', 0), 1) * height
else:
dx = size(surface, child.get('dx', 0), 1)
dy = size(surface, child.get('dy', 0), 1)
surface.context.translate(dx, dy)
def apply_filter_before_painting(surface, node, name):
"""Apply a filter transforming the painting operations."""
if 'id' in node and node['id'] in surface.masks:
return
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Blend
if child.tag == 'feBlend':
surface.context.set_operator(BLEND_OPERATORS.get(
child.get('mode', 'normal'), BLEND_OPERATORS['normal']))
def apply_filter_after_painting(surface, node, name):
"""Apply a filter using the painted surface to transform the image."""
if 'id' in node and node['id'] in surface.masks:
return
if name in surface.filters:
filter_node = surface.filters[name]
for child in filter_node.children:
# Flood
if child.tag == 'feFlood':
surface.context.save()
surface.context.new_path()
if filter_node.get('primitiveUnits') == 'objectBoundingBox':
x = size(surface, node.get('x'), 'x')
y = size(surface, node.get('y'), 'y')
width = size(surface, node.get('width'), 'x')
height = size(surface, node.get('height'), 'y')
else:
x, y, width, height = 0, 0, 1, 1
x += size(surface, child.get('x', 0), 1)
y += size(surface, child.get('y', 0), 1)
width *= size(surface, child.get('width', 0), 1)
height *= size(surface, child.get('height', 0), 1)
rect(surface, dict(x=x, y=y, width=width, height=height))
surface.context.set_source_rgba(*color(
paint(child.get('flood-color'))[1],
float(child.get('flood-opacity', 1))))
surface.context.fill()
surface.context.restore()
def use(surface, node):
"""Draw the content of another SVG node."""
surface.context.save()
surface.context.translate(
size(surface, node.get('x'), 'x'), size(surface, node.get('y'), 'y'))
if 'x' in node:
del node['x']
if 'y' in node:
del node['y']
if 'viewBox' in node:
del node['viewBox']
if 'mask' in node:
del node['mask']
href = parse_url(node.get_href()).geturl()
tree = Tree(
url=href, url_fetcher=node.url_fetcher, parent=node,
tree_cache=surface.tree_cache, unsafe=node.unsafe)
if not match_features(tree.xml_tree):
surface.context.restore()
return
if tree.tag in ('svg', 'symbol'):
# Explicitely specified
# http://www.w3.org/TR/SVG11/struct.html#UseElement
tree.tag = 'svg'
if 'width' in node and 'height' in node:
tree['width'], tree['height'] = node['width'], node['height']
surface.draw(tree)
node.get('fill', None)
node.get('stroke', None)
surface.context.restore()