# 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 . """ Surface helpers. """ import re from math import atan2, cos, radians, sin, tan from .surface import cairo from .url import parse_url UNITS = { 'mm': 1 / 25.4, 'cm': 1 / 2.54, 'in': 1, 'pt': 1 / 72., 'pc': 1 / 6., 'px': None, } PAINT_URL = re.compile(r'(url\(.+\)) *(.*)') PATH_LETTERS = 'achlmqstvzACHLMQSTVZ' RECT = re.compile(r'rect\( ?(.+?) ?\)') class PointError(Exception): """Exception raised when parsing a point fails.""" def distance(x1, y1, x2, y2): """Get the distance between two points.""" return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 def paint(value): """Extract from value an uri and a color. See http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint """ if not value: return None, None value = value.strip() match = PAINT_URL.search(value) if match: source = parse_url(match.group(1)).fragment color = match.group(2) or None else: source = None color = value or None return (source, color) def node_format(surface, node, reference=True): """Return ``(width, height, viewbox)`` of ``node``. If ``reference`` is ``True``, we can rely on surface size to resolve percentages. """ reference_size = 'xy' if reference else (0, 0) width = size(surface, node.get('width', '100%'), reference_size[0]) height = size(surface, node.get('height', '100%'), reference_size[1]) viewbox = node.get('viewBox') if viewbox: viewbox = re.sub('[ \n\r\t,]+', ' ', viewbox) viewbox = tuple(float(position) for position in viewbox.split()) width = width or viewbox[2] height = height or viewbox[3] return width, height, viewbox def normalize(string): """Normalize a string corresponding to an array of various values.""" string = string.replace('E', 'e') string = re.sub('(? 0 else 1 scale_y = height / viewbox_height if viewbox_height > 0 else 1 aspect_ratio = node.get('preserveAspectRatio', 'xMidYMid').split() align = aspect_ratio[0] if align == 'none': x_position = 'min' y_position = 'min' else: meet_or_slice = aspect_ratio[1] if len(aspect_ratio) > 1 else None if meet_or_slice == 'slice': scale_value = max(scale_x, scale_y) else: scale_value = min(scale_x, scale_y) scale_x = scale_y = scale_value x_position = align[1:4].lower() y_position = align[5:].lower() if node.tag == 'marker': translate_x = -size(surface, node.get('refX', '0'), 'x') translate_y = -size(surface, node.get('refY', '0'), 'y') else: translate_x = 0 if x_position == 'mid': translate_x = (width / scale_x - viewbox_width) / 2 elif x_position == 'max': translate_x = width / scale_x - viewbox_width translate_y = 0 if y_position == 'mid': translate_y += (height / scale_y - viewbox_height) / 2 elif y_position == 'max': translate_y += height / scale_y - viewbox_height return scale_x, scale_y, translate_x, translate_y def clip_marker_box(surface, node, scale_x, scale_y): """Get the clip ``(x, y, width, height)`` of the marker box.""" width = size(surface, node.get('markerWidth', '3'), 'x') height = size(surface, node.get('markerHeight', '3'), 'y') _, _, viewbox = node_format(surface, node) viewbox_width, viewbox_height = viewbox[2:] align = node.get('preserveAspectRatio', 'xMidYMid').split(' ')[0] x_position = 'min' if align == 'none' else align[1:4].lower() y_position = 'min' if align == 'none' else align[5:].lower() clip_x = viewbox[0] if x_position == 'mid': clip_x += (viewbox_width - width / scale_x) / 2. elif x_position == 'max': clip_x += viewbox_width - width / scale_x clip_y = viewbox[1] if y_position == 'mid': clip_y += (viewbox_height - height / scale_y) / 2. elif y_position == 'max': clip_y += viewbox_height - height / scale_y return clip_x, clip_y, width / scale_x, height / scale_y def quadratic_points(x1, y1, x2, y2, x3, y3): """Return the quadratic points to create quadratic curves.""" xq1 = x2 * 2 / 3 + x1 / 3 yq1 = y2 * 2 / 3 + y1 / 3 xq2 = x2 * 2 / 3 + x3 / 3 yq2 = y2 * 2 / 3 + y3 / 3 return xq1, yq1, xq2, yq2, x3, y3 def rotate(x, y, angle): """Rotate a point of an angle around the origin point.""" return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle) def transform(surface, string, gradient=None): """Transform ``surface`` or ``gradient`` if supplied using ``string``. See http://www.w3.org/TR/SVG/coords.html#TransformAttribute """ if not string: return transformations = re.findall(r'(\w+) ?\( ?(.*?) ?\)', normalize(string)) matrix = cairo.Matrix() for transformation_type, transformation in transformations: values = [size(surface, value) for value in transformation.split(' ')] if transformation_type == 'matrix': matrix = cairo.Matrix(*values).multiply(matrix) elif transformation_type == 'rotate': angle = radians(float(values.pop(0))) x, y = values or (0, 0) matrix.translate(x, y) matrix.rotate(angle) matrix.translate(-x, -y) elif transformation_type == 'skewX': tangent = tan(radians(float(values[0]))) matrix = cairo.Matrix(1, 0, tangent, 1, 0, 0).multiply(matrix) elif transformation_type == 'skewY': tangent = tan(radians(float(values[0]))) matrix = cairo.Matrix(1, tangent, 0, 1, 0, 0).multiply(matrix) elif transformation_type == 'translate': if len(values) == 1: values += (0,) matrix.translate(*values) elif transformation_type == 'scale': if len(values) == 1: values = 2 * values matrix.scale(*values) try: matrix.invert() except cairo.Error: # Matrix not invertible, clip the surface to an empty path active_path = surface.context.copy_path() surface.context.new_path() surface.context.clip() surface.context.append_path(active_path) else: if gradient: # When applied on gradient use already inverted matrix (mapping # from user space to gradient space) matrix_now = gradient.get_matrix() gradient.set_matrix(matrix_now.multiply(matrix)) else: matrix.invert() surface.context.transform(matrix) def clip_rect(string): """Parse the rect value of a clip.""" match = RECT.search(normalize(string or '')) return match.group(1).split(' ') if match else [] def rotations(node): """Retrieves the original rotations of a `text` or `tspan` node.""" if 'rotate' in node: original_rotate = [ float(i) for i in normalize(node['rotate']).strip().split(' ')] return original_rotate return [] def pop_rotation(node, original_rotate, rotate): """Removes the rotations of a node that are already used.""" node['rotate'] = ' '.join( str(rotate.pop(0) if rotate else original_rotate[-1]) for i in range(len(node.text))) def zip_letters(xl, yl, dxl, dyl, rl, word): """Returns a list with the current letter's positions (x, y and rotation). E.g.: for letter 'L' with positions x = 10, y = 20 and rotation = 30: >>> [[10, 20, 30], 'L'] Store the last value of each position and pop the first one in order to avoid setting an x,y or rotation value that have already been used. """ return ( ([pl.pop(0) if pl else None for pl in (xl, yl, dxl, dyl, rl)], char) for char in word) def flatten(node): """Flatten the text of a node and its children.""" flattened_text = [node.text or ''] for child in list(node): flattened_text.append(flatten(child)) flattened_text.append(child.tail or '') node.remove(child) return ''.join(flattened_text) def size(surface, string, reference='xy'): """Replace a ``string`` with units by a float value. If ``reference`` is a float, it is used as reference for percentages. If it is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we use the viewport height as reference. If it is ``'xy'``, we use ``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as reference. """ if not string: return 0 try: return float(string) except ValueError: # Not a float, try something else pass # No surface (for parsing only) if surface is None: return 0 string = normalize(string).split(' ', 1)[0] if string.endswith('%'): if reference == 'x': reference = surface.context_width or 0 elif reference == 'y': reference = surface.context_height or 0 elif reference == 'xy': reference = ( (surface.context_width ** 2 + surface.context_height ** 2) ** .5 / 2 ** .5) return float(string[:-1]) * reference / 100 elif string.endswith('em'): return surface.font_size * float(string[:-2]) elif string.endswith('ex'): # Assume that 1em == 2ex return surface.font_size * float(string[:-2]) / 2 for unit, coefficient in UNITS.items(): if string.endswith(unit): number = float(string[:-len(unit)]) return number * (surface.dpi * coefficient if coefficient else 1) # Unknown size return 0