# 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()