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