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