1# Copyright 2016 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import operator 16from fontTools.cu2qu import curve_to_quadratic, curves_to_quadratic 17from fontTools.pens.basePen import decomposeSuperBezierSegment 18from fontTools.pens.filterPen import FilterPen 19from fontTools.pens.reverseContourPen import ReverseContourPen 20from fontTools.pens.pointPen import BasePointToSegmentPen 21from fontTools.pens.pointPen import ReverseContourPointPen 22 23 24class Cu2QuPen(FilterPen): 25 """A filter pen to convert cubic bezier curves to quadratic b-splines 26 using the FontTools SegmentPen protocol. 27 28 Args: 29 30 other_pen: another SegmentPen used to draw the transformed outline. 31 max_err: maximum approximation error in font units. For optimal results, 32 if you know the UPEM of the font, we recommend setting this to a 33 value equal, or close to UPEM / 1000. 34 reverse_direction: flip the contours' direction but keep starting point. 35 stats: a dictionary counting the point numbers of quadratic segments. 36 all_quadratic: if True (default), only quadratic b-splines are generated. 37 if False, quadratic curves or cubic curves are generated depending 38 on which one is more economical. 39 """ 40 41 def __init__( 42 self, 43 other_pen, 44 max_err, 45 reverse_direction=False, 46 stats=None, 47 all_quadratic=True, 48 ): 49 if reverse_direction: 50 other_pen = ReverseContourPen(other_pen) 51 super().__init__(other_pen) 52 self.max_err = max_err 53 self.stats = stats 54 self.all_quadratic = all_quadratic 55 56 def _convert_curve(self, pt1, pt2, pt3): 57 curve = (self.current_pt, pt1, pt2, pt3) 58 result = curve_to_quadratic(curve, self.max_err, self.all_quadratic) 59 if self.stats is not None: 60 n = str(len(result) - 2) 61 self.stats[n] = self.stats.get(n, 0) + 1 62 if self.all_quadratic: 63 self.qCurveTo(*result[1:]) 64 else: 65 if len(result) == 3: 66 self.qCurveTo(*result[1:]) 67 else: 68 assert len(result) == 4 69 super().curveTo(*result[1:]) 70 71 def curveTo(self, *points): 72 n = len(points) 73 if n == 3: 74 # this is the most common case, so we special-case it 75 self._convert_curve(*points) 76 elif n > 3: 77 for segment in decomposeSuperBezierSegment(points): 78 self._convert_curve(*segment) 79 else: 80 self.qCurveTo(*points) 81 82 83class Cu2QuPointPen(BasePointToSegmentPen): 84 """A filter pen to convert cubic bezier curves to quadratic b-splines 85 using the FontTools PointPen protocol. 86 87 Args: 88 other_point_pen: another PointPen used to draw the transformed outline. 89 max_err: maximum approximation error in font units. For optimal results, 90 if you know the UPEM of the font, we recommend setting this to a 91 value equal, or close to UPEM / 1000. 92 reverse_direction: reverse the winding direction of all contours. 93 stats: a dictionary counting the point numbers of quadratic segments. 94 all_quadratic: if True (default), only quadratic b-splines are generated. 95 if False, quadratic curves or cubic curves are generated depending 96 on which one is more economical. 97 """ 98 99 __points_required = { 100 "move": (1, operator.eq), 101 "line": (1, operator.eq), 102 "qcurve": (2, operator.ge), 103 "curve": (3, operator.eq), 104 } 105 106 def __init__( 107 self, 108 other_point_pen, 109 max_err, 110 reverse_direction=False, 111 stats=None, 112 all_quadratic=True, 113 ): 114 BasePointToSegmentPen.__init__(self) 115 if reverse_direction: 116 self.pen = ReverseContourPointPen(other_point_pen) 117 else: 118 self.pen = other_point_pen 119 self.max_err = max_err 120 self.stats = stats 121 self.all_quadratic = all_quadratic 122 123 def _flushContour(self, segments): 124 assert len(segments) >= 1 125 closed = segments[0][0] != "move" 126 new_segments = [] 127 prev_points = segments[-1][1] 128 prev_on_curve = prev_points[-1][0] 129 for segment_type, points in segments: 130 if segment_type == "curve": 131 for sub_points in self._split_super_bezier_segments(points): 132 on_curve, smooth, name, kwargs = sub_points[-1] 133 bcp1, bcp2 = sub_points[0][0], sub_points[1][0] 134 cubic = [prev_on_curve, bcp1, bcp2, on_curve] 135 quad = curve_to_quadratic(cubic, self.max_err, self.all_quadratic) 136 if self.stats is not None: 137 n = str(len(quad) - 2) 138 self.stats[n] = self.stats.get(n, 0) + 1 139 new_points = [(pt, False, None, {}) for pt in quad[1:-1]] 140 new_points.append((on_curve, smooth, name, kwargs)) 141 if self.all_quadratic or len(new_points) == 2: 142 new_segments.append(["qcurve", new_points]) 143 else: 144 new_segments.append(["curve", new_points]) 145 prev_on_curve = sub_points[-1][0] 146 else: 147 new_segments.append([segment_type, points]) 148 prev_on_curve = points[-1][0] 149 if closed: 150 # the BasePointToSegmentPen.endPath method that calls _flushContour 151 # rotates the point list of closed contours so that they end with 152 # the first on-curve point. We restore the original starting point. 153 new_segments = new_segments[-1:] + new_segments[:-1] 154 self._drawPoints(new_segments) 155 156 def _split_super_bezier_segments(self, points): 157 sub_segments = [] 158 # n is the number of control points 159 n = len(points) - 1 160 if n == 2: 161 # a simple bezier curve segment 162 sub_segments.append(points) 163 elif n > 2: 164 # a "super" bezier; decompose it 165 on_curve, smooth, name, kwargs = points[-1] 166 num_sub_segments = n - 1 167 for i, sub_points in enumerate( 168 decomposeSuperBezierSegment([pt for pt, _, _, _ in points]) 169 ): 170 new_segment = [] 171 for point in sub_points[:-1]: 172 new_segment.append((point, False, None, {})) 173 if i == (num_sub_segments - 1): 174 # the last on-curve keeps its original attributes 175 new_segment.append((on_curve, smooth, name, kwargs)) 176 else: 177 # on-curves of sub-segments are always "smooth" 178 new_segment.append((sub_points[-1], True, None, {})) 179 sub_segments.append(new_segment) 180 else: 181 raise AssertionError("expected 2 control points, found: %d" % n) 182 return sub_segments 183 184 def _drawPoints(self, segments): 185 pen = self.pen 186 pen.beginPath() 187 last_offcurves = [] 188 points_required = self.__points_required 189 for i, (segment_type, points) in enumerate(segments): 190 if segment_type in points_required: 191 n, op = points_required[segment_type] 192 assert op(len(points), n), ( 193 f"illegal {segment_type!r} segment point count: " 194 f"expected {n}, got {len(points)}" 195 ) 196 offcurves = points[:-1] 197 if i == 0: 198 # any off-curve points preceding the first on-curve 199 # will be appended at the end of the contour 200 last_offcurves = offcurves 201 else: 202 for pt, smooth, name, kwargs in offcurves: 203 pen.addPoint(pt, None, smooth, name, **kwargs) 204 pt, smooth, name, kwargs = points[-1] 205 if pt is None: 206 assert segment_type == "qcurve" 207 # special quadratic contour with no on-curve points: 208 # we need to skip the "None" point. See also the Pen 209 # protocol's qCurveTo() method and fontTools.pens.basePen 210 pass 211 else: 212 pen.addPoint(pt, segment_type, smooth, name, **kwargs) 213 else: 214 raise AssertionError("unexpected segment type: %r" % segment_type) 215 for pt, smooth, name, kwargs in last_offcurves: 216 pen.addPoint(pt, None, smooth, name, **kwargs) 217 pen.endPath() 218 219 def addComponent(self, baseGlyphName, transformation): 220 assert self.currentPath is None 221 self.pen.addComponent(baseGlyphName, transformation) 222 223 224class Cu2QuMultiPen: 225 """A filter multi-pen to convert cubic bezier curves to quadratic b-splines 226 in a interpolation-compatible manner, using the FontTools SegmentPen protocol. 227 228 Args: 229 230 other_pens: list of SegmentPens used to draw the transformed outlines. 231 max_err: maximum approximation error in font units. For optimal results, 232 if you know the UPEM of the font, we recommend setting this to a 233 value equal, or close to UPEM / 1000. 234 reverse_direction: flip the contours' direction but keep starting point. 235 236 This pen does not follow the normal SegmentPen protocol. Instead, its 237 moveTo/lineTo/qCurveTo/curveTo methods take a list of tuples that are 238 arguments that would normally be passed to a SegmentPen, one item for 239 each of the pens in other_pens. 240 """ 241 242 # TODO Simplify like 3e8ebcdce592fe8a59ca4c3a294cc9724351e1ce 243 # Remove start_pts and _add_moveTO 244 245 def __init__(self, other_pens, max_err, reverse_direction=False): 246 if reverse_direction: 247 other_pens = [ 248 ReverseContourPen(pen, outputImpliedClosingLine=True) 249 for pen in other_pens 250 ] 251 self.pens = other_pens 252 self.max_err = max_err 253 self.start_pts = None 254 self.current_pts = None 255 256 def _check_contour_is_open(self): 257 if self.current_pts is None: 258 raise AssertionError("moveTo is required") 259 260 def _check_contour_is_closed(self): 261 if self.current_pts is not None: 262 raise AssertionError("closePath or endPath is required") 263 264 def _add_moveTo(self): 265 if self.start_pts is not None: 266 for pt, pen in zip(self.start_pts, self.pens): 267 pen.moveTo(*pt) 268 self.start_pts = None 269 270 def moveTo(self, pts): 271 self._check_contour_is_closed() 272 self.start_pts = self.current_pts = pts 273 self._add_moveTo() 274 275 def lineTo(self, pts): 276 self._check_contour_is_open() 277 self._add_moveTo() 278 for pt, pen in zip(pts, self.pens): 279 pen.lineTo(*pt) 280 self.current_pts = pts 281 282 def qCurveTo(self, pointsList): 283 self._check_contour_is_open() 284 if len(pointsList[0]) == 1: 285 self.lineTo([(points[0],) for points in pointsList]) 286 return 287 self._add_moveTo() 288 current_pts = [] 289 for points, pen in zip(pointsList, self.pens): 290 pen.qCurveTo(*points) 291 current_pts.append((points[-1],)) 292 self.current_pts = current_pts 293 294 def _curves_to_quadratic(self, pointsList): 295 curves = [] 296 for current_pt, points in zip(self.current_pts, pointsList): 297 curves.append(current_pt + points) 298 quadratics = curves_to_quadratic(curves, [self.max_err] * len(curves)) 299 pointsList = [] 300 for quadratic in quadratics: 301 pointsList.append(quadratic[1:]) 302 self.qCurveTo(pointsList) 303 304 def curveTo(self, pointsList): 305 self._check_contour_is_open() 306 self._curves_to_quadratic(pointsList) 307 308 def closePath(self): 309 self._check_contour_is_open() 310 if self.start_pts is None: 311 for pen in self.pens: 312 pen.closePath() 313 self.current_pts = self.start_pts = None 314 315 def endPath(self): 316 self._check_contour_is_open() 317 if self.start_pts is None: 318 for pen in self.pens: 319 pen.endPath() 320 self.current_pts = self.start_pts = None 321 322 def addComponent(self, glyphName, transformations): 323 self._check_contour_is_closed() 324 for trans, pen in zip(transformations, self.pens): 325 pen.addComponent(glyphName, trans) 326