123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- # Copyright 2015 Google Inc. 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.
- """Converts cubic bezier curves to quadratic splines.
- Conversion is performed such that the quadratic splines keep the same end-curve
- tangents as the original cubics. The approach is iterative, increasing the
- number of segments for a spline until the error gets below a bound.
- Respective curves from multiple fonts will be converted at once to ensure that
- the resulting splines are interpolation-compatible.
- """
- import logging
- from fontTools.pens.basePen import AbstractPen
- from fontTools.pens.pointPen import PointToSegmentPen
- from fontTools.pens.reverseContourPen import ReverseContourPen
- from . import curves_to_quadratic
- from .errors import (
- UnequalZipLengthsError,
- IncompatibleSegmentNumberError,
- IncompatibleSegmentTypesError,
- IncompatibleGlyphsError,
- IncompatibleFontsError,
- )
- __all__ = ["fonts_to_quadratic", "font_to_quadratic"]
- # The default approximation error below is a relative value (1/1000 of the EM square).
- # Later on, we convert it to absolute font units by multiplying it by a font's UPEM
- # (see fonts_to_quadratic).
- DEFAULT_MAX_ERR = 0.001
- CURVE_TYPE_LIB_KEY = "com.github.googlei18n.cu2qu.curve_type"
- logger = logging.getLogger(__name__)
- _zip = zip
- def zip(*args):
- """Ensure each argument to zip has the same length. Also make sure a list is
- returned for python 2/3 compatibility.
- """
- if len(set(len(a) for a in args)) != 1:
- raise UnequalZipLengthsError(*args)
- return list(_zip(*args))
- class GetSegmentsPen(AbstractPen):
- """Pen to collect segments into lists of points for conversion.
- Curves always include their initial on-curve point, so some points are
- duplicated between segments.
- """
- def __init__(self):
- self._last_pt = None
- self.segments = []
- def _add_segment(self, tag, *args):
- if tag in ["move", "line", "qcurve", "curve"]:
- self._last_pt = args[-1]
- self.segments.append((tag, args))
- def moveTo(self, pt):
- self._add_segment("move", pt)
- def lineTo(self, pt):
- self._add_segment("line", pt)
- def qCurveTo(self, *points):
- self._add_segment("qcurve", self._last_pt, *points)
- def curveTo(self, *points):
- self._add_segment("curve", self._last_pt, *points)
- def closePath(self):
- self._add_segment("close")
- def endPath(self):
- self._add_segment("end")
- def addComponent(self, glyphName, transformation):
- pass
- def _get_segments(glyph):
- """Get a glyph's segments as extracted by GetSegmentsPen."""
- pen = GetSegmentsPen()
- # glyph.draw(pen)
- # We can't simply draw the glyph with the pen, but we must initialize the
- # PointToSegmentPen explicitly with outputImpliedClosingLine=True.
- # By default PointToSegmentPen does not outputImpliedClosingLine -- unless
- # last and first point on closed contour are duplicated. Because we are
- # converting multiple glyphs at the same time, we want to make sure
- # this function returns the same number of segments, whether or not
- # the last and first point overlap.
- # https://github.com/googlefonts/fontmake/issues/572
- # https://github.com/fonttools/fonttools/pull/1720
- pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=True)
- glyph.drawPoints(pointPen)
- return pen.segments
- def _set_segments(glyph, segments, reverse_direction):
- """Draw segments as extracted by GetSegmentsPen back to a glyph."""
- glyph.clearContours()
- pen = glyph.getPen()
- if reverse_direction:
- pen = ReverseContourPen(pen)
- for tag, args in segments:
- if tag == "move":
- pen.moveTo(*args)
- elif tag == "line":
- pen.lineTo(*args)
- elif tag == "curve":
- pen.curveTo(*args[1:])
- elif tag == "qcurve":
- pen.qCurveTo(*args[1:])
- elif tag == "close":
- pen.closePath()
- elif tag == "end":
- pen.endPath()
- else:
- raise AssertionError('Unhandled segment type "%s"' % tag)
- def _segments_to_quadratic(segments, max_err, stats, all_quadratic=True):
- """Return quadratic approximations of cubic segments."""
- assert all(s[0] == "curve" for s in segments), "Non-cubic given to convert"
- new_points = curves_to_quadratic([s[1] for s in segments], max_err, all_quadratic)
- n = len(new_points[0])
- assert all(len(s) == n for s in new_points[1:]), "Converted incompatibly"
- spline_length = str(n - 2)
- stats[spline_length] = stats.get(spline_length, 0) + 1
- if all_quadratic or n == 3:
- return [("qcurve", p) for p in new_points]
- else:
- return [("curve", p) for p in new_points]
- def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats, all_quadratic=True):
- """Do the actual conversion of a set of compatible glyphs, after arguments
- have been set up.
- Return True if the glyphs were modified, else return False.
- """
- try:
- segments_by_location = zip(*[_get_segments(g) for g in glyphs])
- except UnequalZipLengthsError:
- raise IncompatibleSegmentNumberError(glyphs)
- if not any(segments_by_location):
- return False
- # always modify input glyphs if reverse_direction is True
- glyphs_modified = reverse_direction
- new_segments_by_location = []
- incompatible = {}
- for i, segments in enumerate(segments_by_location):
- tag = segments[0][0]
- if not all(s[0] == tag for s in segments[1:]):
- incompatible[i] = [s[0] for s in segments]
- elif tag == "curve":
- new_segments = _segments_to_quadratic(
- segments, max_err, stats, all_quadratic
- )
- if all_quadratic or new_segments != segments:
- glyphs_modified = True
- segments = new_segments
- new_segments_by_location.append(segments)
- if glyphs_modified:
- new_segments_by_glyph = zip(*new_segments_by_location)
- for glyph, new_segments in zip(glyphs, new_segments_by_glyph):
- _set_segments(glyph, new_segments, reverse_direction)
- if incompatible:
- raise IncompatibleSegmentTypesError(glyphs, segments=incompatible)
- return glyphs_modified
- def glyphs_to_quadratic(
- glyphs, max_err=None, reverse_direction=False, stats=None, all_quadratic=True
- ):
- """Convert the curves of a set of compatible of glyphs to quadratic.
- All curves will be converted to quadratic at once, ensuring interpolation
- compatibility. If this is not required, calling glyphs_to_quadratic with one
- glyph at a time may yield slightly more optimized results.
- Return True if glyphs were modified, else return False.
- Raises IncompatibleGlyphsError if glyphs have non-interpolatable outlines.
- """
- if stats is None:
- stats = {}
- if not max_err:
- # assume 1000 is the default UPEM
- max_err = DEFAULT_MAX_ERR * 1000
- if isinstance(max_err, (list, tuple)):
- max_errors = max_err
- else:
- max_errors = [max_err] * len(glyphs)
- assert len(max_errors) == len(glyphs)
- return _glyphs_to_quadratic(
- glyphs, max_errors, reverse_direction, stats, all_quadratic
- )
- def fonts_to_quadratic(
- fonts,
- max_err_em=None,
- max_err=None,
- reverse_direction=False,
- stats=None,
- dump_stats=False,
- remember_curve_type=True,
- all_quadratic=True,
- ):
- """Convert the curves of a collection of fonts to quadratic.
- All curves will be converted to quadratic at once, ensuring interpolation
- compatibility. If this is not required, calling fonts_to_quadratic with one
- font at a time may yield slightly more optimized results.
- Return True if fonts were modified, else return False.
- By default, cu2qu stores the curve type in the fonts' lib, under a private
- key "com.github.googlei18n.cu2qu.curve_type", and will not try to convert
- them again if the curve type is already set to "quadratic".
- Setting 'remember_curve_type' to False disables this optimization.
- Raises IncompatibleFontsError if same-named glyphs from different fonts
- have non-interpolatable outlines.
- """
- if remember_curve_type:
- curve_types = {f.lib.get(CURVE_TYPE_LIB_KEY, "cubic") for f in fonts}
- if len(curve_types) == 1:
- curve_type = next(iter(curve_types))
- if curve_type in ("quadratic", "mixed"):
- logger.info("Curves already converted to quadratic")
- return False
- elif curve_type == "cubic":
- pass # keep converting
- else:
- raise NotImplementedError(curve_type)
- elif len(curve_types) > 1:
- # going to crash later if they do differ
- logger.warning("fonts may contain different curve types")
- if stats is None:
- stats = {}
- if max_err_em and max_err:
- raise TypeError("Only one of max_err and max_err_em can be specified.")
- if not (max_err_em or max_err):
- max_err_em = DEFAULT_MAX_ERR
- if isinstance(max_err, (list, tuple)):
- assert len(max_err) == len(fonts)
- max_errors = max_err
- elif max_err:
- max_errors = [max_err] * len(fonts)
- if isinstance(max_err_em, (list, tuple)):
- assert len(fonts) == len(max_err_em)
- max_errors = [f.info.unitsPerEm * e for f, e in zip(fonts, max_err_em)]
- elif max_err_em:
- max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
- modified = False
- glyph_errors = {}
- for name in set().union(*(f.keys() for f in fonts)):
- glyphs = []
- cur_max_errors = []
- for font, error in zip(fonts, max_errors):
- if name in font:
- glyphs.append(font[name])
- cur_max_errors.append(error)
- try:
- modified |= _glyphs_to_quadratic(
- glyphs, cur_max_errors, reverse_direction, stats, all_quadratic
- )
- except IncompatibleGlyphsError as exc:
- logger.error(exc)
- glyph_errors[name] = exc
- if glyph_errors:
- raise IncompatibleFontsError(glyph_errors)
- if modified and dump_stats:
- spline_lengths = sorted(stats.keys())
- logger.info(
- "New spline lengths: %s"
- % (", ".join("%s: %d" % (l, stats[l]) for l in spline_lengths))
- )
- if remember_curve_type:
- for font in fonts:
- curve_type = font.lib.get(CURVE_TYPE_LIB_KEY, "cubic")
- new_curve_type = "quadratic" if all_quadratic else "mixed"
- if curve_type != new_curve_type:
- font.lib[CURVE_TYPE_LIB_KEY] = new_curve_type
- modified = True
- return modified
- def glyph_to_quadratic(glyph, **kwargs):
- """Convenience wrapper around glyphs_to_quadratic, for just one glyph.
- Return True if the glyph was modified, else return False.
- """
- return glyphs_to_quadratic([glyph], **kwargs)
- def font_to_quadratic(font, **kwargs):
- """Convenience wrapper around fonts_to_quadratic, for just one font.
- Return True if the font was modified, else return False.
- """
- return fonts_to_quadratic([font], **kwargs)
|