mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-09-17 15:37:34 +03:00
282 lines
9.8 KiB
Python
282 lines
9.8 KiB
Python
"""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 it’s 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
|