aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/cu2qu/ufo.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/cu2qu/ufo.py')
-rw-r--r--Lib/fontTools/cu2qu/ufo.py324
1 files changed, 324 insertions, 0 deletions
diff --git a/Lib/fontTools/cu2qu/ufo.py b/Lib/fontTools/cu2qu/ufo.py
new file mode 100644
index 00000000..447de7bb
--- /dev/null
+++ b/Lib/fontTools/cu2qu/ufo.py
@@ -0,0 +1,324 @@
+# 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):
+ """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)
+ 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
+
+ return [('qcurve', p) for p in new_points]
+
+
+def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats):
+ """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':
+ segments = _segments_to_quadratic(segments, max_err, stats)
+ glyphs_modified = True
+ 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):
+ """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)
+
+
+def fonts_to_quadratic(
+ fonts, max_err_em=None, max_err=None, reverse_direction=False,
+ stats=None, dump_stats=False, remember_curve_type=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 == "quadratic":
+ 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)
+ 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")
+ if curve_type != "quadratic":
+ font.lib[CURVE_TYPE_LIB_KEY] = "quadratic"
+ 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)