forked from Kozea/WeasyPrint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.py
189 lines (157 loc) · 6.51 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""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('(?<!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):
"""Compute size from string, resolving units and percentages."""
from ..css.utils import LENGTHS_TO_PIXELS
if not string:
return 0
with suppress(ValueError):
return float(string)
# Not a float, try something else
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])
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
for unit, coefficient in LENGTHS_TO_PIXELS.items():
if string.endswith(unit):
return float(string[:-len(unit)]) * coefficient
# Unknown size
return 0
def point(svg, string, font_size):
"""Pop first two size values from a string."""
match = re.match('(.*?) (.*?)(?: |$)', string)
if match:
x, y = match.group(1, 2)
string = string[match.end():]
return (*svg.point(x, y, font_size), string)
else:
raise PointError
def preserve_ratio(svg, node, font_size, width, height, viewbox=None):
"""Compute scale and translation needed to preserve ratio."""
viewbox = viewbox or node.get_viewbox()
if viewbox:
viewbox_width, viewbox_height = viewbox[2:]
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
else:
return 1, 1, 0, 0
scale_x = width / viewbox_width if viewbox_width else 1
scale_y = height / viewbox_height if viewbox_height 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, 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 Page._gather_links_and_bookmarks 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