Spaces:
Running
Running
# Copyright 2016 Google Inc. All Rights Reserved. | |
# Copyright 2023 Behdad Esfahbod. All Rights Reserved. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
from fontTools.qu2cu import quadratic_to_curves | |
from fontTools.pens.filterPen import ContourFilterPen | |
from fontTools.pens.reverseContourPen import ReverseContourPen | |
import math | |
class Qu2CuPen(ContourFilterPen): | |
"""A filter pen to convert quadratic bezier splines to cubic curves | |
using the FontTools SegmentPen protocol. | |
Args: | |
other_pen: another SegmentPen used to draw the transformed outline. | |
max_err: maximum approximation error in font units. For optimal results, | |
if you know the UPEM of the font, we recommend setting this to a | |
value equal, or close to UPEM / 1000. | |
reverse_direction: flip the contours' direction but keep starting point. | |
stats: a dictionary counting the point numbers of cubic segments. | |
""" | |
def __init__( | |
self, | |
other_pen, | |
max_err, | |
all_cubic=False, | |
reverse_direction=False, | |
stats=None, | |
): | |
if reverse_direction: | |
other_pen = ReverseContourPen(other_pen) | |
super().__init__(other_pen) | |
self.all_cubic = all_cubic | |
self.max_err = max_err | |
self.stats = stats | |
def _quadratics_to_curve(self, q): | |
curves = quadratic_to_curves(q, self.max_err, all_cubic=self.all_cubic) | |
if self.stats is not None: | |
for curve in curves: | |
n = str(len(curve) - 2) | |
self.stats[n] = self.stats.get(n, 0) + 1 | |
for curve in curves: | |
if len(curve) == 4: | |
yield ("curveTo", curve[1:]) | |
else: | |
yield ("qCurveTo", curve[1:]) | |
def filterContour(self, contour): | |
quadratics = [] | |
currentPt = None | |
newContour = [] | |
for op, args in contour: | |
if op == "qCurveTo" and ( | |
self.all_cubic or (len(args) > 2 and args[-1] is not None) | |
): | |
if args[-1] is None: | |
raise NotImplementedError( | |
"oncurve-less contours with all_cubic not implemented" | |
) | |
quadratics.append((currentPt,) + args) | |
else: | |
if quadratics: | |
newContour.extend(self._quadratics_to_curve(quadratics)) | |
quadratics = [] | |
newContour.append((op, args)) | |
currentPt = args[-1] if args else None | |
if quadratics: | |
newContour.extend(self._quadratics_to_curve(quadratics)) | |
if not self.all_cubic: | |
# Add back implicit oncurve points | |
contour = newContour | |
newContour = [] | |
for op, args in contour: | |
if op == "qCurveTo" and newContour and newContour[-1][0] == "qCurveTo": | |
pt0 = newContour[-1][1][-2] | |
pt1 = newContour[-1][1][-1] | |
pt2 = args[0] | |
if ( | |
pt1 is not None | |
and math.isclose(pt2[0] - pt1[0], pt1[0] - pt0[0]) | |
and math.isclose(pt2[1] - pt1[1], pt1[1] - pt0[1]) | |
): | |
newArgs = newContour[-1][1][:-1] + args | |
newContour[-1] = (op, newArgs) | |
continue | |
newContour.append((op, args)) | |
return newContour | |