|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""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"] |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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: |
|
|
|
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 |
|
else: |
|
raise NotImplementedError(curve_type) |
|
elif len(curve_types) > 1: |
|
|
|
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) |
|
|