# Source code for fontTools.cu2qu.ufo

    # 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)