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