1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-09-11 20:47:56 +03:00
WeasyPrint/weasyprint/svg/utils.py

200 lines
6.8 KiB
Python
Raw Permalink Normal View History

2022-03-25 13:47:27 +03:00
"""Util functions for SVG rendering."""
2021-02-22 01:25:35 +03:00
import re
from contextlib import suppress
2021-04-16 16:33:04 +03:00
from math import cos, radians, sin, tan
2021-03-14 11:29:33 +03:00
from urllib.parse import urlparse
2021-02-22 01:25:35 +03:00
2021-04-12 16:21:43 +03:00
from tinycss2.color3 import parse_color
from ..matrix import Matrix
2021-02-22 01:25:35 +03:00
2021-07-30 17:08:19 +03:00
class PointError(Exception):
"""Exception raised when parsing a point fails."""
2021-02-22 01:25:35 +03:00
def normalize(string):
2021-04-12 18:19:46 +03:00
"""Give a canonical version of a given value string."""
string = (string or '').replace('E', 'e')
2021-02-22 01:25:35 +03:00
string = re.sub('(?<!e)-', ' -', string)
string = re.sub('[ \n\r\t,]+', ' ', string)
string = re.sub(r'(\.[0-9-]+)(?=\.)', r'\1 ', string)
return string.strip()
def size(string, font_size=None, percentage_reference=None):
2021-04-12 18:19:46 +03:00
"""Compute size from string, resolving units and percentages."""
from ..css.utils import LENGTHS_TO_PIXELS
2021-02-22 01:25:35 +03:00
if not string:
return 0
with suppress(ValueError):
2021-02-22 01:25:35 +03:00
return float(string)
# Not a float, try something else
2021-02-22 01:25:35 +03:00
string = normalize(string).split(' ', 1)[0]
if string.endswith('%'):
assert percentage_reference is not None
return float(string[:-1]) * percentage_reference / 100
elif string.endswith('rem'):
assert font_size is not None
return font_size * float(string[:-3])
2021-02-22 01:25:35 +03:00
elif string.endswith('em'):
assert font_size is not None
return font_size * float(string[:-2])
elif string.endswith('ex'):
# Assume that 1em == 2ex
assert font_size is not None
return font_size * float(string[:-2]) / 2
2021-04-12 18:19:46 +03:00
for unit, coefficient in LENGTHS_TO_PIXELS.items():
2021-02-22 01:25:35 +03:00
if string.endswith(unit):
2021-04-12 18:19:46 +03:00
return float(string[:-len(unit)]) * coefficient
2021-02-22 01:25:35 +03:00
# Unknown size
return 0
def alpha_value(value):
"""Return opacity between 0 and 1 from str, number or percentage."""
ratio = 1
if isinstance(value, str):
value = value.strip()
if value.endswith('%'):
ratio = 100
value = value[:-1].strip()
return min(1, max(0, float(value) / ratio))
def point(svg, string, font_size):
2021-04-12 18:19:46 +03:00
"""Pop first two size values from a string."""
match = re.match('(.*?) (.*?)(?: |$)', string)
2021-07-30 17:08:19 +03:00
if match:
x, y = match.group(1, 2)
string = string[match.end():]
return (*svg.point(x, y, font_size), string)
else:
raise PointError
2021-03-12 17:20:43 +03:00
def preserve_ratio(svg, node, font_size, width, height, viewbox=None):
2021-04-12 18:19:46 +03:00
"""Compute scale and translation needed to preserve ratio."""
viewbox = viewbox or node.get_viewbox()
2021-03-23 15:58:31 +03:00
if viewbox:
2021-03-14 11:29:33 +03:00
viewbox_width, viewbox_height = viewbox[2:]
2021-10-03 11:19:31 +03:00
elif svg.tree == node:
viewbox_width, viewbox_height = svg.get_intrinsic_size(font_size)
if None in (viewbox_width, viewbox_height):
return 1, 1, 0, 0
2021-10-03 11:19:31 +03:00
else:
return 1, 1, 0, 0
2021-03-14 11:29:33 +03:00
2021-03-23 15:58:31 +03:00
scale_x = width / viewbox_width if viewbox_width else 1
scale_y = height / viewbox_height if viewbox_height else 1
2021-03-14 11:29:33 +03:00
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':
2021-04-02 19:34:40 +03:00
translate_x, translate_y = svg.point(
node.get('refX'), node.get('refY', '0'), font_size)
2021-03-14 11:29:33 +03:00
else:
translate_x = 0
if x_position == 'mid':
2021-04-02 19:34:40 +03:00
translate_x = (width - viewbox_width * scale_x) / 2
2021-03-14 11:29:33 +03:00
elif x_position == 'max':
2021-04-02 19:34:40 +03:00
translate_x = width - viewbox_width * scale_x
2021-03-14 11:29:33 +03:00
translate_y = 0
if y_position == 'mid':
2021-04-02 19:34:40 +03:00
translate_y += (height - viewbox_height * scale_y) / 2
2021-03-14 11:29:33 +03:00
elif y_position == 'max':
2021-04-02 19:34:40 +03:00
translate_y += height - viewbox_height * scale_y
2021-03-14 11:29:33 +03:00
if viewbox:
translate_x -= viewbox[0] * scale_x
translate_y -= viewbox[1] * scale_y
2021-03-14 11:29:33 +03:00
return scale_x, scale_y, translate_x, translate_y
def parse_url(url):
2021-04-12 18:19:46 +03:00
"""Parse a URL, possibly in a "url(…)" string."""
2021-03-14 11:29:33 +03:00
if url and url.startswith('url(') and url.endswith(')'):
url = url[4:-1]
if len(url) >= 2:
2022-09-26 18:22:58 +03:00
for quote in ("'", '"'):
if url[0] == url[-1] == quote:
url = url[1:-1]
break
2021-03-14 11:29:33 +03:00
return urlparse(url or '')
2021-04-12 16:21:43 +03:00
2021-04-12 18:19:46 +03:00
def color(string):
"""Safely parse a color string and return a RGBA tuple."""
return parse_color(string or '') or (0, 0, 0, 1)
2021-04-16 16:33:04 +03:00
def transform(transform_string, font_size, normalized_diagonal):
"""Get a matrix corresponding to the transform string."""
2023-01-15 23:59:13 +03:00
# TODO: merge with gather_anchors and css.validation.properties.transform
2021-04-16 16:33:04 +03:00
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
2021-04-16 16:33:04 +03:00
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
2021-04-16 16:33:04 +03:00
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