From ca68822376ef5cfd0d58aea1fabacb82a23fce01 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Sun, 29 Nov 2020 22:43:10 +0100 Subject: [PATCH 01/10] Attempting to fix angles; consistency between cubic and quadratic arc cases --- cairosvg/path.py | 139 ++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 74 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index fe9784fb..b3afa6e3 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -35,9 +35,9 @@ def draw_markers(surface, node): angles = node.vertices.pop(0) if node.vertices else None if angles: if position == 'start': - angle = pi - angles[0] + angle = pi + angles[0] else: - angle = (angle2 + pi - angles[0]) / 2 + angle = (angle2 + pi + angles[0]) / 2 angle1, angle2 = angles else: angle = angle2 @@ -226,7 +226,8 @@ def path(surface, node): angle2 = point_angle(xc, yc, xe, ye) # Store the tangent angles - node.vertices.append((-angle1, -angle2)) + radius_to_tangent = pi/2 if sweep else -pi/2 + node.vertices.append((angle1 + radius_to_tangent, angle2 + radius_to_tangent)) # Draw the arc surface.context.save() @@ -243,8 +244,7 @@ def path(surface, node): 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))) + node.vertices.append((point_angle(0, 0, 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 @@ -258,31 +258,31 @@ def path(surface, node): elif letter == 'C': # 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))) + node.vertices.append((point_angle(x, y, 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') + old_x, old_y = current_point + angle = 0 if x > 0 else pi + node.vertices.append((angle, angle)) 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') + old_x, old_y = current_point + angle = 0 if x > old_x else pi + node.vertices.append((angle, angle)) surface.context.line_to(x, old_y) current_point = x, current_point[1] @@ -290,7 +290,7 @@ def path(surface, node): # Relative straight line x, y, string = point(surface, string) angle = point_angle(0, 0, x, y) - node.vertices.append((pi - angle, angle)) + node.vertices.append((angle, angle)) surface.context.rel_line_to(x, y) current_point = current_point[0] + x, current_point[1] + y @@ -299,7 +299,7 @@ def path(surface, node): 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)) + node.vertices.append((angle, angle)) surface.context.line_to(x, y) current_point = x, y @@ -321,37 +321,40 @@ def path(surface, node): elif letter == 'q': # Relative quadratic curve - x1, y1 = 0, 0 + x, y = current_point + x1, y1, string = point(surface, string) 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) + xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) 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 + node.vertices.append((point_angle(0, 0, x1, y1), point_angle(x1, y1, x2, y2))) + current_point = x + x2, y + y2 + + # Save absolute values for x and y, useful if next letter is t or T + x1 += x + x2 += x + y1 += y + y2 += y elif letter == 'Q': # Quadratic curve - x1, y1 = current_point + x, y = current_point + x1, y1, string = point(surface, string) 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) + xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - node.vertices.append((0, 0)) - current_point = x3, y3 + node.vertices.append((point_angle(x, y, x1, y1), point_angle(x1, y1, x2, y2))) + current_point = x2, y2 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 + x1 = x - x2 if last_letter in 'csCS' else 0 + y1 = y - 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))) + node.vertices.append((point_angle(0, 0, 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 + current_point = x + x3, y + y3 # Save absolute values for x and y, useful if next letter is s or S x1 += x @@ -364,71 +367,59 @@ def path(surface, node): 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 + x1 = x + (x - x2) if last_letter in 'csCS' else x + y1 = y + (y - 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))) + node.vertices.append((point_angle(x, y, 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)) + # Relative quadratic smooth curve + x, y = current_point + x1 = x - x1 if last_letter in 'qtQT' else 0 + y1 = y - y1 if last_letter in 'qtQT' else 0 + x2, y2, string = point(surface, string) + xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) + node.vertices.append((point_angle(0, 0, x1, y1), point_angle(x1, y1, x2, y2))) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - current_point = current_point[0] + x3, current_point[1] + y3 + current_point = x + x2, y + y2 + + # Save absolute values for x and y, useful if next letter is t or T + x1 += x + x2 += x + y1 += y + y2 += y 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)) + x, y = current_point + x1 = x + (x - x1) if last_letter in 'qtQT' else x + y1 = y + (y - y1) if last_letter in 'qtQT' else y + x2, y2, string = point(surface, string) + xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) + node.vertices.append((point_angle(x, y, x1, y1), point_angle(x1, y1, x2, y2))) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - current_point = x3, y3 + current_point = x2, y2 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') + old_x, old_y = current_point + angle = pi / 2 if y > 0 else -pi / 2 + node.vertices.append((angle, angle)) 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') + old_x, old_y = current_point + angle = pi / 2 if y > old_y else -pi / 2 + node.vertices.append((angle, angle)) surface.context.line_to(old_x, y) current_point = current_point[0], y From 6af8cd9e0dd666a24233e34498a53aed34d8f1bf Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Mon, 30 Nov 2020 22:35:48 +0100 Subject: [PATCH 02/10] Fix marker angle calculation (arithmetic mean may be 180 degrees off due to the angles wrapping around) --- cairosvg/path.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index b3afa6e3..e32ce823 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -3,7 +3,7 @@ """ -from math import pi, radians +from math import pi, radians, cos, sin from .bounding_box import calculate_bounding_box from .helpers import ( @@ -34,11 +34,13 @@ def draw_markers(surface, node): point = node.vertices.pop(0) angles = node.vertices.pop(0) if node.vertices else None if angles: + angle1 = angles[0] if position == 'start': - angle = pi + angles[0] + angle = angle1 else: - angle = (angle2 + pi + angles[0]) / 2 - angle1, angle2 = angles + # Bisect the angle difference by summing the corresponding unit vectors + angle = point_angle(0, 0, cos(angle1) + cos(angle2), sin(angle1) + sin(angle2)) + angle2 = angles[1] else: angle = angle2 position = 'end' From 87232ddba812465b65c4adeee4f8bda203b9a2b4 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Tue, 1 Dec 2020 21:26:38 +0100 Subject: [PATCH 03/10] Fix degenerate Beziers --- cairosvg/helpers.py | 14 ++++++++++++++ cairosvg/path.py | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/cairosvg/helpers.py b/cairosvg/helpers.py index c3fea7f8..5c332bae 100644 --- a/cairosvg/helpers.py +++ b/cairosvg/helpers.py @@ -153,6 +153,20 @@ def preserve_ratio(surface, node, width=None, height=None): return scale_x, scale_y, translate_x, translate_y +def bezier_angles(*points): + """Return the tangent angles of a Bezier curve of any degree.""" + if len(points) < 2: + # zero-length segment + return (0, 0) + # Control points that coincide with vertices can be removed + elif points[0] == points[1]: + return bezier_angles(*points[1:]) + elif points[-2] == points[-1]: + return bezier_angles(*points[:-1]) + else: + return (point_angle(*points[0], *points[1]), point_angle(*points[-2], *points[-1])) + + def clip_marker_box(surface, node, scale_x, scale_y): """Get the clip ``(x, y, width, height)`` of the marker box.""" width = size(surface, node.get('markerWidth', '3'), 'x') diff --git a/cairosvg/path.py b/cairosvg/path.py index e32ce823..9cea3960 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -7,8 +7,8 @@ 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) + PATH_LETTERS, bezier_angles, clip_marker_box, node_format, normalize, + point, point_angle, preserve_ratio, quadratic_points, rotate, size) from .url import parse_url @@ -246,7 +246,7 @@ def path(surface, node): x1, y1, string = point(surface, string) x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append((point_angle(0, 0, x1, y1), point_angle(x2, y2, x3, y3))) + node.vertices.append(bezier_angles((0, 0), (x1, y1), (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 @@ -264,7 +264,7 @@ def path(surface, node): x1, y1, string = point(surface, string) x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append((point_angle(x, y, x1, y1), point_angle(x2, y2, x3, y3))) + node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 @@ -328,7 +328,7 @@ def path(surface, node): x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - node.vertices.append((point_angle(0, 0, x1, y1), point_angle(x1, y1, x2, y2))) + node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2))) current_point = x + x2, y + y2 # Save absolute values for x and y, useful if next letter is t or T @@ -344,7 +344,7 @@ def path(surface, node): x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - node.vertices.append((point_angle(x, y, x1, y1), point_angle(x1, y1, x2, y2))) + node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2))) current_point = x2, y2 elif letter == 's': @@ -354,7 +354,7 @@ def path(surface, node): y1 = y - 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(0, 0, x1, y1), point_angle(x2, y2, x3, y3))) + node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2), (x3, y3))) surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3) current_point = x + x3, y + y3 @@ -373,7 +373,7 @@ def path(surface, node): y1 = y + (y - 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(x, y, x1, y1), point_angle(x2, y2, x3, y3))) + node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 @@ -384,7 +384,7 @@ def path(surface, node): y1 = y - y1 if last_letter in 'qtQT' else 0 x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) - node.vertices.append((point_angle(0, 0, x1, y1), point_angle(x1, y1, x2, y2))) + node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2))) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) current_point = x + x2, y + y2 @@ -401,7 +401,7 @@ def path(surface, node): y1 = y + (y - y1) if last_letter in 'qtQT' else y x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) - node.vertices.append((point_angle(x, y, x1, y1), point_angle(x1, y1, x2, y2))) + node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2))) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) current_point = x2, y2 From ba2759c05ca93d56598b4b6529a7d681ce8c478d Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Tue, 1 Dec 2020 21:42:11 +0100 Subject: [PATCH 04/10] Fix elliptic & rotated arc angles --- cairosvg/path.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index 9cea3960..ad5f8aea 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -3,7 +3,7 @@ """ -from math import pi, radians, cos, sin +from math import pi, radians, cos, sin, atan2 from .bounding_box import calculate_bounding_box from .helpers import ( @@ -228,8 +228,12 @@ def path(surface, node): angle2 = point_angle(xc, yc, xe, ye) # Store the tangent angles - radius_to_tangent = pi/2 if sweep else -pi/2 - node.vertices.append((angle1 + radius_to_tangent, angle2 + radius_to_tangent)) + tangent1 = angle1 + (pi/2 if sweep else -pi/2) + tangent2 = angle2 + (pi/2 if sweep else -pi/2) + if radii_ratio != 1: + tangent1 = atan2(radii_ratio*sin(tangent1), cos(tangent1)) + tangent2 = atan2(radii_ratio*sin(tangent2), cos(tangent2)) + node.vertices.append((tangent1 + rotation, tangent2 + rotation)) # Draw the arc surface.context.save() From 52e3993964f9c6195be78161c165307b0ace1994 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Thu, 3 Dec 2020 00:00:40 +0100 Subject: [PATCH 05/10] Fix angles for M and z by breaking node.vertices into subpaths --- cairosvg/path.py | 228 +++++++++++++++++++++++++---------------------- 1 file changed, 121 insertions(+), 107 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index ad5f8aea..efd4fa56 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -26,98 +26,109 @@ def draw_markers(surface, node): 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: - angle1 = angles[0] - if position == 'start': - angle = angle1 - else: - # Bisect the angle difference by summing the corresponding unit vectors - angle = point_angle(0, 0, cos(angle1) + cos(angle2), sin(angle1) + sin(angle2)) - angle2 = angles[1] - 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'): + subpath = node.vertices.pop(0) + angle1, angle2 = None, None + while subpath: + # Calculate position and angle + point = subpath.pop(0) + angles = subpath.pop(0) if subpath else None + if angle2 is None and len(subpath) % 2 == 0: + # Start of closed subpath: average angles of first and last segment + angle2 = subpath[-1][1] + + if angles: + angle1 = angles[0] + if angle2 is None: + # Start of unclosed subpath + angle = angle1 + else: + # Two adjoining segments: bisect the angle difference + # by summing the corresponding unit vectors + angle = point_angle(0, 0, cos(angle1) + cos(angle2), sin(angle1) + sin(angle2)) + angle2 = angles[1] + else: # End of unclosed subpath + angle = angle2 + + if not node.vertices and not subpath: + # Last node of path + 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.rectangle(*clip_box) + 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.clip() - - surface.draw(child) - surface.context.restore() - surface.context.append_path(temp_path) + surface.context.append_path(temp_path) - position = 'mid' if angles else 'start' + position = 'mid' def path(surface, node): @@ -139,13 +150,17 @@ def path(surface, node): else: surface.context.move_to(0, 0) current_point = 0, 0 + if string[0] not in 'Mm': + # Avoid index error for invalid paths that do not start with + # a moveto; should this raise an error or skip the path? + node.vertices.append([]) 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) + node.vertices[-1].append(current_point) first_path_point = current_point elif letter == 'M': letter = 'L' @@ -233,7 +248,7 @@ def path(surface, node): if radii_ratio != 1: tangent1 = atan2(radii_ratio*sin(tangent1), cos(tangent1)) tangent2 = atan2(radii_ratio*sin(tangent2), cos(tangent2)) - node.vertices.append((tangent1 + rotation, tangent2 + rotation)) + node.vertices[-1].append((tangent1 + rotation, tangent2 + rotation)) # Draw the arc surface.context.save() @@ -250,7 +265,7 @@ def path(surface, node): x1, y1, string = point(surface, string) x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2), (x3, y3))) + node.vertices[-1].append(bezier_angles((0, 0), (x1, y1), (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 @@ -268,7 +283,7 @@ def path(surface, node): x1, y1, string = point(surface, string) x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) + node.vertices[-1].append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 @@ -278,7 +293,7 @@ def path(surface, node): x = size(surface, x, 'x') old_x, old_y = current_point angle = 0 if x > 0 else pi - node.vertices.append((angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.rel_line_to(x, 0) current_point = current_point[0] + x, current_point[1] @@ -288,7 +303,7 @@ def path(surface, node): x = size(surface, x, 'x') old_x, old_y = current_point angle = 0 if x > old_x else pi - node.vertices.append((angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.line_to(x, old_y) current_point = x, current_point[1] @@ -296,7 +311,7 @@ def path(surface, node): # Relative straight line x, y, string = point(surface, string) angle = point_angle(0, 0, x, y) - node.vertices.append((angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.rel_line_to(x, y) current_point = current_point[0] + x, current_point[1] + y @@ -305,23 +320,21 @@ def path(surface, node): x, y, string = point(surface, string) old_x, old_y = current_point angle = point_angle(old_x, old_y, x, y) - node.vertices.append((angle, angle)) + node.vertices[-1].append((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) + node.vertices.append([]) 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) + node.vertices.append([]) surface.context.move_to(x, y) current_point = x, y @@ -332,7 +345,7 @@ def path(surface, node): x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2))) + node.vertices[-1].append(bezier_angles((0, 0), (x1, y1), (x2, y2))) current_point = x + x2, y + y2 # Save absolute values for x and y, useful if next letter is t or T @@ -348,7 +361,7 @@ def path(surface, node): x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) - node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2))) + node.vertices[-1].append(bezier_angles((x, y), (x1, y1), (x2, y2))) current_point = x2, y2 elif letter == 's': @@ -358,7 +371,7 @@ def path(surface, node): y1 = y - y2 if last_letter in 'csCS' else 0 x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2), (x3, y3))) + node.vertices[-1].append(bezier_angles((0, 0), (x1, y1), (x2, y2), (x3, y3))) surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3) current_point = x + x3, y + y3 @@ -377,7 +390,7 @@ def path(surface, node): y1 = y + (y - y2) if last_letter in 'csCS' else y x2, y2, string = point(surface, string) x3, y3, string = point(surface, string) - node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) + node.vertices[-1].append(bezier_angles((x, y), (x1, y1), (x2, y2), (x3, y3))) surface.context.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 @@ -388,7 +401,7 @@ def path(surface, node): y1 = y - y1 if last_letter in 'qtQT' else 0 x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(0, 0, x1, y1, x2, y2) - node.vertices.append(bezier_angles((0, 0), (x1, y1), (x2, y2))) + node.vertices[-1].append(bezier_angles((0, 0), (x1, y1), (x2, y2))) surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3) current_point = x + x2, y + y2 @@ -405,7 +418,7 @@ def path(surface, node): y1 = y + (y - y1) if last_letter in 'qtQT' else y x2, y2, string = point(surface, string) xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(x, y, x1, y1, x2, y2) - node.vertices.append(bezier_angles((x, y), (x1, y1), (x2, y2))) + node.vertices[-1].append(bezier_angles((x, y), (x1, y1), (x2, y2))) surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3) current_point = x2, y2 @@ -415,7 +428,7 @@ def path(surface, node): y = size(surface, y, 'y') old_x, old_y = current_point angle = pi / 2 if y > 0 else -pi / 2 - node.vertices.append((angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.rel_line_to(0, y) current_point = current_point[0], current_point[1] + y @@ -425,18 +438,19 @@ def path(surface, node): y = size(surface, y, 'y') old_x, old_y = current_point angle = pi / 2 if y > old_y else -pi / 2 - node.vertices.append((angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.line_to(old_x, y) current_point = current_point[0], y elif letter in 'zZ': # End of path - node.vertices.append(None) + angle = point_angle(*current_point, *first_path_point) + node.vertices[-1].append((angle, angle)) surface.context.close_path() current_point = first_path_point or (0, 0) if letter not in 'zZ': - node.vertices.append(current_point) + node.vertices[-1].append(current_point) string = string.strip() last_letter = letter From a2a06a8a2a42033a75c543aaf3195c9b5720bd36 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Thu, 17 Dec 2020 22:42:28 +0100 Subject: [PATCH 06/10] Draw two markers on top of one another for closepaths in accordance with the spec --- cairosvg/path.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index efd4fa56..6b1073e5 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -31,13 +31,17 @@ def draw_markers(surface, node): while node.vertices: subpath = node.vertices.pop(0) angle1, angle2 = None, None + + if len(subpath) % 2 == 0: + # Closed subpath: assign final angle to average with initial angle & + # append copy of first vertex and angle at the end + angle2 = subpath[-1][1] + subpath += subpath[:2] + while subpath: # Calculate position and angle point = subpath.pop(0) angles = subpath.pop(0) if subpath else None - if angle2 is None and len(subpath) % 2 == 0: - # Start of closed subpath: average angles of first and last segment - angle2 = subpath[-1][1] if angles: angle1 = angles[0] From 0835fc88780de1272eeb9181a02986d4289146cc Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Thu, 17 Dec 2020 23:08:25 +0100 Subject: [PATCH 07/10] Markers were wrongly sized if they didn't fit the whole viewport; I'm not sure if these scales in the non-viewBox case would ever be needed. --- cairosvg/path.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index 6b1073e5..ae11fa2c 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -81,21 +81,17 @@ def draw_markers(surface, node): clip_box = clip_marker_box( surface, marker_node, scale_x, scale_y) else: - # Calculate sizes + # Calculate sizes and position 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 + # No clipping or scaling since viewbox is not present + scale_x = scale_y = 1 clip_box = None # Add extra path for marker From 52dff3e8e98c9fc629aa195439f2da6e4d731ee6 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Tue, 29 Dec 2020 22:30:22 +0100 Subject: [PATCH 08/10] Sync change to Kozea/CairoSVG --- cairosvg/path.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index ae11fa2c..d171ccd0 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -203,7 +203,14 @@ def path(surface, node): # rx=0 or ry=0 means straight line if not rx or not ry: - string = 'l {} {} {}'.format(x3, y3, string) + 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 = '{} '.format(letter) + else: + next_letter = '' + string = 'l {} {} {}{}'.format(x3, y3, next_letter, string) continue continue radii_ratio = ry / rx From 5ebdd1831e1c61d3a954097f950652c28604cb1c Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Tue, 29 Dec 2020 22:32:31 +0100 Subject: [PATCH 09/10] m --- cairosvg/path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cairosvg/path.py b/cairosvg/path.py index d171ccd0..43136fa0 100644 --- a/cairosvg/path.py +++ b/cairosvg/path.py @@ -210,7 +210,7 @@ def path(surface, node): next_letter = '{} '.format(letter) else: next_letter = '' - string = 'l {} {} {}{}'.format(x3, y3, next_letter, string) continue + string = 'l {} {} {}{}'.format(x3, y3, next_letter, string) continue radii_ratio = ry / rx From 6395329305c2d5257f9c4c498b99f770af5479c3 Mon Sep 17 00:00:00 2001 From: SilverCardioid Date: Fri, 8 Jan 2021 14:44:13 +0100 Subject: [PATCH 10/10] Update .vertices for other shapes --- cairosvg/shapes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cairosvg/shapes.py b/cairosvg/shapes.py index 8e01f22d..f89615e4 100644 --- a/cairosvg/shapes.py +++ b/cairosvg/shapes.py @@ -45,13 +45,15 @@ def line(surface, node): surface.context.move_to(x1, y1) surface.context.line_to(x2, y2) angle = point_angle(x1, y1, x2, y2) - node.vertices = [(x1, y1), (pi - angle, angle), (x2, y2)] + node.vertices = [[(x1, y1), (angle, angle), (x2, y2)]] def polygon(surface, node): """Draw a polygon ``node`` on ``surface``.""" polyline(surface, node) surface.context.close_path() + closing_angle = point_angle(*node.vertices[-1][-1], *node.vertices[-1][0]) + node.vertices[-1].append((closing_angle, closing_angle)) def polyline(surface, node): @@ -60,14 +62,14 @@ def polyline(surface, node): if points: x, y, points = point(surface, points) surface.context.move_to(x, y) - node.vertices = [(x, y)] + node.vertices = [[(x, y)]] while points: x_old, y_old = x, y x, y, points = point(surface, points) angle = point_angle(x_old, y_old, x, y) - node.vertices.append((pi - angle, angle)) + node.vertices[-1].append((angle, angle)) surface.context.line_to(x, y) - node.vertices.append((x, y)) + node.vertices[-1].append((x, y)) def rect(surface, node):