208 lines
6.4 KiB
Python
208 lines
6.4 KiB
Python
# 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.
|
|
"""
|
|
|
|
|
|
from __future__ import print_function, division, absolute_import
|
|
|
|
from fontTools.pens.basePen import AbstractPen
|
|
|
|
from cu2qu import curve_to_quadratic, curves_to_quadratic
|
|
from cu2qu.pens import ReverseContourPen
|
|
|
|
__all__ = ['fonts_to_quadratic', 'font_to_quadratic']
|
|
|
|
DEFAULT_MAX_ERR = 0.0025
|
|
|
|
|
|
_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:
|
|
msg = 'Args to zip in cu2qu should have equal lengths: '
|
|
raise ValueError(msg + ' '.join(str(a) for a in 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]
|
|
|
|
# don't collect ufo2-style anchors
|
|
if tag in ['close', 'end'] and self.segments[-1][0] == 'move':
|
|
self.segments.pop()
|
|
else:
|
|
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)
|
|
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 == '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'
|
|
|
|
n = str(n)
|
|
if stats is not None:
|
|
stats[n] = stats.get(n, 0) + 1
|
|
|
|
return [('qcurve', p) for p in new_points]
|
|
|
|
|
|
def _fonts_to_quadratic(fonts, max_err, reverse_direction, stats):
|
|
"""Do the actual conversion of fonts, after arguments have been set up."""
|
|
|
|
for glyphs in zip(*fonts):
|
|
name = glyphs[0].name
|
|
assert all(g.name == name for g in glyphs), 'Incompatible fonts'
|
|
|
|
segments_by_location = zip(*[_get_segments(g) for g in glyphs])
|
|
if not any(segments_by_location):
|
|
continue
|
|
|
|
new_segments_by_location = []
|
|
for segments in segments_by_location:
|
|
tag = segments[0][0]
|
|
assert all(s[0] == tag for s in segments[1:]), (
|
|
'Incompatible glyphs "%s"' % name)
|
|
if tag == 'curve':
|
|
segments = _segments_to_quadratic(segments, max_err, stats)
|
|
new_segments_by_location.append(segments)
|
|
|
|
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)
|
|
|
|
|
|
def fonts_to_quadratic(
|
|
fonts, max_err_em=None, max_err=None, reverse_direction=False,
|
|
stats=None, dump_stats=False):
|
|
"""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.
|
|
"""
|
|
|
|
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)):
|
|
max_errors = max_err
|
|
elif isinstance(max_err_em, (list, tuple)):
|
|
max_errors = max_err_em
|
|
elif max_err:
|
|
max_errors = [max_err] * len(fonts)
|
|
else:
|
|
max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
|
|
|
|
num_fonts = len(fonts)
|
|
assert len(max_errors) == num_fonts
|
|
|
|
_fonts_to_quadratic(fonts, max_errors, reverse_direction, stats)
|
|
|
|
if dump_stats:
|
|
spline_lengths = sorted(stats.keys())
|
|
print('New spline lengths:\n%s\n' % (
|
|
'\n'.join('%s: %d' % (l, stats[l]) for l in spline_lengths)))
|
|
return stats
|
|
|
|
|
|
def font_to_quadratic(font, **kwargs):
|
|
"""Convenience wrapper around fonts_to_quadratic, for just one font."""
|
|
|
|
fonts_to_quadratic([font], **kwargs)
|