"""Util functions for SVG rendering.""" import re from contextlib import suppress from math import cos, radians, sin, tan from urllib.parse import urlparse from tinycss2.color3 import parse_color from ..matrix import Matrix class PointError(Exception): """Exception raised when parsing a point fails.""" def normalize(string): """Give a canonical version of a given value string.""" string = (string or '').replace('E', 'e') string = re.sub('(? 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, translate_y = svg.point( node.get('refX'), node.get('refY', '0'), font_size) else: translate_x = 0 if x_position == 'mid': translate_x = (width - viewbox_width * scale_x) / 2 elif x_position == 'max': translate_x = width - viewbox_width * scale_x translate_y = 0 if y_position == 'mid': translate_y += (height - viewbox_height * scale_y) / 2 elif y_position == 'max': translate_y += height - viewbox_height * scale_y if viewbox: translate_x -= viewbox[0] * scale_x translate_y -= viewbox[1] * scale_y return scale_x, scale_y, translate_x, translate_y def parse_url(url): """Parse a URL, possibly in a "url(…)" string.""" if url and url.startswith('url(') and url.endswith(')'): url = url[4:-1] if len(url) >= 2: for quote in ("'", '"'): if url[0] == url[-1] == quote: url = url[1:-1] break return urlparse(url or '') def color(string): """Safely parse a color string and return a RGBA tuple.""" return parse_color(string or '') or (0, 0, 0, 1) def transform(transform_string, font_size, normalized_diagonal): """Get a matrix corresponding to the transform string.""" # TODO: merge with gather_anchors and css.validation.properties.transform transformations = re.findall( r'(\w+) ?\( ?(.*?) ?\)', normalize(transform_string)) matrix = Matrix() for transformation_type, transformation in transformations: values = [ size(value, font_size, normalized_diagonal) for value in transformation.split(' ')] if transformation_type == 'matrix': matrix = Matrix(*values) @ matrix elif transformation_type == 'rotate': if len(values) == 3: matrix = Matrix(e=values[1], f=values[2]) @ matrix matrix = Matrix( cos(radians(float(values[0]))), sin(radians(float(values[0]))), -sin(radians(float(values[0]))), cos(radians(float(values[0])))) @ matrix if len(values) == 3: matrix = Matrix(e=-values[1], f=-values[2]) @ matrix elif transformation_type.startswith('skew'): if len(values) == 1: values.append(0) if transformation_type in ('skewX', 'skew'): matrix = Matrix( c=tan(radians(float(values.pop(0))))) @ matrix if transformation_type in ('skewY', 'skew'): matrix = Matrix( b=tan(radians(float(values.pop(0))))) @ matrix elif transformation_type.startswith('translate'): if len(values) == 1: values.append(0) if transformation_type in ('translateX', 'translate'): matrix = Matrix(e=values.pop(0)) @ matrix if transformation_type in ('translateY', 'translate'): matrix = Matrix(f=values.pop(0)) @ matrix elif transformation_type.startswith('scale'): if len(values) == 1: values.append(values[0]) if transformation_type in ('scaleX', 'scale'): matrix = Matrix(a=values.pop(0)) @ matrix if transformation_type in ('scaleY', 'scale'): matrix = Matrix(d=values.pop(0)) @ matrix return matrix