1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-08-17 16:40:45 +03:00
WeasyPrint/weasyprint/svg/path.py
2022-05-17 16:40:30 +02:00

282 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Draw paths."""
from math import atan2, cos, isclose, pi, radians, sin, tan
from ..matrix import Matrix
from .utils import normalize, point
PATH_LETTERS = 'achlmqstvzACHLMQSTVZ'
def _rotate(x, y, angle):
"""Rotate (x, y) point of given angle around (0, 0)."""
return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle)
def path(svg, node, font_size):
"""Draw path node."""
string = node.get('d', '')
for letter in PATH_LETTERS:
string = string.replace(letter, f' {letter} ')
string = normalize(string)
# TODO: get current point
current_point = 0, 0
svg.stream.move_to(*current_point)
last_letter = None
while string:
string = string.strip()
if string.split(' ', 1)[0] in PATH_LETTERS:
letter, string = (f'{string} ').split(' ', 1)
if last_letter in (None, 'z', 'Z') and letter not in 'mM':
node.vertices.append(current_point)
first_path_point = current_point
elif letter == 'M':
letter = 'L'
elif letter == 'm':
letter = 'l'
if last_letter in (None, 'm', 'M', 'z', 'Z'):
first_path_point = None
if letter not in (None, 'm', 'M', 'z', 'Z') and (
first_path_point is None):
first_path_point = current_point
if letter in 'aA':
# Elliptic curve
# Drawn as an approximation using Bézier curves
x1, y1 = current_point
rx, ry, string = point(svg, string, font_size)
rotation, string = string.split(' ', 1)
rotation = radians(float(rotation))
# The large and sweep values are not always separated from the
# following values. These flags can only be 0 or 1, so reading a
# single digit suffices.
large, string = string[0], string[1:].strip()
sweep, string = string[0], string[1:].strip()
# Retrieve end point and set remainder (before checking flags)
x3, y3, string = point(svg, string, font_size)
if letter == 'a':
x3 += x1
y3 += y1
# Only allow 0 or 1 for flags
large, sweep = int(large), int(sweep)
if large not in (0, 1) or sweep not in (0, 1):
continue
large, sweep = bool(large), bool(sweep)
# rx=0 or ry=0 means straight line
if not rx or not ry:
if string and string[0] not in PATH_LETTERS:
# As we replace the current operation by l, we must be sure
# that the next letter is set to the real current letter (a
# or A) in case its omitted
next_letter = f'{letter} '
else:
next_letter = ''
string = f'L {x3} {y3} {next_letter}{string}'
continue
# Cancel the rotation of the second point
xe, ye = _rotate(x3 - x1, y3 - y1, -rotation)
y_scale = ry / rx
ye /= y_scale
# Find the angle between the second point and the x axis
angle = atan2(ye, xe)
# Put the second point onto the x axis
xe = (xe ** 2 + ye ** 2) ** .5
ye = 0
# Update the x radius if it is too small
rx = max(rx, xe / 2)
# Find one circle centre
xc = xe / 2
yc = (rx ** 2 - xc ** 2) ** .5
# Choose between the two circles according to flags
if large == sweep:
yc = -yc
# Put the second point and the center back to their positions
xe, ye = _rotate(xe, ye, angle)
xc, yc = _rotate(xc, yc, angle)
# Find the drawing angles
angle1 = atan2(-yc, -xc)
angle2 = atan2(ye - yc, xe - xc)
while angle1 < 0 or angle2 < 0:
angle1 += 2 * pi
angle2 += 2 * pi
# Store the tangent angles
node.vertices.append((-angle1, -angle2))
# Fix angles to follow large arc flag
if isclose(abs(angle2 - angle1), pi):
if sweep and (angle2 < angle1):
angle1 -= 2 * pi
elif not sweep and (angle2 > angle1):
angle2 -= 2 * pi
elif large == (abs(angle2 - angle1) < pi):
if angle1 > angle2:
angle1 -= 2 * pi
else:
angle2 -= 2 * pi
# Split arc into 3 Bézier curves when larger than pi
if large:
step = (angle2 - angle1) / 3
angles = (
(angle1, angle1 + step),
(angle1 + step, angle1 + 2 * step),
(angle1 + 2 * step, angle2))
else:
angles = ((angle1, angle2),)
# Draw Bézier curves
matrix = Matrix(
cos(rotation), sin(rotation),
-sin(rotation) * y_scale, cos(rotation) * y_scale,
x1, y1)
h = 4 / 3 * tan((angles[0][1] - angles[0][0]) / 4)
for angle1, angle2 in angles:
point1 = matrix.transform_point(
xc + rx * cos(angle1) - h * rx * sin(angle1),
yc + rx * sin(angle1) + h * rx * cos(angle1))
point2 = matrix.transform_point(
xc + rx * cos(angle2) + h * rx * sin(angle2),
yc + rx * sin(angle2) - h * rx * cos(angle2))
point3 = matrix.transform_point(
xc + rx * cos(angle2),
yc + rx * sin(angle2))
svg.stream.curve_to(*point1, *point2, *point3)
current_point = x3, y3
elif letter in 'cC':
# Curve
x1, y1, string = point(svg, string, font_size)
x2, y2, string = point(svg, string, font_size)
x3, y3, string = point(svg, string, font_size)
if letter == 'c':
x, y = current_point
x1 += x
x2 += x
x3 += x
y1 += y
y2 += y
y3 += y
node.vertices.append((
atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2)))
svg.stream.curve_to(x1, y1, x2, y2, x3, y3)
current_point = x3, y3
elif letter in 'hH':
# Horizontal line
x, string = (f'{string} ').split(' ', 1)
old_x, old_y = current_point
x, _ = svg.point(x, 0, font_size)
if letter == 'h':
x += old_x
angle = 0 if x > old_x else pi
node.vertices.append((pi - angle, angle))
svg.stream.line_to(x, old_y)
current_point = x, old_y
elif letter in 'lL':
# Straight line
x, y, string = point(svg, string, font_size)
old_x, old_y = current_point
if letter == 'l':
x += old_x
y += old_y
angle = atan2(y - old_y, x - old_x)
node.vertices.append((pi - angle, angle))
svg.stream.line_to(x, y)
current_point = x, y
elif letter in 'mM':
# Current point move
x, y, string = point(svg, string, font_size)
if last_letter and last_letter not in 'zZ':
node.vertices.append(None)
if letter == 'm':
x += current_point[0]
y += current_point[1]
svg.stream.move_to(x, y)
current_point = x, y
elif letter in 'qQtT':
# Quadratic curve
x1, y1 = current_point
if letter in 'qQ':
x2, y2, string = point(svg, string, font_size)
else:
if last_letter not in 'QqTt':
x2, y2, x3, y3 = x, y, x, y
x2 = x1 + x3 - x2
y2 = y1 + y3 - y2
x3, y3, string = point(svg, string, font_size)
if letter == 'q':
x2 += x1
y2 += y1
if letter in 'qt':
x3 += x1
y3 += y1
xq1 = x2 * 2 / 3 + x1 / 3
yq1 = y2 * 2 / 3 + y1 / 3
xq2 = x2 * 2 / 3 + x3 / 3
yq2 = y2 * 2 / 3 + y3 / 3
svg.stream.curve_to(xq1, yq1, xq2, yq2, x3, y3)
node.vertices.append((0, 0))
current_point = x3, y3
elif letter in 'sS':
# Smooth curve
x, y = current_point
x1 = x3 + (x3 - x2) if last_letter in 'csCS' else x
y1 = y3 + (y3 - y2) if last_letter in 'csCS' else y
x2, y2, string = point(svg, string, font_size)
x3, y3, string = point(svg, string, font_size)
if letter == 's':
x2 += x
x3 += x
y2 += y
y3 += y
node.vertices.append((
atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2)))
svg.stream.curve_to(x1, y1, x2, y2, x3, y3)
current_point = x3, y3
elif letter in 'vV':
# Vertical line
y, string = (f'{string} ').split(' ', 1)
old_x, old_y = current_point
_, y = svg.point(0, y, font_size)
if letter == 'v':
y += old_y
angle = pi / 2 if y > old_y else -pi / 2
node.vertices.append((pi - angle, angle))
svg.stream.line_to(old_x, y)
current_point = old_x, y
elif letter in 'zZ' and first_path_point:
# End of path
node.vertices.append(None)
svg.stream.close()
current_point = first_path_point
if letter not in 'zZ':
node.vertices.append(current_point)
string = string.strip()
last_letter = letter