File: //proc/1233/root/home/arjun/projects/aigenerator/venv/lib/python3.12/site-packages/cairosvg/path.py
"""
Paths manager.
"""
from math import 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 = (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 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 = pi / 2 if size(surface, y, 'y') > 0 else -pi / 2
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 = pi / 2 if size(surface, y, 'y') > old_y else -pi / 2
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