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