• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Convert SVG Path's elliptical arcs to Bezier curves.
2
3The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic
4https://github.com/chromium/chromium/blob/93831f2/third_party/
5blink/renderer/core/svg/svg_path_parser.cc#L169-L278
6"""
7from __future__ import print_function, division, absolute_import, unicode_literals
8from fontTools.misc.py23 import *
9from fontTools.misc.py23 import isfinite
10from fontTools.misc.transform import Identity, Scale
11from math import atan2, ceil, cos, fabs, pi, radians, sin, sqrt, tan
12
13
14TWO_PI = 2 * pi
15PI_OVER_TWO = 0.5 * pi
16
17
18def _map_point(matrix, pt):
19    # apply Transform matrix to a point represented as a complex number
20    r = matrix.transformPoint((pt.real, pt.imag))
21    return r[0] + r[1] * 1j
22
23
24class EllipticalArc(object):
25
26    def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point):
27        self.current_point = current_point
28        self.rx = rx
29        self.ry = ry
30        self.rotation = rotation
31        self.large = large
32        self.sweep = sweep
33        self.target_point = target_point
34
35        # SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate
36        # uses radians
37        self.angle = radians(rotation)
38
39        # these derived attributes are computed by the _parametrize method
40        self.center_point = self.theta1 = self.theta2 = self.theta_arc = None
41
42    def _parametrize(self):
43        # convert from endopoint to center parametrization:
44        # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
45
46        # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
47        # "lineto") joining the endpoints.
48        # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
49        rx = fabs(self.rx)
50        ry = fabs(self.ry)
51        if not (rx and ry):
52            return False
53
54        # If the current point and target point for the arc are identical, it should
55        # be treated as a zero length path. This ensures continuity in animations.
56        if self.target_point == self.current_point:
57            return False
58
59        mid_point_distance = (self.current_point - self.target_point) * 0.5
60
61        point_transform = Identity.rotate(-self.angle)
62
63        transformed_mid_point = _map_point(point_transform, mid_point_distance)
64        square_rx = rx * rx
65        square_ry = ry * ry
66        square_x = transformed_mid_point.real * transformed_mid_point.real
67        square_y = transformed_mid_point.imag * transformed_mid_point.imag
68
69        # Check if the radii are big enough to draw the arc, scale radii if not.
70        # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
71        radii_scale = square_x / square_rx + square_y / square_ry
72        if radii_scale > 1:
73            rx *= sqrt(radii_scale)
74            ry *= sqrt(radii_scale)
75            self.rx, self.ry = rx, ry
76
77        point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle)
78
79        point1 = _map_point(point_transform, self.current_point)
80        point2 = _map_point(point_transform, self.target_point)
81        delta = point2 - point1
82
83        d = delta.real * delta.real + delta.imag * delta.imag
84        scale_factor_squared = max(1 / d - 0.25, 0.0)
85
86        scale_factor = sqrt(scale_factor_squared)
87        if self.sweep == self.large:
88            scale_factor = -scale_factor
89
90        delta *= scale_factor
91        center_point = (point1 + point2) * 0.5
92        center_point += complex(-delta.imag, delta.real)
93        point1 -= center_point
94        point2 -= center_point
95
96        theta1 = atan2(point1.imag, point1.real)
97        theta2 = atan2(point2.imag, point2.real)
98
99        theta_arc = theta2 - theta1
100        if theta_arc < 0 and self.sweep:
101            theta_arc += TWO_PI
102        elif theta_arc > 0 and not self.sweep:
103            theta_arc -= TWO_PI
104
105        self.theta1 = theta1
106        self.theta2 = theta1 + theta_arc
107        self.theta_arc = theta_arc
108        self.center_point = center_point
109
110        return True
111
112    def _decompose_to_cubic_curves(self):
113        if self.center_point is None and not self._parametrize():
114            return
115
116        point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry)
117
118        # Some results of atan2 on some platform implementations are not exact
119        # enough. So that we get more cubic curves than expected here. Adding 0.001f
120        # reduces the count of sgements to the correct count.
121        num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001))))
122        for i in range(num_segments):
123            start_theta = self.theta1 + i * self.theta_arc / num_segments
124            end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments
125
126            t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
127            if not isfinite(t):
128                return
129
130            sin_start_theta = sin(start_theta)
131            cos_start_theta = cos(start_theta)
132            sin_end_theta = sin(end_theta)
133            cos_end_theta = cos(end_theta)
134
135            point1 = complex(
136                cos_start_theta - t * sin_start_theta,
137                sin_start_theta + t * cos_start_theta,
138            )
139            point1 += self.center_point
140            target_point = complex(cos_end_theta, sin_end_theta)
141            target_point += self.center_point
142            point2 = target_point
143            point2 += complex(t * sin_end_theta, -t * cos_end_theta)
144
145            point1 = _map_point(point_transform, point1)
146            point2 = _map_point(point_transform, point2)
147            target_point = _map_point(point_transform, target_point)
148
149            yield point1, point2, target_point
150
151    def draw(self, pen):
152        for point1, point2, target_point in self._decompose_to_cubic_curves():
153            pen.curveTo(
154                (point1.real, point1.imag),
155                (point2.real, point2.imag),
156                (target_point.real, target_point.imag),
157            )
158