453 lines
17 KiB
Python
453 lines
17 KiB
Python
"""
|
||
Paths manager.
|
||
|
||
"""
|
||
|
||
from math import copysign, hypot, pi, radians
|
||
|
||
from .bounding_box import calculate_bounding_box
|
||
from .helpers import (
|
||
PATH_LETTERS, clip_marker_box, node_format, normalize, point, point_angle,
|
||
preserve_ratio, quadratic_points, rotate, size)
|
||
from .url import parse_url
|
||
|
||
|
||
def draw_markers(surface, node):
|
||
"""Draw the markers attached to a path ``node``."""
|
||
if not getattr(node, 'vertices', None):
|
||
return
|
||
|
||
markers = {}
|
||
common_marker = parse_url(node.get('marker', '')).fragment
|
||
for position in ('start', 'mid', 'end'):
|
||
attribute = f'marker-{position}'
|
||
if attribute in node:
|
||
markers[position] = parse_url(node[attribute]).fragment
|
||
else:
|
||
markers[position] = common_marker
|
||
|
||
angle1, angle2 = None, None
|
||
position = 'start'
|
||
|
||
while node.vertices:
|
||
# Calculate position and angle
|
||
point = node.vertices.pop(0)
|
||
angles = node.vertices.pop(0) if node.vertices else None
|
||
if angles:
|
||
if position == 'start':
|
||
angle = pi - angles[0]
|
||
else:
|
||
angle = (angle2 + pi - angles[0]) / 2
|
||
angle1, angle2 = angles
|
||
else:
|
||
angle = angle2
|
||
position = 'end'
|
||
|
||
# Draw marker (if a marker exists for 'position')
|
||
marker = markers[position]
|
||
if marker:
|
||
marker_node = surface.markers.get(marker)
|
||
|
||
# Calculate scale based on current stroke (if requested)
|
||
if marker_node.get('markerUnits') == 'userSpaceOnUse':
|
||
scale = 1
|
||
else:
|
||
scale = size(
|
||
surface, surface.parent_node.get('stroke-width', '1'))
|
||
|
||
# Calculate position, (additional) scale and clipping based on
|
||
# marker properties
|
||
viewbox = node_format(surface, marker_node)[2]
|
||
if viewbox:
|
||
scale_x, scale_y, translate_x, translate_y = preserve_ratio(
|
||
surface, marker_node)
|
||
clip_box = clip_marker_box(
|
||
surface, marker_node, scale_x, scale_y)
|
||
else:
|
||
# Calculate sizes
|
||
marker_width = size(surface,
|
||
marker_node.get('markerWidth', '3'), 'x')
|
||
marker_height = size(surface,
|
||
marker_node.get('markerHeight', '3'), 'y')
|
||
bounding_box = calculate_bounding_box(surface, marker_node)
|
||
|
||
# Calculate position and scale (preserve aspect ratio)
|
||
translate_x = -size(surface, marker_node.get('refX', '0'), 'x')
|
||
translate_y = -size(surface, marker_node.get('refY', '0'), 'y')
|
||
scale_x = scale_y = min(
|
||
marker_width / bounding_box[2],
|
||
marker_height / bounding_box[3])
|
||
|
||
# No clipping since viewbox is not present
|
||
clip_box = None
|
||
|
||
# Add extra path for marker
|
||
temp_path = surface.context.copy_path()
|
||
surface.context.new_path()
|
||
|
||
# Override angle (if requested)
|
||
node_angle = marker_node.get('orient', '0')
|
||
if node_angle not in ('auto', 'auto-start-reverse'):
|
||
angle = radians(float(node_angle))
|
||
elif node_angle == 'auto-start-reverse' and position == 'start':
|
||
angle += radians(180)
|
||
|
||
# Draw marker path
|
||
# See http://www.w3.org/TR/SVG/painting.html#MarkerAlgorithm
|
||
for child in marker_node.children:
|
||
surface.context.save()
|
||
surface.context.translate(*point)
|
||
surface.context.rotate(angle)
|
||
surface.context.scale(scale)
|
||
surface.context.scale(scale_x, scale_y)
|
||
surface.context.translate(translate_x, translate_y)
|
||
|
||
# Add clipping (if present and requested)
|
||
overflow = marker_node.get('overflow', 'hidden')
|
||
if clip_box and overflow in ('hidden', 'scroll'):
|
||
surface.context.save()
|
||
surface.context.rectangle(*clip_box)
|
||
surface.context.restore()
|
||
surface.context.clip()
|
||
|
||
surface.draw(child)
|
||
surface.context.restore()
|
||
|
||
surface.context.append_path(temp_path)
|
||
|
||
position = 'mid' if angles else 'start'
|
||
|
||
|
||
def path(surface, node):
|
||
"""Draw a path ``node``."""
|
||
string = node.get('d', '')
|
||
|
||
node.vertices = []
|
||
|
||
for letter in PATH_LETTERS:
|
||
string = string.replace(letter, f' {letter} ')
|
||
|
||
last_letter = None
|
||
string = normalize(string)
|
||
|
||
# Keep the current point because Cairo's get_current_point is not accurate
|
||
# enough. See https://github.com/Kozea/CairoSVG/issues/111.
|
||
if surface.context.has_current_point():
|
||
current_point = surface.context.get_current_point()
|
||
else:
|
||
surface.context.move_to(0, 0)
|
||
current_point = 0, 0
|
||
|
||
while string:
|
||
string = string.strip()
|
||
if string.split(' ', 1)[0] in PATH_LETTERS:
|
||
letter, string = (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
|
||
surface.context.set_tolerance(0.00001)
|
||
x1, y1 = current_point
|
||
rx, ry, string = point(surface, string)
|
||
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(surface, string)
|
||
|
||
# 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)
|
||
|
||
if letter == 'A':
|
||
# Absolute x3 and y3, convert to relative
|
||
x3 -= x1
|
||
y3 -= y1
|
||
|
||
# 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
|
||
|
||
radii_ratio = ry / rx
|
||
|
||
# Cancel the rotation of the second point
|
||
xe, ye = rotate(x3, y3, -rotation)
|
||
ye /= radii_ratio
|
||
|
||
# Find the angle between the second point and the x axis
|
||
angle = point_angle(0, 0, xe, ye)
|
||
|
||
# Put the second point onto the x axis
|
||
xe = hypot(xe, ye)
|
||
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 not (large ^ sweep):
|
||
yc = -yc
|
||
|
||
# Define the arc sweep
|
||
arc = (
|
||
surface.context.arc if sweep else surface.context.arc_negative)
|
||
|
||
# Put the second point and the center back to their positions
|
||
xe, ye = rotate(xe, 0, angle)
|
||
xc, yc = rotate(xc, yc, angle)
|
||
|
||
# Find the drawing angles
|
||
angle1 = point_angle(xc, yc, 0, 0)
|
||
angle2 = point_angle(xc, yc, xe, ye)
|
||
|
||
# Store the tangent angles
|
||
node.vertices.append((-angle1, -angle2))
|
||
|
||
# Draw the arc
|
||
surface.context.save()
|
||
surface.context.translate(x1, y1)
|
||
surface.context.rotate(rotation)
|
||
surface.context.scale(1, radii_ratio)
|
||
arc(xc, yc, rx, angle1, angle2)
|
||
surface.context.restore()
|
||
current_point = current_point[0] + x3, current_point[1] + y3
|
||
|
||
elif letter == 'c':
|
||
# Relative curve
|
||
x, y = current_point
|
||
x1, y1, string = point(surface, string)
|
||
x2, y2, string = point(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
node.vertices.append((
|
||
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
|
||
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
|
||
current_point = current_point[0] + x3, current_point[1] + y3
|
||
|
||
# Save absolute values for x and y, useful if next letter is s or S
|
||
x1 += x
|
||
x2 += x
|
||
x3 += x
|
||
y1 += y
|
||
y2 += y
|
||
y3 += y
|
||
|
||
elif letter == 'C':
|
||
# Curve
|
||
x1, y1, string = point(surface, string)
|
||
x2, y2, string = point(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
node.vertices.append((
|
||
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
|
||
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
|
||
current_point = x3, y3
|
||
|
||
elif letter == 'h':
|
||
# Relative horizontal line
|
||
x, string = (string + ' ').split(' ', 1)
|
||
old_x, old_y = current_point
|
||
angle = 0 if size(surface, x, 'x') > 0 else pi
|
||
node.vertices.append((pi - angle, angle))
|
||
x = size(surface, x, 'x')
|
||
surface.context.rel_line_to(x, 0)
|
||
current_point = current_point[0] + x, current_point[1]
|
||
|
||
elif letter == 'H':
|
||
# Horizontal line
|
||
x, string = (string + ' ').split(' ', 1)
|
||
old_x, old_y = current_point
|
||
angle = 0 if size(surface, x, 'x') > old_x else pi
|
||
node.vertices.append((pi - angle, angle))
|
||
x = size(surface, x, 'x')
|
||
surface.context.line_to(x, old_y)
|
||
current_point = x, current_point[1]
|
||
|
||
elif letter == 'l':
|
||
# Relative straight line
|
||
x, y, string = point(surface, string)
|
||
angle = point_angle(0, 0, x, y)
|
||
node.vertices.append((pi - angle, angle))
|
||
surface.context.rel_line_to(x, y)
|
||
current_point = current_point[0] + x, current_point[1] + y
|
||
|
||
elif letter == 'L':
|
||
# Straight line
|
||
x, y, string = point(surface, string)
|
||
old_x, old_y = current_point
|
||
angle = point_angle(old_x, old_y, x, y)
|
||
node.vertices.append((pi - angle, angle))
|
||
surface.context.line_to(x, y)
|
||
current_point = x, y
|
||
|
||
elif letter == 'm':
|
||
# Current point relative move
|
||
x, y, string = point(surface, string)
|
||
if last_letter and last_letter not in 'zZ':
|
||
node.vertices.append(None)
|
||
surface.context.rel_move_to(x, y)
|
||
current_point = current_point[0] + x, current_point[1] + y
|
||
|
||
elif letter == 'M':
|
||
# Current point move
|
||
x, y, string = point(surface, string)
|
||
if last_letter and last_letter not in 'zZ':
|
||
node.vertices.append(None)
|
||
surface.context.move_to(x, y)
|
||
current_point = x, y
|
||
|
||
elif letter == 'q':
|
||
# Relative quadratic curve
|
||
x1, y1 = 0, 0
|
||
x2, y2, string = point(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
|
||
x1, y1, x2, y2, x3, y3)
|
||
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
|
||
node.vertices.append((0, 0))
|
||
current_point = current_point[0] + x3, current_point[1] + y3
|
||
|
||
elif letter == 'Q':
|
||
# Quadratic curve
|
||
x1, y1 = current_point
|
||
x2, y2, string = point(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
|
||
x1, y1, x2, y2, x3, y3)
|
||
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
|
||
node.vertices.append((0, 0))
|
||
current_point = x3, y3
|
||
|
||
elif letter == 's':
|
||
# Relative smooth curve
|
||
x, y = current_point
|
||
x1 = x3 - x2 if last_letter in 'csCS' else 0
|
||
y1 = y3 - y2 if last_letter in 'csCS' else 0
|
||
x2, y2, string = point(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
node.vertices.append((
|
||
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
|
||
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
|
||
current_point = current_point[0] + x3, current_point[1] + y3
|
||
|
||
# Save absolute values for x and y, useful if next letter is s or S
|
||
x1 += x
|
||
x2 += x
|
||
x3 += x
|
||
y1 += y
|
||
y2 += y
|
||
y3 += y
|
||
|
||
elif letter == 'S':
|
||
# 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(surface, string)
|
||
x3, y3, string = point(surface, string)
|
||
node.vertices.append((
|
||
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
|
||
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
|
||
current_point = x3, y3
|
||
|
||
elif letter == 't':
|
||
# Relative quadratic curve end
|
||
if last_letter not in 'QqTt':
|
||
x2, y2, x3, y3 = 0, 0, 0, 0
|
||
elif last_letter in 'QT':
|
||
x2 -= x1
|
||
y2 -= y1
|
||
x3 -= x1
|
||
y3 -= y1
|
||
x2 = x3 - x2
|
||
y2 = y3 - y2
|
||
x1, y1 = 0, 0
|
||
x3, y3, string = point(surface, string)
|
||
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
|
||
x1, y1, x2, y2, x3, y3)
|
||
node.vertices.append((0, 0))
|
||
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
|
||
current_point = current_point[0] + x3, current_point[1] + y3
|
||
|
||
elif letter == 'T':
|
||
# Quadratic curve end
|
||
abs_x, abs_y = current_point
|
||
if last_letter not in 'QqTt':
|
||
x2, y2, x3, y3 = abs_x, abs_y, abs_x, abs_y
|
||
elif last_letter in 'qt':
|
||
x2 += abs_x
|
||
y2 += abs_y
|
||
x3 += abs_x
|
||
y3 += abs_y
|
||
x2 = abs_x + (x3 - x2)
|
||
y2 = abs_y + (y3 - y2)
|
||
x1, y1 = abs_x, abs_y
|
||
x3, y3, string = point(surface, string)
|
||
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
|
||
x1, y1, x2, y2, x3, y3)
|
||
node.vertices.append((0, 0))
|
||
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
|
||
current_point = x3, y3
|
||
|
||
elif letter == 'v':
|
||
# Relative vertical line
|
||
y, string = (string + ' ').split(' ', 1)
|
||
old_x, old_y = current_point
|
||
angle = copysign(pi / 2, size(surface, y, 'y'))
|
||
node.vertices.append((-angle, angle))
|
||
y = size(surface, y, 'y')
|
||
surface.context.rel_line_to(0, y)
|
||
current_point = current_point[0], current_point[1] + y
|
||
|
||
elif letter == 'V':
|
||
# Vertical line
|
||
y, string = (string + ' ').split(' ', 1)
|
||
old_x, old_y = current_point
|
||
angle = copysign(pi / 2, size(surface, y, 'y') - old_y)
|
||
node.vertices.append((-angle, angle))
|
||
y = size(surface, y, 'y')
|
||
surface.context.line_to(old_x, y)
|
||
current_point = current_point[0], y
|
||
|
||
elif letter in 'zZ' and first_path_point:
|
||
# End of path
|
||
node.vertices.append(None)
|
||
surface.context.close_path()
|
||
current_point = first_path_point
|
||
|
||
if letter not in 'zZ':
|
||
node.vertices.append(current_point)
|
||
|
||
string = string.strip()
|
||
last_letter = letter
|